frontend(welcome): revamp the zero-state intro modal
This commit is contained in:
parent
bcca7f4f2e
commit
108cfb9a19
1 changed files with 321 additions and 22 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue