@usableapp/cardds 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +31 -11
- package/css/actions.css +14 -23
- package/css/layover.css +48 -18
- package/css/lists.css +3 -0
- package/css/media.css +2 -0
- package/css/stack.css +19 -1
- package/css/step.css +2 -8
- package/dist/actions/Chip.d.ts +3 -1
- package/dist/actions/Chip.js +2 -2
- package/dist/actions/Dropdown.d.ts +3 -1
- package/dist/actions/Dropdown.js +2 -2
- package/dist/cardds.css +88 -50
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layover/Banner.d.ts +5 -6
- package/dist/layover/Banner.js +5 -3
- package/dist/lists/Row.d.ts +4 -2
- package/dist/lists/Row.js +6 -4
- package/dist/scaffold/ActionBar.d.ts +2 -2
- package/dist/scaffold/ActionBar.js +1 -1
- package/dist/sheets/StackSheet.js +21 -2
- package/dist/templates/Splash.d.ts +22 -0
- package/dist/templates/Splash.js +32 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
> For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
|
|
4
4
|
|
|
5
|
+
## 0.7.2 — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### `Banner` is one light band (breaking)
|
|
8
|
+
- Before: `summary` was a `Chip on` (with its chevron Icon) that opened a `card--3` body under it; `tone` 2 | 3.
|
|
9
|
+
After: ONE band — a light tint (`--ink-tint`), a hairline above and below, edge to edge of its card or screen; the
|
|
10
|
+
summary line and the body share it. `summary` = words (the chevron is the Banner's own); `tone` is gone. Fix:
|
|
11
|
+
`<Banner summary={<Chip on>Save 2 h… <Icon …/></Chip>}>` → `<Banner summary="Save 2 h…">`. Knobs `--banner-bg`,
|
|
12
|
+
`--banner-border`, `--banner-summary-pad-y`, `--banner-summary-font`. A read-only note inside a sheet or card is a
|
|
13
|
+
`Banner`, not a `Card sm`.
|
|
14
|
+
|
|
15
|
+
### New
|
|
16
|
+
- `Splash` (template): the app's own mark (its Lottie player, an img — cardds plays nothing) + one `text` line, the group's
|
|
17
|
+
middle on `--screen-centre`; `done` (the app is ready) · `min` (ms at least) · `onLeft` (after the fade; unmount then);
|
|
18
|
+
reduced motion = no fade. Knobs `--splash-mark-w`, `--splash-mark-x` / `-y` (nudge a lottie's drawing onto the line),
|
|
19
|
+
`--splash-at`, `--splash-bg`, `--splash-text-ink`, `--splash-gap`.
|
|
20
|
+
- `Rows heading` — a list grouped (Today · This week), not spaced out. Knobs `--rows-head-ink`, `--rows-group-gap`.
|
|
21
|
+
- `Chip muted`, `Dropdown muted` — an untouched default reads quiet; drop `muted` once the member picks.
|
|
22
|
+
- README rules 11–14: a screen is scanned, not read — edges, show don't explain, quiet defaults, a frame says "tap me"
|
|
23
|
+
(inside a card only a control has an edge; a count is `Text caption muted`, never a `Chip`).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `Modal` with no `Drawer` stretched to the whole stage in Safari 18 (iOS 18.6): it is centred by `top: 50%` +
|
|
27
|
+
`translate: -50%` now.
|
|
28
|
+
- `Tile stat` has no frame (it is read, not pressed).
|
|
29
|
+
- `StackSheet` head: tabs 8px apart (`--stack-sheet-tabs-gap`), the title on the discs' middle
|
|
30
|
+
(`--stack-sheet-title-shift`), ONE line always — a title that does not fit runs sideways, one way, and starts again
|
|
31
|
+
(`--stack-sheet-run-time`); reduced motion = an ellipsis.
|
|
32
|
+
|
|
33
|
+
## 0.7.1 — 2026-09-24
|
|
34
|
+
|
|
35
|
+
### The bar fixed from outside is gone
|
|
36
|
+
- Before: an `ActionBar` written after `</Screen>` (in `<body>` or an app shell) was fixed to the viewport, and the screen
|
|
37
|
+
/ step kept `--bar-reserve` of room for it. After: `.action-bar` is in flow; a bar is always the Screen's `bottom` (its
|
|
38
|
+
last child) — a layer over the content's foot in a fill screen, a sticky last row on a scrolling one (0.7.0). A bar left
|
|
39
|
+
outside a Screen now sits wherever it was written. `--bar-reserve` is still published (the centred card's cap reads it);
|
|
40
|
+
nothing reserves room with it any more. `ActionBar inline` stays, for a bare bar in a preview.
|
|
41
|
+
|
|
5
42
|
## 0.7.0 — 2026-09-24
|
|
6
43
|
|
|
7
44
|
Breaking — the clean-up promised since 0.6.5. Four renames/removals, one new way.
|
package/README.md
CHANGED
|
@@ -410,17 +410,9 @@ Demo: the *skeleton* block at the top of `elements.html`.
|
|
|
410
410
|
5. **`.screen` carries the horizontal inset and nothing vertical.** The inset
|
|
411
411
|
is `--screen-pad`; cards and on-surface UI sit flush inside it, with no
|
|
412
412
|
per-component side padding. Top and bottom are zero: vertical room only
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
parent hosts the bar counts (`:has(> .action-bar) .screen`): `<body>` or an
|
|
417
|
-
app shell alike. A page with no bars needs no override, and `.screen--fill`
|
|
418
|
-
is excluded from the bottom reserves — there a `.step` carries the bar's
|
|
419
|
-
room itself, as a bottom margin, reading the same `--bar-reserve`. The
|
|
420
|
-
reserves use the nominal control height, so a bar whose button wraps to two
|
|
421
|
-
lines can still overlap the last line of content.
|
|
422
|
-
**In a `.screen--fill` none of this applies to a bar in the screen** (2026-09-23): it is the column's last
|
|
423
|
-
part and the browser measures it — the reserves are the old way for a bar outside a fill screen.
|
|
413
|
+
is taken by what is in the column. The bottom bar is always the screen's last child (0.7.1 — the bar fixed to
|
|
414
|
+
the viewport from outside and its reserves are gone): in a `.screen--fill` a layer over the content's foot, on a
|
|
415
|
+
scrolling screen a `sticky` last row. `--bar-reserve` stays published only for a `.card--centre`'s cap.
|
|
424
416
|
**Padding is published, not copied.** `.screen` sets `--screen-pad` and
|
|
425
417
|
`--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
|
|
426
418
|
`--card-pad` and `--card-gap`; anything that has to cancel a container's
|
|
@@ -505,6 +497,34 @@ Demo: the *skeleton* block at the top of `elements.html`.
|
|
|
505
497
|
`--on-accent` to its own ink and bg, so every accent consumer (buttons,
|
|
506
498
|
checks, toggles, rings, selected days) inverts without extra rules.
|
|
507
499
|
|
|
500
|
+
**A screen is scanned, not read** (Lh 2026-09-24) — a member arrives with one
|
|
501
|
+
question and hunts for its answer. Rules 11–13 serve that:
|
|
502
|
+
|
|
503
|
+
11. **Everything holds to edges.** Every element touches at least two edges —
|
|
504
|
+
the card's, the column's, or a line made by the thing above it (a row, the
|
|
505
|
+
avatar + name line). Words to READ are aligned start, values end (the
|
|
506
|
+
receipt: items left, prices right; `.card__head`, `.kv`, `.row`); centred
|
|
507
|
+
only what is not read — dots, a lone number, an icon, a mark. Empty room is
|
|
508
|
+
not filled with more content; a new edge is made instead (line the buttons
|
|
509
|
+
up with the avatar).
|
|
510
|
+
12. **Show, don't explain.** Where a known picture carries it — an avatar for a
|
|
511
|
+
person, an icon, a `Ring`/`Track`/`Band`, a `Chip` for a category — use it
|
|
512
|
+
instead of words. Never add a label, a caption line, a comparison or a
|
|
513
|
+
tooltip to explain what the picture already says (the retired overline was
|
|
514
|
+
that mistake). Long lists are differentiated, not spaced out: group them
|
|
515
|
+
(`Rows heading` per group), put the avatar where a person is named.
|
|
516
|
+
13. **Emphasis is the difference from the neighbours.** An untouched default
|
|
517
|
+
is quiet (grey, thin border, no fill — `Chip muted`, `Dropdown muted`);
|
|
518
|
+
only what the member changed, or the one thing asked of them, takes the
|
|
519
|
+
accent. To make one thing stand out, calm what surrounds it — never
|
|
520
|
+
bolder, never a second colour.
|
|
521
|
+
14. **A frame says "tap me".** Inside a card, only a control draws its own
|
|
522
|
+
edge (a chip, a field, a pick tile, a small `Card sm` to pick). What is
|
|
523
|
+
only READ never gets a frame — no card in a card: a stat tile stands on
|
|
524
|
+
the card's ground, a note is a `Banner` (a light band, a rule above and
|
|
525
|
+
below, edge to edge), a count is words (`Text caption muted`), never a
|
|
526
|
+
chip. A chip or tile that looks pressable and is not lies.
|
|
527
|
+
|
|
508
528
|
## A project's theme
|
|
509
529
|
|
|
510
530
|
A project retunes cardds from ONE file of its own, and nothing else. Copy
|
package/css/actions.css
CHANGED
|
@@ -158,6 +158,8 @@
|
|
|
158
158
|
letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
|
|
159
159
|
.dot--b { background: var(--dot-b); }
|
|
160
160
|
.chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
|
|
161
|
+
/* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (README 13) */
|
|
162
|
+
.chip--muted { color: var(--chip-muted-ink, var(--card-muted)); }
|
|
161
163
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
162
164
|
|
|
163
165
|
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
|
@@ -187,6 +189,8 @@
|
|
|
187
189
|
text-align: left;
|
|
188
190
|
cursor: pointer;
|
|
189
191
|
}
|
|
192
|
+
/* muted: the value is still the default (README 13) */
|
|
193
|
+
.dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--card-muted)); }
|
|
190
194
|
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
191
195
|
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
192
196
|
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
@@ -216,21 +220,15 @@
|
|
|
216
220
|
opacity: 1;
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
/* ---- bottom CTA pack:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
the
|
|
225
|
-
--kb is the on-screen keyboard's height, published on :root by cardds.js
|
|
226
|
-
from the visual viewport (0 without one, and on a platform that shrinks
|
|
227
|
-
the layout viewport itself): the bar rides above the keyboard on iOS the
|
|
228
|
-
way it already does on Android, and .step grows its room by the same. */
|
|
223
|
+
/* ---- bottom CTA pack: the screen's bottom (Screen `bottom`) ----
|
|
224
|
+
A bar is always INSIDE its .screen, its last child (0.7.1: the bar fixed to the viewport from outside, and the
|
|
225
|
+
reserves the screen and the step kept for it, are gone). In a .screen--fill it is a layer over the content's foot
|
|
226
|
+
(step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
|
|
227
|
+
height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
|
|
228
|
+
--kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
|
|
229
229
|
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
230
230
|
.action-bar {
|
|
231
|
-
position:
|
|
232
|
-
inset-inline: 0;
|
|
233
|
-
bottom: var(--kb, 0);
|
|
231
|
+
position: relative;
|
|
234
232
|
z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
|
|
235
233
|
max-width: var(--screen-w);
|
|
236
234
|
margin-inline: auto;
|
|
@@ -241,9 +239,9 @@
|
|
|
241
239
|
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
|
|
242
240
|
last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
|
|
243
241
|
it takes the room it needs in the column, so nothing is added to the content for it */
|
|
244
|
-
.screen:not(.screen--fill):has(> .action-bar) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
245
|
-
.screen:not(.screen--fill) > .action-bar {
|
|
246
|
-
position: sticky;
|
|
242
|
+
.screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
243
|
+
.screen:not(.screen--fill) > .action-bar:last-child {
|
|
244
|
+
position: sticky; bottom: var(--kb, 0rem);
|
|
247
245
|
max-width: none;
|
|
248
246
|
margin: auto calc(-1 * var(--screen-pad)) 0;
|
|
249
247
|
}
|
|
@@ -313,13 +311,6 @@
|
|
|
313
311
|
:is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
|
|
314
312
|
:is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
|
|
315
313
|
|
|
316
|
-
/* .screen's default bottom padding is a constant; derive the reserve from the
|
|
317
|
-
bar instead, so a taller button (a longer label, a script with more leading)
|
|
318
|
-
can never end up underneath it. Any parent that hosts the bar counts —
|
|
319
|
-
<body>, or an app shell (#app > .screen + .action-bar) — since the bar is
|
|
320
|
-
fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
|
|
321
|
-
its .step carries the bar's room itself, step.css.) */
|
|
322
|
-
:has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
|
|
323
314
|
|
|
324
315
|
/* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
|
|
325
316
|
rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
|
package/css/layover.css
CHANGED
|
@@ -31,12 +31,11 @@
|
|
|
31
31
|
Reduced motion: the finished state, no beats; closing fires at once. */
|
|
32
32
|
.modal {
|
|
33
33
|
--drawer-h: 14rem;
|
|
34
|
-
position: absolute; /* at the stage's centre, and kept there as it grows:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
share the rest. NOT `height: fit-content` — WebKit/Safari sizes it wrong (Lh 2026-09-24, iOS 18.6) */
|
|
34
|
+
position: absolute; /* at the stage's centre, and kept there as it grows: its middle is pinned at 50% and */
|
|
35
|
+
top: 50%; /* translate -50% of its OWN height re-centres it on every height. NOT inset 0 + margin auto + */
|
|
36
|
+
inset-inline: 0; /* align-self: center — Safari 18 ignores align-self on an absolute box in a block container, */
|
|
37
|
+
margin-inline: auto; /* so the card stretched to the whole stage (iOS 18.6, Lh 2026-09-24); nor height: fit-content */
|
|
38
|
+
translate: 0 -50%;
|
|
40
39
|
width: calc(100% - 2 * var(--screen-pad));
|
|
41
40
|
z-index: 2; /* over the dim stage's scrim (z 1) */
|
|
42
41
|
max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
|
|
@@ -80,8 +79,8 @@
|
|
|
80
79
|
.modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
|
|
81
80
|
.modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
|
|
82
81
|
@keyframes modal-drop {
|
|
83
|
-
0% { translate: 0 -100cqh; } /* above the stage */
|
|
84
|
-
50%, 100% { translate: 0
|
|
82
|
+
0% { translate: 0 calc(-50% - 100cqh); } /* above the stage */
|
|
83
|
+
50%, 100% { translate: 0 -50%; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
|
|
85
84
|
}
|
|
86
85
|
@keyframes modal-open {
|
|
87
86
|
0%, 50% { height: 0; padding-block: 0; } /* inside the card */
|
|
@@ -166,16 +165,29 @@
|
|
|
166
165
|
@starting-style { .menu:popover-open { opacity: 0; translate: 0 calc(-1 * var(--sp-2)); } }
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
/* ---- banner:
|
|
170
|
-
<details class="banner"><summary class="
|
|
171
|
-
<div class="
|
|
172
|
-
|
|
173
|
-
.banner
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
/* ---- banner: ONE band — the line that opens it and the body share one ground (Lh 2026-09-24: "ปุ่มกับเนื้อหาใช้พื้นที่เดียวกัน") ----
|
|
169
|
+
<details class="banner"><summary class="banner__summary">Save 2 h… <svg class="icon icon--xs"/></summary>
|
|
170
|
+
<div class="banner__body">…</div></details>
|
|
171
|
+
A light tint (not a card tone) with a slightly darker rule above and below, edge to edge of its card or screen (Lh 2026-09-24). */
|
|
172
|
+
.banner {
|
|
173
|
+
display: grid;
|
|
174
|
+
background: var(--banner-bg, var(--ink-tint));
|
|
175
|
+
color: var(--banner-ink, inherit);
|
|
176
|
+
border-block: var(--border-w) solid var(--banner-border, var(--hairline));
|
|
177
|
+
/* a band edge to edge of what holds it — the card's or the screen's inset; no radius, no side border: a band, not a card */
|
|
178
|
+
margin-inline: calc(-1 * var(--card-pad, var(--screen-pad)));
|
|
179
|
+
padding-inline: var(--card-pad, var(--screen-pad));
|
|
180
|
+
}
|
|
181
|
+
.banner__summary {
|
|
182
|
+
list-style: none; cursor: pointer;
|
|
183
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
|
|
184
|
+
padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: 400;
|
|
185
|
+
}
|
|
186
|
+
.banner__summary::-webkit-details-marker { display: none; }
|
|
187
|
+
.banner__summary .icon { transition: rotate 0.2s ease; }
|
|
188
|
+
.banner[open] .banner__summary .icon { rotate: 180deg; }
|
|
189
|
+
.banner__body { display: grid; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
|
|
190
|
+
.banner::details-content { width: 100%; }
|
|
179
191
|
@supports (interpolate-size: allow-keywords) {
|
|
180
192
|
@media (prefers-reduced-motion: no-preference) {
|
|
181
193
|
.banner { interpolate-size: allow-keywords; }
|
|
@@ -289,3 +301,21 @@
|
|
|
289
301
|
.deck > .card:nth-last-child(n + 4) { display: none; }
|
|
290
302
|
.deck__actions { display: flex; justify-content: center; gap: var(--deck-actions-gap, var(--sp-8)); }
|
|
291
303
|
.deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
|
|
304
|
+
|
|
305
|
+
/* ---- splash: the app's mark at the human centre while it boots (Lh 2026-09-24) ----
|
|
306
|
+
<section class="screen screen--fill splash"><div class="splash__group"><div class="splash__mark">{the app's lottie/img}</div>
|
|
307
|
+
<p class="splash__text">…</p></div></section>
|
|
308
|
+
The group's MIDDLE is on --screen-centre (40% down), the same line a centred card uses. The mark is the app's own content —
|
|
309
|
+
a lottie box often carries empty room around its drawing — the app sets the knobs below, cardds does not guess it. */
|
|
310
|
+
.splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) ease; }
|
|
311
|
+
.splash--leaving { opacity: 0; }
|
|
312
|
+
.splash__group {
|
|
313
|
+
position: absolute; inset-inline: var(--screen-pad); top: var(--splash-at, var(--screen-centre)); translate: 0 -50%;
|
|
314
|
+
display: grid; justify-items: center; gap: var(--splash-gap, var(--sp-4)); text-align: center;
|
|
315
|
+
}
|
|
316
|
+
.splash__mark { width: var(--splash-mark-w, 60%); max-width: 100%; translate: var(--splash-mark-x, 0) var(--splash-mark-y, 0); }
|
|
317
|
+
/* each app's lottie is its own size and carries its own empty room: --splash-mark-w sizes the box, --splash-mark-x / -y nudge the
|
|
318
|
+
drawing onto the centre line (the line under it does not move), --splash-at moves the whole group (default the 40% line) */
|
|
319
|
+
.splash__mark > * { display: block; width: 100%; height: auto; }
|
|
320
|
+
.splash__text { margin: 0; font: var(--type-body); color: var(--splash-text-ink, var(--on-surface-muted)); }
|
|
321
|
+
@media (prefers-reduced-motion: reduce) { .splash { transition: none; } }
|
package/css/lists.css
CHANGED
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
/* rows: a list of rows separated by hairlines */
|
|
68
68
|
.rows { display: grid; --rows-gap: var(--sp-4); row-gap: var(--rows-gap); }
|
|
69
69
|
.rows:has(> .row--lg) { --rows-gap: var(--sp-6); }
|
|
70
|
+
/* the group's name: a label on the rows' start edge; two groups one after another read apart by it, not by space */
|
|
71
|
+
.rows__head { font: var(--type-label); color: var(--rows-head-ink, var(--card-muted)); }
|
|
72
|
+
.rows + .rows { margin-top: var(--rows-group-gap, var(--sp-6)); }
|
|
70
73
|
/* the hairline between rows sits in the middle of the gap — drawn, not spaced:
|
|
71
74
|
no padding, no spacer element */
|
|
72
75
|
.rows > .row + .row { position: relative; }
|
package/css/media.css
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
.tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
91
91
|
.tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
92
92
|
.tile--wide { grid-column: 1 / -1; }
|
|
93
|
+
/* stat: READ, not pressed — no frame of its own inside a card (a frame says "tap me"): icon · value · words on the card's ground */
|
|
94
|
+
.tile--stat { border-color: transparent; background: none; padding-inline: 0; cursor: default; }
|
|
93
95
|
.tile--stat { min-height: var(--tile-stat-min-h, var(--avatar-xl)); gap: var(--tile-stat-gap, var(--sp-3)); grid-template-columns: auto 1fr; align-items: center; }
|
|
94
96
|
/* upload: dashed, centred plus + label */
|
|
95
97
|
.tile--upload {
|
package/css/stack.css
CHANGED
|
@@ -271,7 +271,25 @@
|
|
|
271
271
|
|
|
272
272
|
/* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
|
|
273
273
|
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
274
|
-
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap,
|
|
274
|
+
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
|
|
275
|
+
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
276
|
+
(StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
|
|
277
|
+
.stack-sheet > .card__head { flex-wrap: nowrap; }
|
|
278
|
+
.stack-sheet__title {
|
|
279
|
+
flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
|
|
280
|
+
hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
|
|
281
|
+
translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
|
|
282
|
+
letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
|
|
283
|
+
}
|
|
284
|
+
.stack-sheet__run { display: inline-block; }
|
|
285
|
+
.stack-sheet__title.is-long .stack-sheet__run {
|
|
286
|
+
animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, 6s)) ease-in-out infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
|
|
287
|
+
}
|
|
288
|
+
@keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
|
|
289
|
+
@media (prefers-reduced-motion: reduce) {
|
|
290
|
+
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
291
|
+
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
292
|
+
}
|
|
275
293
|
.stack-sheet__tab { position: relative; }
|
|
276
294
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
277
295
|
position: absolute;
|
package/css/step.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<section class="page">…the words…</section> ← first
|
|
11
11
|
<article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
|
|
12
12
|
</section>
|
|
13
|
-
<div class="action-bar">…</div> ← only when there is no sheet
|
|
13
|
+
<div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
|
|
14
14
|
|
|
15
15
|
The step is the box the sheet slides in. It fills the screen
|
|
16
16
|
column between the top bar's reserve (.screen already pads for
|
|
@@ -59,11 +59,6 @@
|
|
|
59
59
|
more air and a short one less; a screen with a bar keeps the bar's own room */
|
|
60
60
|
.screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
|
|
61
61
|
|
|
62
|
-
/* the bar's room — read from actions.css, never recomputed. Margin, not
|
|
63
|
-
padding, so the clip cuts at the box edge and nothing shows behind the
|
|
64
|
-
bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
|
|
65
|
-
frame — the bar is fixed to the viewport wherever it sits. */
|
|
66
|
-
:has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
|
|
67
62
|
/* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
|
|
68
63
|
is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
|
|
69
64
|
|
|
@@ -119,7 +114,7 @@
|
|
|
119
114
|
TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
|
|
120
115
|
in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
|
|
121
116
|
under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
|
|
122
|
-
(
|
|
117
|
+
(On a scrolling screen the bar is a sticky last row instead, actions.css.)
|
|
123
118
|
============================================================ */
|
|
124
119
|
.screen--fill > .action-bar {
|
|
125
120
|
position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
|
|
@@ -127,7 +122,6 @@
|
|
|
127
122
|
margin: 0;
|
|
128
123
|
padding-top: 0; /* no ground of its own, never a solid strip */
|
|
129
124
|
}
|
|
130
|
-
.screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
|
|
131
125
|
/* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
|
|
132
126
|
.action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
|
|
133
127
|
/* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
|
package/dist/actions/Chip.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type ComponentProps, type ReactNode } from 'react';
|
|
|
2
2
|
export interface ChipProps extends ComponentProps<'span'> {
|
|
3
3
|
/** filled accent: the selected / live state */
|
|
4
4
|
on?: boolean;
|
|
5
|
+
/** quiet: an untouched default value — muted words, the thin edge, nothing to find the eye (README rule 13) */
|
|
6
|
+
muted?: boolean;
|
|
5
7
|
/** a leading signal dot: `a` (the accent signal) or `b` (the secondary) */
|
|
6
8
|
dot?: 'a' | 'b';
|
|
7
9
|
/** the element: span (default), a link, or a button */
|
|
@@ -10,7 +12,7 @@ export interface ChipProps extends ComponentProps<'span'> {
|
|
|
10
12
|
children?: ReactNode;
|
|
11
13
|
}
|
|
12
14
|
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
|
|
13
|
-
export declare function Chip({ on, dot, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
|
|
15
|
+
export declare function Chip({ on, muted, dot, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
|
|
14
16
|
ref?: import("react").Ref<HTMLSpanElement> | undefined;
|
|
15
17
|
key?: import("react").Key | null | undefined;
|
|
16
18
|
defaultChecked?: boolean | undefined;
|
package/dist/actions/Chip.js
CHANGED
|
@@ -3,9 +3,9 @@ import { createElement } from 'react';
|
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Dot } from './Dot.js';
|
|
5
5
|
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
|
|
6
|
-
export function Chip({ on, dot, as, href, className, children, ...rest }) {
|
|
6
|
+
export function Chip({ on, muted, dot, as, href, className, children, ...rest }) {
|
|
7
7
|
const el = as ?? (href ? 'a' : 'span');
|
|
8
|
-
return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b' }), children);
|
|
8
|
+
return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', muted && 'chip--muted', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b' }), children);
|
|
9
9
|
}
|
|
10
10
|
/** ChipPick — a chip that is a choice: full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
|
|
11
11
|
* interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */
|
|
@@ -13,6 +13,8 @@ export interface DropdownProps extends Omit<ComponentProps<'div'>, 'onChange' |
|
|
|
13
13
|
label?: ReactNode;
|
|
14
14
|
/** right-aligned trigger: the panel grows leftwards (in TopBar actions) */
|
|
15
15
|
end?: boolean;
|
|
16
|
+
/** quiet: the value is still the default — muted words; drop it once the member picks (README rule 13) */
|
|
17
|
+
muted?: boolean;
|
|
16
18
|
/** MenuItems, each with a `value` */
|
|
17
19
|
children?: ReactNode;
|
|
18
20
|
}
|
|
@@ -22,7 +24,7 @@ export interface DropdownProps extends Omit<ComponentProps<'div'>, 'onChange' |
|
|
|
22
24
|
* input: `value` + `onChange`, or `defaultValue`. Children are MenuItems with a `value`; the chosen one is marked and names the pill.
|
|
23
25
|
* In a FilterRow dropdowns share the width.
|
|
24
26
|
*/
|
|
25
|
-
export declare function Dropdown({ id, value, defaultValue, onChange, label, end, className, children, ...rest }: DropdownProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Dropdown({ id, value, defaultValue, onChange, label, end, muted, className, children, ...rest }: DropdownProps): import("react").JSX.Element;
|
|
26
28
|
export interface MenuProps extends Omit<ComponentProps<'div'>, 'popover' | 'onChange' | 'defaultValue'> {
|
|
27
29
|
/** render as a popover panel: give it an `id`, and ANY button opens it with the platform's own `popoverTarget={id}` — the Menu does
|
|
28
30
|
* not know which. Omit for an inline menu card */
|
package/dist/actions/Dropdown.js
CHANGED
|
@@ -19,10 +19,10 @@ function useValue(value, defaultValue, onChange) {
|
|
|
19
19
|
* input: `value` + `onChange`, or `defaultValue`. Children are MenuItems with a `value`; the chosen one is marked and names the pill.
|
|
20
20
|
* In a FilterRow dropdowns share the width.
|
|
21
21
|
*/
|
|
22
|
-
export function Dropdown({ id, value, defaultValue, onChange, label, end, className, children, ...rest }) {
|
|
22
|
+
export function Dropdown({ id, value, defaultValue, onChange, label, end, muted, className, children, ...rest }) {
|
|
23
23
|
const [current, pick] = useValue(value, defaultValue, onChange);
|
|
24
24
|
const chosen = Children.toArray(children).find((c) => isValidElement(c) && c.props.value === current);
|
|
25
|
-
return (_jsxs("div", { className: cx('dropdown', end && 'dropdown--end', className), "data-value": current, ...rest, children: [_jsxs("button", { type: "button", className: "dropdown__btn", popoverTarget: id, children: [_jsx("span", { className: "dropdown__label", children: label ?? (isValidElement(chosen) ? chosen.props.children : null) }), _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx(Menu, { popover: true, id: id, value: current, onChange: pick, children: children })] }));
|
|
25
|
+
return (_jsxs("div", { className: cx('dropdown', end && 'dropdown--end', muted && 'dropdown--muted', className), "data-value": current, ...rest, children: [_jsxs("button", { type: "button", className: "dropdown__btn", popoverTarget: id, children: [_jsx("span", { className: "dropdown__label", children: label ?? (isValidElement(chosen) ? chosen.props.children : null) }), _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx(Menu, { popover: true, id: id, value: current, onChange: pick, children: children })] }));
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Menu — a card of MenuItems: a popover, or inline rows of icon + label. A COMPONENT on its own — what opens a popover Menu is any
|
package/dist/cardds.css
CHANGED
|
@@ -1114,7 +1114,25 @@ button { font: inherit; cursor: pointer; }
|
|
|
1114
1114
|
|
|
1115
1115
|
/* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
|
|
1116
1116
|
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
1117
|
-
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap,
|
|
1117
|
+
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
|
|
1118
|
+
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
1119
|
+
(StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
|
|
1120
|
+
.stack-sheet > .card__head { flex-wrap: nowrap; }
|
|
1121
|
+
.stack-sheet__title {
|
|
1122
|
+
flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
|
|
1123
|
+
hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
|
|
1124
|
+
translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
|
|
1125
|
+
letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
|
|
1126
|
+
}
|
|
1127
|
+
.stack-sheet__run { display: inline-block; }
|
|
1128
|
+
.stack-sheet__title.is-long .stack-sheet__run {
|
|
1129
|
+
animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, 6s)) ease-in-out infinite; /* ONE way: start → end, then back to the start at once (Lh 2026-09-24) */ /* --_run: from the overflow, 30px a second + the pauses */
|
|
1130
|
+
}
|
|
1131
|
+
@keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
|
|
1132
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1133
|
+
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
1134
|
+
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
1135
|
+
}
|
|
1118
1136
|
.stack-sheet__tab { position: relative; }
|
|
1119
1137
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
1120
1138
|
position: absolute;
|
|
@@ -1543,7 +1561,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1543
1561
|
<section class="page">…the words…</section> ← first
|
|
1544
1562
|
<article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
|
|
1545
1563
|
</section>
|
|
1546
|
-
<div class="action-bar">…</div> ← only when there is no sheet
|
|
1564
|
+
<div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
|
|
1547
1565
|
|
|
1548
1566
|
The step is the box the sheet slides in. It fills the screen
|
|
1549
1567
|
column between the top bar's reserve (.screen already pads for
|
|
@@ -1592,11 +1610,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
1592
1610
|
more air and a short one less; a screen with a bar keeps the bar's own room */
|
|
1593
1611
|
.screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
|
|
1594
1612
|
|
|
1595
|
-
/* the bar's room — read from actions.css, never recomputed. Margin, not
|
|
1596
|
-
padding, so the clip cuts at the box edge and nothing shows behind the
|
|
1597
|
-
bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
|
|
1598
|
-
frame — the bar is fixed to the viewport wherever it sits. */
|
|
1599
|
-
:has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
|
|
1600
1613
|
/* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
|
|
1601
1614
|
is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
|
|
1602
1615
|
|
|
@@ -1652,7 +1665,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1652
1665
|
TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
|
|
1653
1666
|
in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
|
|
1654
1667
|
under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
|
|
1655
|
-
(
|
|
1668
|
+
(On a scrolling screen the bar is a sticky last row instead, actions.css.)
|
|
1656
1669
|
============================================================ */
|
|
1657
1670
|
.screen--fill > .action-bar {
|
|
1658
1671
|
position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
|
|
@@ -1660,7 +1673,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
1660
1673
|
margin: 0;
|
|
1661
1674
|
padding-top: 0; /* no ground of its own, never a solid strip */
|
|
1662
1675
|
}
|
|
1663
|
-
.screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
|
|
1664
1676
|
/* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
|
|
1665
1677
|
.action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
|
|
1666
1678
|
/* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
|
|
@@ -1990,6 +2002,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
1990
2002
|
letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
|
|
1991
2003
|
.dot--b { background: var(--dot-b); }
|
|
1992
2004
|
.chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
|
|
2005
|
+
/* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (README 13) */
|
|
2006
|
+
.chip--muted { color: var(--chip-muted-ink, var(--card-muted)); }
|
|
1993
2007
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
1994
2008
|
|
|
1995
2009
|
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
|
@@ -2019,6 +2033,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
2019
2033
|
text-align: left;
|
|
2020
2034
|
cursor: pointer;
|
|
2021
2035
|
}
|
|
2036
|
+
/* muted: the value is still the default (README 13) */
|
|
2037
|
+
.dropdown--muted .dropdown__btn { color: var(--dropdown-muted-ink, var(--card-muted)); }
|
|
2022
2038
|
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2023
2039
|
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
2024
2040
|
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
@@ -2048,21 +2064,15 @@ button { font: inherit; cursor: pointer; }
|
|
|
2048
2064
|
opacity: 1;
|
|
2049
2065
|
}
|
|
2050
2066
|
|
|
2051
|
-
/* ---- bottom CTA pack:
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
the
|
|
2057
|
-
--kb is the on-screen keyboard's height, published on :root by cardds.js
|
|
2058
|
-
from the visual viewport (0 without one, and on a platform that shrinks
|
|
2059
|
-
the layout viewport itself): the bar rides above the keyboard on iOS the
|
|
2060
|
-
way it already does on Android, and .step grows its room by the same. */
|
|
2067
|
+
/* ---- bottom CTA pack: the screen's bottom (Screen `bottom`) ----
|
|
2068
|
+
A bar is always INSIDE its .screen, its last child (0.7.1: the bar fixed to the viewport from outside, and the
|
|
2069
|
+
reserves the screen and the step kept for it, are gone). In a .screen--fill it is a layer over the content's foot
|
|
2070
|
+
(step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
|
|
2071
|
+
height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
|
|
2072
|
+
--kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
|
|
2061
2073
|
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2062
2074
|
.action-bar {
|
|
2063
|
-
position:
|
|
2064
|
-
inset-inline: 0;
|
|
2065
|
-
bottom: var(--kb, 0);
|
|
2075
|
+
position: relative;
|
|
2066
2076
|
z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
|
|
2067
2077
|
max-width: var(--screen-w);
|
|
2068
2078
|
margin-inline: auto;
|
|
@@ -2073,9 +2083,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
2073
2083
|
/* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
|
|
2074
2084
|
last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
|
|
2075
2085
|
it takes the room it needs in the column, so nothing is added to the content for it */
|
|
2076
|
-
.screen:not(.screen--fill):has(> .action-bar) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
2077
|
-
.screen:not(.screen--fill) > .action-bar {
|
|
2078
|
-
position: sticky;
|
|
2086
|
+
.screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
|
|
2087
|
+
.screen:not(.screen--fill) > .action-bar:last-child {
|
|
2088
|
+
position: sticky; bottom: var(--kb, 0rem);
|
|
2079
2089
|
max-width: none;
|
|
2080
2090
|
margin: auto calc(-1 * var(--screen-pad)) 0;
|
|
2081
2091
|
}
|
|
@@ -2145,13 +2155,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
2145
2155
|
:is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
|
|
2146
2156
|
:is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
|
|
2147
2157
|
|
|
2148
|
-
/* .screen's default bottom padding is a constant; derive the reserve from the
|
|
2149
|
-
bar instead, so a taller button (a longer label, a script with more leading)
|
|
2150
|
-
can never end up underneath it. Any parent that hosts the bar counts —
|
|
2151
|
-
<body>, or an app shell (#app > .screen + .action-bar) — since the bar is
|
|
2152
|
-
fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
|
|
2153
|
-
its .step carries the bar's room itself, step.css.) */
|
|
2154
|
-
:has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
|
|
2155
2158
|
|
|
2156
2159
|
/* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
|
|
2157
2160
|
rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
|
|
@@ -2919,6 +2922,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
2919
2922
|
/* rows: a list of rows separated by hairlines */
|
|
2920
2923
|
.rows { display: grid; --rows-gap: var(--sp-4); row-gap: var(--rows-gap); }
|
|
2921
2924
|
.rows:has(> .row--lg) { --rows-gap: var(--sp-6); }
|
|
2925
|
+
/* the group's name: a label on the rows' start edge; two groups one after another read apart by it, not by space */
|
|
2926
|
+
.rows__head { font: var(--type-label); color: var(--rows-head-ink, var(--card-muted)); }
|
|
2927
|
+
.rows + .rows { margin-top: var(--rows-group-gap, var(--sp-6)); }
|
|
2922
2928
|
/* the hairline between rows sits in the middle of the gap — drawn, not spaced:
|
|
2923
2929
|
no padding, no spacer element */
|
|
2924
2930
|
.rows > .row + .row { position: relative; }
|
|
@@ -3150,6 +3156,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
3150
3156
|
.tile--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
3151
3157
|
.tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
3152
3158
|
.tile--wide { grid-column: 1 / -1; }
|
|
3159
|
+
/* stat: READ, not pressed — no frame of its own inside a card (a frame says "tap me"): icon · value · words on the card's ground */
|
|
3160
|
+
.tile--stat { border-color: transparent; background: none; padding-inline: 0; cursor: default; }
|
|
3153
3161
|
.tile--stat { min-height: var(--tile-stat-min-h, var(--avatar-xl)); gap: var(--tile-stat-gap, var(--sp-3)); grid-template-columns: auto 1fr; align-items: center; }
|
|
3154
3162
|
/* upload: dashed, centred plus + label */
|
|
3155
3163
|
.tile--upload {
|
|
@@ -3378,12 +3386,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
3378
3386
|
Reduced motion: the finished state, no beats; closing fires at once. */
|
|
3379
3387
|
.modal {
|
|
3380
3388
|
--drawer-h: 14rem;
|
|
3381
|
-
position: absolute; /* at the stage's centre, and kept there as it grows:
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
share the rest. NOT `height: fit-content` — WebKit/Safari sizes it wrong (Lh 2026-09-24, iOS 18.6) */
|
|
3389
|
+
position: absolute; /* at the stage's centre, and kept there as it grows: its middle is pinned at 50% and */
|
|
3390
|
+
top: 50%; /* translate -50% of its OWN height re-centres it on every height. NOT inset 0 + margin auto + */
|
|
3391
|
+
inset-inline: 0; /* align-self: center — Safari 18 ignores align-self on an absolute box in a block container, */
|
|
3392
|
+
margin-inline: auto; /* so the card stretched to the whole stage (iOS 18.6, Lh 2026-09-24); nor height: fit-content */
|
|
3393
|
+
translate: 0 -50%;
|
|
3387
3394
|
width: calc(100% - 2 * var(--screen-pad));
|
|
3388
3395
|
z-index: 2; /* over the dim stage's scrim (z 1) */
|
|
3389
3396
|
max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
|
|
@@ -3427,8 +3434,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
3427
3434
|
.modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
|
|
3428
3435
|
.modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
|
|
3429
3436
|
@keyframes modal-drop {
|
|
3430
|
-
0% { translate: 0 -100cqh; } /* above the stage */
|
|
3431
|
-
50%, 100% { translate: 0
|
|
3437
|
+
0% { translate: 0 calc(-50% - 100cqh); } /* above the stage */
|
|
3438
|
+
50%, 100% { translate: 0 -50%; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
|
|
3432
3439
|
}
|
|
3433
3440
|
@keyframes modal-open {
|
|
3434
3441
|
0%, 50% { height: 0; padding-block: 0; } /* inside the card */
|
|
@@ -3513,16 +3520,29 @@ button { font: inherit; cursor: pointer; }
|
|
|
3513
3520
|
@starting-style { .menu:popover-open { opacity: 0; translate: 0 calc(-1 * var(--sp-2)); } }
|
|
3514
3521
|
}
|
|
3515
3522
|
|
|
3516
|
-
/* ---- banner:
|
|
3517
|
-
<details class="banner"><summary class="
|
|
3518
|
-
<div class="
|
|
3519
|
-
|
|
3520
|
-
.banner
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3523
|
+
/* ---- banner: ONE band — the line that opens it and the body share one ground (Lh 2026-09-24: "ปุ่มกับเนื้อหาใช้พื้นที่เดียวกัน") ----
|
|
3524
|
+
<details class="banner"><summary class="banner__summary">Save 2 h… <svg class="icon icon--xs"/></summary>
|
|
3525
|
+
<div class="banner__body">…</div></details>
|
|
3526
|
+
A light tint (not a card tone) with a slightly darker rule above and below, edge to edge of its card or screen (Lh 2026-09-24). */
|
|
3527
|
+
.banner {
|
|
3528
|
+
display: grid;
|
|
3529
|
+
background: var(--banner-bg, var(--ink-tint));
|
|
3530
|
+
color: var(--banner-ink, inherit);
|
|
3531
|
+
border-block: var(--border-w) solid var(--banner-border, var(--hairline));
|
|
3532
|
+
/* a band edge to edge of what holds it — the card's or the screen's inset; no radius, no side border: a band, not a card */
|
|
3533
|
+
margin-inline: calc(-1 * var(--card-pad, var(--screen-pad)));
|
|
3534
|
+
padding-inline: var(--card-pad, var(--screen-pad));
|
|
3535
|
+
}
|
|
3536
|
+
.banner__summary {
|
|
3537
|
+
list-style: none; cursor: pointer;
|
|
3538
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
|
|
3539
|
+
padding-block: var(--banner-summary-pad-y, calc(var(--sp-base) * 1.5)); font: var(--banner-summary-font, var(--type-caption)); font-weight: 400;
|
|
3540
|
+
}
|
|
3541
|
+
.banner__summary::-webkit-details-marker { display: none; }
|
|
3542
|
+
.banner__summary .icon { transition: rotate 0.2s ease; }
|
|
3543
|
+
.banner[open] .banner__summary .icon { rotate: 180deg; }
|
|
3544
|
+
.banner__body { display: grid; gap: var(--banner-body-gap, var(--sp-1)); padding-bottom: var(--sp-4); }
|
|
3545
|
+
.banner::details-content { width: 100%; }
|
|
3526
3546
|
@supports (interpolate-size: allow-keywords) {
|
|
3527
3547
|
@media (prefers-reduced-motion: no-preference) {
|
|
3528
3548
|
.banner { interpolate-size: allow-keywords; }
|
|
@@ -3637,3 +3657,21 @@ button { font: inherit; cursor: pointer; }
|
|
|
3637
3657
|
.deck__actions { display: flex; justify-content: center; gap: var(--deck-actions-gap, var(--sp-8)); }
|
|
3638
3658
|
.deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
|
|
3639
3659
|
|
|
3660
|
+
/* ---- splash: the app's mark at the human centre while it boots (Lh 2026-09-24) ----
|
|
3661
|
+
<section class="screen screen--fill splash"><div class="splash__group"><div class="splash__mark">{the app's lottie/img}</div>
|
|
3662
|
+
<p class="splash__text">…</p></div></section>
|
|
3663
|
+
The group's MIDDLE is on --screen-centre (40% down), the same line a centred card uses. The mark is the app's own content —
|
|
3664
|
+
a lottie box often carries empty room around its drawing — the app sets the knobs below, cardds does not guess it. */
|
|
3665
|
+
.splash { position: relative; background: var(--splash-bg, var(--surface)); transition: opacity var(--motion-sheet) ease; }
|
|
3666
|
+
.splash--leaving { opacity: 0; }
|
|
3667
|
+
.splash__group {
|
|
3668
|
+
position: absolute; inset-inline: var(--screen-pad); top: var(--splash-at, var(--screen-centre)); translate: 0 -50%;
|
|
3669
|
+
display: grid; justify-items: center; gap: var(--splash-gap, var(--sp-4)); text-align: center;
|
|
3670
|
+
}
|
|
3671
|
+
.splash__mark { width: var(--splash-mark-w, 60%); max-width: 100%; translate: var(--splash-mark-x, 0) var(--splash-mark-y, 0); }
|
|
3672
|
+
/* each app's lottie is its own size and carries its own empty room: --splash-mark-w sizes the box, --splash-mark-x / -y nudge the
|
|
3673
|
+
drawing onto the centre line (the line under it does not move), --splash-at moves the whole group (default the 40% line) */
|
|
3674
|
+
.splash__mark > * { display: block; width: 100%; height: auto; }
|
|
3675
|
+
.splash__text { margin: 0; font: var(--type-body); color: var(--splash-text-ink, var(--on-surface-muted)); }
|
|
3676
|
+
@media (prefers-reduced-motion: reduce) { .splash { transition: none; } }
|
|
3677
|
+
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/layover/Banner.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
-
export interface BannerProps extends ComponentProps<'details'> {
|
|
3
|
-
/** the
|
|
2
|
+
export interface BannerProps extends Omit<ComponentProps<'details'>, 'summary'> {
|
|
3
|
+
/** the line that opens it — words (and an inline Icon); the chevron is the Banner's own */
|
|
4
4
|
summary: ReactNode;
|
|
5
|
-
/** the tone of the opened card */
|
|
6
|
-
tone?: 1 | 2 | 3;
|
|
7
5
|
}
|
|
8
|
-
/** Banner —
|
|
9
|
-
|
|
6
|
+
/** Banner — ONE band: the line that opens it and what it opens share the same ground, so they read as one piece (Lh 2026-09-24).
|
|
7
|
+
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card. A native details, no JS. */
|
|
8
|
+
export declare function Banner({ summary, className, children, ...rest }: BannerProps): import("react").JSX.Element;
|
package/dist/layover/Banner.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { Icon } from '../type/Icon.js';
|
|
4
|
+
/** Banner — ONE band: the line that opens it and what it opens share the same ground, so they read as one piece (Lh 2026-09-24).
|
|
5
|
+
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card. A native details, no JS. */
|
|
6
|
+
export function Banner({ summary, className, children, ...rest }) {
|
|
7
|
+
return (_jsxs("details", { className: cx('banner', className), ...rest, children: [_jsxs("summary", { className: "banner__summary", children: [summary, _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx("div", { className: "banner__body", children: children })] }));
|
|
6
8
|
}
|
package/dist/lists/Row.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function Row({ lead, leadIcon, title, meta, body, end, endValue, endCaption, lg, done, as, href, more, open, defaultOpen, onOpenChange, className, children, ...rest }: RowProps): import("react").JSX.Element;
|
|
43
43
|
export interface RowsProps extends ComponentProps<'div'> {
|
|
44
|
+
/** the group's name, above its rows (Today · Tomorrow · Later): a long list is GROUPED, not spaced out — several Rows one after another */
|
|
45
|
+
heading?: ReactNode;
|
|
44
46
|
}
|
|
45
|
-
/** Rows — a list of Rows separated by hairlines. */
|
|
46
|
-
export declare function Rows({ className, ...rest }: RowsProps): import("react").JSX.Element;
|
|
47
|
+
/** Rows — a list of Rows separated by hairlines; `heading` names the group (a `role="group"` labelled by it). */
|
|
48
|
+
export declare function Rows({ heading, className, children, ...rest }: RowsProps): import("react").JSX.Element;
|
package/dist/lists/Row.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createElement } from 'react';
|
|
2
|
+
import { createElement, useId } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Icon } from '../type/Icon.js';
|
|
5
5
|
/**
|
|
@@ -18,7 +18,9 @@ export function Row({ lead, leadIcon, title, meta, body, end, endValue, endCapti
|
|
|
18
18
|
e.currentTarget.open = open; if (is !== (open ?? !is))
|
|
19
19
|
onOpenChange?.(is); }, children: [row, _jsx("div", { className: "row-expand__more", children: more })] }));
|
|
20
20
|
}
|
|
21
|
-
/** Rows — a list of Rows separated by hairlines. */
|
|
22
|
-
export function Rows({ className, ...rest }) {
|
|
23
|
-
|
|
21
|
+
/** Rows — a list of Rows separated by hairlines; `heading` names the group (a `role="group"` labelled by it). */
|
|
22
|
+
export function Rows({ heading, className, children, ...rest }) {
|
|
23
|
+
const id = useId();
|
|
24
|
+
const named = heading != null;
|
|
25
|
+
return (_jsxs("div", { className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && _jsx("span", { className: "rows__head", id: id, children: heading }), children] }));
|
|
24
26
|
}
|
|
@@ -2,7 +2,7 @@ import type { ComponentProps } from 'react';
|
|
|
2
2
|
export interface ActionBarProps extends ComponentProps<'div'> {
|
|
3
3
|
/** `pager`: previous / next as two equal buttons at the edges (a Link may sit between them; the PagerAt dots never do — they go under the card) · `tiers`: rows stacked — a control tier above the buttons */
|
|
4
4
|
variant?: 'pager' | 'tiers';
|
|
5
|
-
/**
|
|
5
|
+
/** a bare bar for a preview: no position, full width, no side padding (a real bar is a Screen's `bottom`) */
|
|
6
6
|
inline?: boolean;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
@@ -11,7 +11,7 @@ export interface ActionBarProps extends ComponentProps<'div'> {
|
|
|
11
11
|
* Plain: one wide primary Btn (the step that commits). `pager`: Back + Next as peers — buttons only; where you are is the
|
|
12
12
|
* PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-12).
|
|
13
13
|
* `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
|
|
14
|
-
* In a `Screen fill`
|
|
14
|
+
* In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
|
|
15
15
|
*/
|
|
16
16
|
export declare function ActionBar({ variant, inline, className, style, ...rest }: ActionBarProps): import("react").JSX.Element;
|
|
17
17
|
export interface ActionBarTierProps extends ComponentProps<'div'> {
|
|
@@ -6,7 +6,7 @@ import { cx } from '../cx.js';
|
|
|
6
6
|
* Plain: one wide primary Btn (the step that commits). `pager`: Back + Next as peers — buttons only; where you are is the
|
|
7
7
|
* PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-12).
|
|
8
8
|
* `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
|
|
9
|
-
* In a `Screen fill`
|
|
9
|
+
* In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
|
|
10
10
|
*/
|
|
11
11
|
export function ActionBar({ variant, inline, className, style, ...rest }) {
|
|
12
12
|
return (_jsx("div", { className: cx('action-bar', variant && `action-bar--${variant}`, className), style: inline ? { position: 'static', width: '100%', maxWidth: 'none', paddingInline: 0, ...style } : style, ...rest }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Card } from '../cards/Card.js';
|
|
5
5
|
import { CardHead } from '../cards/CardHead.js';
|
|
@@ -20,5 +20,24 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
|
|
|
20
20
|
const tab = tabProp ?? own;
|
|
21
21
|
const pick = (id) => { if (tabProp === undefined)
|
|
22
22
|
setOwn(id); onTabChange?.(id); };
|
|
23
|
-
|
|
23
|
+
// a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
|
|
24
|
+
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
25
|
+
const titleRef = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const h = titleRef.current, run = h?.firstElementChild;
|
|
28
|
+
if (!h || !run || typeof ResizeObserver === 'undefined')
|
|
29
|
+
return;
|
|
30
|
+
const measure = () => {
|
|
31
|
+
const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
|
|
32
|
+
h.style.setProperty('--_over', `${over}px`);
|
|
33
|
+
h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
34
|
+
h.classList.toggle('is-long', over > 0);
|
|
35
|
+
};
|
|
36
|
+
const ro = new ResizeObserver(measure);
|
|
37
|
+
ro.observe(h);
|
|
38
|
+
ro.observe(run);
|
|
39
|
+
measure();
|
|
40
|
+
return () => ro.disconnect();
|
|
41
|
+
}, [title]);
|
|
42
|
+
return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === tab, "aria-label": t.label, className: cx('icon-btn icon-btn--sm stack-sheet__tab', t.id === tab && 'icon-btn--invert'), onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
|
|
24
43
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { Screen } from '../scaffold/Screen.js';
|
|
3
|
+
export interface SplashProps extends Omit<ComponentProps<typeof Screen>, 'children' | 'fill' | 'top' | 'bottom'> {
|
|
4
|
+
/** the app's mark — ITS OWN content: a Lottie player, an <img>, an SVG. cardds places it; it never plays anything itself */
|
|
5
|
+
mark: ReactNode;
|
|
6
|
+
/** one line under the mark (optional) */
|
|
7
|
+
text?: ReactNode;
|
|
8
|
+
/** the app is ready: the splash fades out (after `min`), then calls `onLeft` */
|
|
9
|
+
done?: boolean;
|
|
10
|
+
/** the least time it stays up, in ms, counted from when it appeared (default 0) — so a fast boot does not flash it */
|
|
11
|
+
min?: number;
|
|
12
|
+
/** it has faded out: the app unmounts it now */
|
|
13
|
+
onLeft?: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Splash — the screen an app shows while it boots: the app's mark (and a line) at the human centre, `--screen-centre` (40%) down,
|
|
17
|
+
* nothing else. A TEMPLATE: cardds does the place, the line, the timing and the fade; the mark is the app's (a Lottie it loads and
|
|
18
|
+
* plays, or a still) — cardds has no animation library. The app says when it is ready (`done`); `min` keeps a fast boot from
|
|
19
|
+
* flashing it. Each lottie is its own size: knobs `--splash-mark-w` (the box's width), `--splash-mark-x` / `--splash-mark-y` (nudge the
|
|
20
|
+
* drawing onto the line), `--splash-at` (the group's line, default --screen-centre) — in the app's theme.css or `style`. `onLeft` fires after the fade (at once under prefers-reduced-motion — the app should give its mark a still frame there too).
|
|
21
|
+
*/
|
|
22
|
+
export declare function Splash({ mark, text, done, min, onLeft, className, ...rest }: SplashProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Screen } from '../scaffold/Screen.js';
|
|
4
|
+
import { cx } from '../cx.js';
|
|
5
|
+
/**
|
|
6
|
+
* Splash — the screen an app shows while it boots: the app's mark (and a line) at the human centre, `--screen-centre` (40%) down,
|
|
7
|
+
* nothing else. A TEMPLATE: cardds does the place, the line, the timing and the fade; the mark is the app's (a Lottie it loads and
|
|
8
|
+
* plays, or a still) — cardds has no animation library. The app says when it is ready (`done`); `min` keeps a fast boot from
|
|
9
|
+
* flashing it. Each lottie is its own size: knobs `--splash-mark-w` (the box's width), `--splash-mark-x` / `--splash-mark-y` (nudge the
|
|
10
|
+
* drawing onto the line), `--splash-at` (the group's line, default --screen-centre) — in the app's theme.css or `style`. `onLeft` fires after the fade (at once under prefers-reduced-motion — the app should give its mark a still frame there too).
|
|
11
|
+
*/
|
|
12
|
+
export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }) {
|
|
13
|
+
const born = useRef(Date.now());
|
|
14
|
+
const [leaving, setLeaving] = useState(false);
|
|
15
|
+
const left = useRef(onLeft);
|
|
16
|
+
left.current = onLeft;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!done)
|
|
19
|
+
return;
|
|
20
|
+
const t = setTimeout(() => setLeaving(true), Math.max(0, min - (Date.now() - born.current)));
|
|
21
|
+
return () => clearTimeout(t);
|
|
22
|
+
}, [done, min]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!leaving)
|
|
25
|
+
return;
|
|
26
|
+
const still = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
const ms = still ? 0 : parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--motion-sheet')) * 1000 || 350;
|
|
28
|
+
const t = setTimeout(() => left.current?.(), ms);
|
|
29
|
+
return () => clearTimeout(t);
|
|
30
|
+
}, [leaving]);
|
|
31
|
+
return (_jsx(Screen, { fill: true, className: cx('splash', leaving && 'splash--leaving', className), role: "status", "aria-busy": !leaving, ...rest, children: _jsxs("div", { className: "splash__group", children: [_jsx("div", { className: "splash__mark", children: mark }), text != null && _jsx("p", { className: "splash__text", children: text })] }) }));
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
|