Modernise & deploy #1

Merged
andras merged 25 commits from asch/modernise into master 2026-05-31 16:48:45 +01:00
Showing only changes of commit 108cfb9a19 - Show all commits

View file

@ -1,31 +1,97 @@
import { Component, ChangeDetectionStrategy, output } from '@angular/core'; import { Component, ChangeDetectionStrategy, output } from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';
@Component({ @Component({
selector: 'lt-welcome', selector: 'lt-welcome',
standalone: true, standalone: true,
imports: [], imports: [A11yModule],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: ` template: `
<div class="welcome-card"> <div class="welcome-card">
<button class="exit" type="button" (click)="close.emit()" aria-label="Close"></button> <h2 id="welcome-title" tabindex="-1" cdkFocusInitial>Welcome to Life Towers</h2>
<h2>Welcome to Life Towers</h2> <button
class="exit"
type="button"
(click)="close.emit()"
aria-label="Dismiss welcome"
></button>
<p class="lead"> <p class="lead" id="welcome-description">
A visual TODO with bite. Each <strong>page</strong> is a context (work, hobbies, a project). Life Towers turns completed tasks into visible stacks. Create pages for work, hobbies,
Each <strong>tower</strong> is a stack of related tasks. As you finish a task, it falls into the home, or any project. Add towers for task groups, then check off tasks to build them
tower as a colored square the more you do, the taller it grows. block by block.
</p> </p>
<p class="muted"> <p class="sr-only">
Everything you write is saved to a small remote database, keyed to a private UUID token shown Preview showing three towers with pending task bars at the top and completed task
under <em>Settings Account</em>. Copy that token to recover your data on another device, or blocks stacked below.
paste a friend's token to look at theirs.
</p> </p>
<div class="tower-preview" aria-hidden="true">
<div class="preview-shell">
<div class="preview-page-tab"></div>
<div class="preview-towers">
<div class="preview-tower preview-tower--reading">
<span class="preview-task"></span>
<div class="preview-stack">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="preview-tower preview-tower--projects">
<span class="preview-task"></span>
<span class="preview-task preview-task--short"></span>
<div class="preview-stack">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="preview-tower preview-tower--exercise">
<span class="preview-task"></span>
<div class="preview-stack">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<h3 class="sr-only" id="welcome-basics-title">How Life Towers works</h3>
<dl class="basics" aria-labelledby="welcome-basics-title">
<div class="basic">
<dt class="basic__label">Pages</dt>
<dd class="basic__text">Keep each area separate.</dd>
</div>
<div class="basic">
<dt class="basic__label">Towers</dt>
<dd class="basic__text">Stack related tasks together.</dd>
</div>
<div class="basic">
<dt class="basic__label">Blocks</dt>
<dd class="basic__text">Finished tasks become colored blocks.</dd>
</div>
</dl>
<div class="actions"> <div class="actions">
<button type="button" (click)="startFresh.emit()">Start fresh</button> <button type="button" (click)="startFresh.emit()">Start empty</button>
<button type="button" class="primary" (click)="loadExample.emit()">Try an example</button> <button type="button" class="primary" (click)="loadExample.emit()">Load sample towers</button>
</div> </div>
</div> </div>
`, `,
@ -34,18 +100,49 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
:host { display: block; } :host { display: block; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.welcome-card { .welcome-card {
@include card(); @include card();
width: 66vw; width: min(560px, calc(100vw - (2 * var(--large-padding))));
max-width: 480px; max-width: 560px;
@media (max-width: $mobile-width) { width: 300px; } max-height: calc(100svh - (2 * var(--medium-padding)));
overflow-y: auto;
@media (max-width: $mobile-width) {
width: min(88vw, calc(100vw - (2 * var(--medium-padding))));
max-width: 88vw;
padding: var(--medium-padding);
}
box-sizing: border-box; box-sizing: border-box;
padding: var(--large-padding); padding: var(--large-padding);
position: relative; position: relative;
box-shadow: $shadow; box-shadow: $shadow;
text-align: left; text-align: left;
font-family: $normal-font;
font-weight: 300;
font-size: var(--medium-font-size);
line-height: 1.45;
@include inner-spacing(var(--medium-padding)); @include inner-spacing(var(--medium-padding));
h2,
h3,
p,
dl,
dd {
margin: 0;
}
.exit { .exit {
position: absolute; position: absolute;
top: var(--medium-padding); top: var(--medium-padding);
@ -54,29 +151,231 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
} }
h2 { h2 {
font-family: $title-font;
font-weight: 400;
font-size: var(--larger-font-size);
text-align: center; text-align: center;
margin-bottom: var(--medium-padding); padding: 0 36px;
line-height: 1.25;
@media (max-width: $mobile-width) {
padding: 0 28px;
}
} }
p.lead { color: $text-color; } .lead {
color: $text-color;
font-family: inherit;
font-weight: inherit;
font-size: inherit;
line-height: inherit;
max-width: 46ch;
margin-inline: auto;
text-align: center;
}
p.muted { .tower-preview {
color: rgba($text-color, 0.7); box-sizing: border-box;
font-size: var(--medium-font-size); padding: var(--medium-padding) 0;
em { font-style: italic; } border-top: 1px solid rgba($text-color, 0.08);
border-bottom: 1px solid rgba($text-color, 0.08);
@media (max-width: $mobile-width) {
padding-block: var(--small-padding);
}
}
.preview-shell {
width: min(100%, 370px);
margin: 0 auto;
}
.preview-page-tab {
width: 96px;
height: 14px;
margin: 0 auto var(--small-padding);
border-radius: var(--border-radius);
background: rgba($text-color, 0.08);
box-shadow: $shadow-border;
}
.preview-towers {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: end;
gap: var(--small-padding);
}
.preview-tower {
display: flex;
flex-direction: column;
min-height: 128px;
padding: 6px;
box-sizing: border-box;
border-radius: var(--border-radius);
background: rgba($text-color, 0.025);
box-shadow: $shadow-border;
}
.preview-task {
display: block;
width: 74%;
height: 5px;
margin-bottom: 4px;
border-radius: var(--border-radius);
background: var(--task-color);
}
.preview-task--short {
width: 48%;
}
.preview-stack {
display: flex;
flex-flow: row wrap-reverse;
align-content: flex-start;
gap: 2px;
min-height: 86px;
margin-top: auto;
span {
display: block;
flex: 0 0 calc((100% - 4px) / 3);
aspect-ratio: 1;
border-radius: 2px;
background: var(--block-color);
box-shadow: $shadow-border;
}
span:nth-child(2n) {
filter: saturate(0.9) brightness(1.06);
}
span:nth-child(3n) {
filter: saturate(1.08) brightness(0.96);
}
}
@media (prefers-reduced-motion: no-preference) {
@keyframes preview-task-pulse {
0%, 100% {
opacity: 0.42;
transform: scaleX(0.86);
}
45%, 70% {
opacity: 1;
transform: scaleX(1);
}
}
@keyframes preview-block-fall {
0% {
opacity: 0;
transform: translateY(-220%);
}
60%, 100% {
opacity: 1;
transform: translateY(0);
}
}
.preview-task {
transform-origin: left center;
animation: preview-task-pulse 2600ms ease-in-out infinite;
}
.preview-stack span {
opacity: 0;
animation: preview-block-fall 1200ms cubic-bezier(0.5, 0, 1, 0) forwards;
animation-delay: calc(160ms + var(--fall-index, 0) * 95ms);
}
.preview-stack span:nth-child(1) { --fall-index: 0; }
.preview-stack span:nth-child(2) { --fall-index: 1; }
.preview-stack span:nth-child(3) { --fall-index: 2; }
.preview-stack span:nth-child(4) { --fall-index: 3; }
.preview-stack span:nth-child(5) { --fall-index: 4; }
.preview-stack span:nth-child(6) { --fall-index: 5; }
.preview-stack span:nth-child(7) { --fall-index: 6; }
.preview-stack span:nth-child(8) { --fall-index: 7; }
.preview-stack span:nth-child(9) { --fall-index: 8; }
}
.preview-tower--reading {
--block-color: hsl(18, 70%, 58%);
--task-color: hsla(18, 70%, 58%, 0.26);
}
.preview-tower--projects {
--block-color: hsl(209, 65%, 52%);
--task-color: hsla(209, 65%, 52%, 0.24);
}
.preview-tower--exercise {
--block-color: hsl(130, 45%, 44%);
--task-color: hsla(130, 45%, 44%, 0.22);
}
.basics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--small-padding);
@media (max-width: $mobile-width) {
grid-template-columns: 1fr;
}
}
.basic {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.basic__label,
.basic__text {
font-family: inherit;
font-weight: inherit;
font-size: inherit;
line-height: inherit;
}
.basic__label {
color: $text-color;
}
.basic__text {
color: rgba($text-color, 0.82);
} }
.actions { .actions {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: flex-end;
flex-wrap: wrap;
gap: var(--large-padding); gap: var(--large-padding);
margin-top: var(--large-padding); margin-top: var(--large-padding);
button {
font-family: inherit;
font-weight: inherit;
font-size: inherit;
margin: 0;
max-width: 100%;
line-height: inherit;
text-align: center;
}
button.primary { button.primary {
color: $accent-color; color: $accent-color;
border-bottom-color: rgba($accent-color, 0.33); border-bottom-color: rgba($accent-color, 0.33);
&:after { background-color: $accent-color; } &:after { background-color: $accent-color; }
} }
@media (max-width: $mobile-width) {
gap: var(--small-padding);
}
} }
} }
`, `,