frontend(welcome): revamp the zero-state intro modal

This commit is contained in:
Andras Schmelczer 2026-05-31 10:49:26 +01:00
parent bcca7f4f2e
commit 108cfb9a19

View file

@ -1,31 +1,97 @@
import { Component, ChangeDetectionStrategy, output } from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';
@Component({
selector: 'lt-welcome',
standalone: true,
imports: [],
imports: [A11yModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<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">
A visual TODO with bite. Each <strong>page</strong> is a context (work, hobbies, a project).
Each <strong>tower</strong> is a stack of related tasks. As you finish a task, it falls into the
tower as a colored square the more you do, the taller it grows.
<p class="lead" id="welcome-description">
Life Towers turns completed tasks into visible stacks. Create pages for work, hobbies,
home, or any project. Add towers for task groups, then check off tasks to build them
block by block.
</p>
<p class="muted">
Everything you write is saved to a small remote database, keyed to a private UUID token shown
under <em>Settings Account</em>. Copy that token to recover your data on another device, or
paste a friend's token to look at theirs.
<p class="sr-only">
Preview showing three towers with pending task bars at the top and completed task
blocks stacked below.
</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">
<button type="button" (click)="startFresh.emit()">Start fresh</button>
<button type="button" class="primary" (click)="loadExample.emit()">Try an example</button>
<button type="button" (click)="startFresh.emit()">Start empty</button>
<button type="button" class="primary" (click)="loadExample.emit()">Load sample towers</button>
</div>
</div>
`,
@ -34,18 +100,49 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
: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 {
@include card();
width: 66vw;
max-width: 480px;
@media (max-width: $mobile-width) { width: 300px; }
width: min(560px, calc(100vw - (2 * var(--large-padding))));
max-width: 560px;
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;
padding: var(--large-padding);
position: relative;
box-shadow: $shadow;
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));
h2,
h3,
p,
dl,
dd {
margin: 0;
}
.exit {
position: absolute;
top: var(--medium-padding);
@ -54,29 +151,231 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
}
h2 {
font-family: $title-font;
font-weight: 400;
font-size: var(--larger-font-size);
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 {
color: rgba($text-color, 0.7);
font-size: var(--medium-font-size);
em { font-style: italic; }
.tower-preview {
box-sizing: border-box;
padding: var(--medium-padding) 0;
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 {
display: flex;
justify-content: space-around;
align-items: flex-end;
flex-wrap: wrap;
gap: 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 {
color: $accent-color;
border-bottom-color: rgba($accent-color, 0.33);
&:after { background-color: $accent-color; }
}
@media (max-width: $mobile-width) {
gap: var(--small-padding);
}
}
}
`,