@usableapp/cardds 0.7.15 → 0.7.16

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -8
  3. package/css/actions.css +24 -1
  4. package/css/base.css +2 -2
  5. package/css/card.css +7 -13
  6. package/css/choice.css +2 -2
  7. package/css/journey.css +3 -3
  8. package/css/layover.css +5 -17
  9. package/css/media.css +4 -5
  10. package/css/numbers.css +4 -3
  11. package/css/people.css +3 -2
  12. package/css/sheet.css +4 -6
  13. package/css/stack.css +6 -10
  14. package/css/step.css +3 -5
  15. package/css/theme.template.css +2 -1
  16. package/css/tokens.css +4 -1
  17. package/dist/actions/Dropdown.js +4 -5
  18. package/dist/cardds.css +71 -70
  19. package/dist/cards/Card.d.ts +1 -1
  20. package/dist/choice/Calendar.js +3 -5
  21. package/dist/choice/Mood.d.ts +10 -5
  22. package/dist/choice/Mood.js +4 -2
  23. package/dist/forms/Pin.js +7 -8
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/lists/Row.d.ts +2 -1
  27. package/dist/lists/Row.js +2 -2
  28. package/dist/media/Postcard.js +3 -5
  29. package/dist/numbers/Picker.js +4 -5
  30. package/dist/numbers/Track.d.ts +4 -2
  31. package/dist/numbers/Track.js +3 -3
  32. package/dist/people/AvatarPick.d.ts +3 -1
  33. package/dist/people/AvatarPick.js +2 -2
  34. package/dist/scaffold/Centre.d.ts +1 -1
  35. package/dist/scaffold/Centre.js +1 -1
  36. package/dist/scaffold/Screen.js +3 -4
  37. package/dist/scaffold/TopBar.js +3 -5
  38. package/dist/sheets/Sheet.js +5 -7
  39. package/dist/sheets/SheetStack.js +4 -5
  40. package/dist/sheets/SheetSteps.js +5 -6
  41. package/dist/sheets/StackSheet.js +4 -5
  42. package/dist/state.d.ts +7 -0
  43. package/dist/state.js +17 -0
  44. package/dist/templates/CallSheet.js +3 -4
  45. package/dist/templates/Home.js +3 -4
  46. package/dist/templates/Splash.js +2 -1
  47. package/dist/templates/Walkthrough.js +4 -5
  48. package/dist/type/Icon.js +3 -5
  49. package/dist/type/textScale.d.ts +6 -0
  50. package/dist/type/textScale.js +12 -0
  51. package/package.json +1 -1
  52. package/dist/layover/Deck.d.ts +0 -9
  53. package/dist/layover/Deck.js +0 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
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.16 — 2026-09-25
6
+
7
+ Refactor + additions. Same props, same markup, same computed style on every story.
8
+
9
+ ### Added
10
+ - `setTextScale(percent, root?)` — the member's text size as a share of the theme's `--fs-base` (100 = the theme's own). It reads the base from the CSS, so an app no longer copies `1.375rem`. Keeping the choice (localStorage…) stays the app's.
11
+
12
+ - PRESS: every control (Btn, IconBtn, Fab, Link, a/button Chip, ChipPick, Tile as button/link/pick, Day, SegmentItem, Calendar day, FloatBarItem, AvatarPickItem, AddRow, Picker item, StackBack, Dropdown, AppBar brand, MenuItem) gives under the finger (`--press-scale` 0.96, `--motion-press` 0.12s, off under reduced motion) and shows a focus ring; a choice's fill fades in. Zero specificity (`:where`), so a component's own rule wins.
13
+ - `Rows heading` takes an ELEMENT too: `<Rows heading={<Text level="title" as="h3">…</Text>}>` — that h3 is the group's name (role=group, aria-labelledby). Words still give the small label. Drop a hand-written `role="group" aria-label`.
14
+ - Token `--dim` (0.7): the one share secondary words fade to on the dark card, a drawer, a tile's sub line (was 14 hand-typed `opacity: 0.7`). A theme sets it once.
15
+ - `TrackStep num` — the step's number in the round instead of an Icon (the `.track__dot` look existed in CSS with no way in; story Track · Numbered).
16
+ - `AvatarPickItem selected` (aria-pressed; the picked person's avatar fills with the accent).
17
+
18
+ ### Removed (breaking for anyone using them)
19
+ - `Deck` and `DeckActions` (the swipe deck), the `.deck*` CSS and the `--deck-*` knobs — Lh 2026-09-26: no use case. Nothing replaces them; a card to pick from = `Card` + its own buttons.
20
+
21
+ ### Changed (may touch a screen)
22
+ - `Mood` is controlled like every other control: `value` + `onChange(value)` or `defaultValue`. Before: `value` was only the starting dot and `onChange` got the input event. Move a starting value to `defaultValue`; read the number from `onChange`.
23
+ - INFORMATION never looks tappable: `.tile` (a plain div) and `Avatar add` no longer set `cursor: pointer`.
24
+ - Stories: Segment, DayStrip, FloatBar (buttons), AvatarPick answer a tap; the Chip filter row is `ChipPick`s (plain `Chip on` is a label, not a choice).
25
+
26
+ ### Changed (internal)
27
+ - Every controlled/uncontrolled state (`value`+`onChange` or `defaultValue`, and the same for `state`, `open`, `at`, `tab`, `selected`, `flipped`) now runs on one internal hook, `useControlled`, instead of 12 hand copies.
28
+ - Dev warnings (Icon, TopBar, Screen) share one `warnOnce`.
29
+ - CSS: dead selectors removed (`.dropdown__trigger`, `.menu__item.is-on`, `.float-bar__item.is-on`), one overridden stack-head rule dropped, split rules merged. Two raw `0px` in the SheetSteps CSS → `0rem`.
30
+
5
31
  ## 0.7.15 — 2026-09-25
6
32
 
7
33
  ### Removed (breaking for anyone using them)
package/README.md CHANGED
@@ -307,11 +307,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
307
307
  above the centre (2 × (40% − the title bar)): a card taller than that is
308
308
  not a centred card — it is a plain scrolling `.screen` (the detail view).
309
309
  - **A small group at the same centre** — an empty state; a card with its quiet
310
- second button and the pager dots under it; a deck with its two round
311
- buttons — is `.screen__centre` (React: `Centre`), a block in the fill
310
+ second button and the pager dots under it — is `.screen__centre` (React: `Centre`), a block in the fill
312
311
  screen after the topbar. Same placement and cap; it runs edge to edge with
313
- the inset as padding, so a deck's fanned cards may overhang and clip at the
314
- screen's edges (the fold visual, not a bug), and stacks its children at the
312
+ the inset as padding, and stacks its children at the
315
313
  screen's gap. Never hand-lay `top: 40%` on a screen.
316
314
  - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
317
315
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
@@ -588,8 +586,8 @@ Rules:
588
586
  theme's values are the project's call. The derivations are only starting points;
589
587
  any role can be set outright.
590
588
  - The member's text-size setting is the same `--fs-base`, written on `<html>`
591
- by the app at run time (2.2rem = 160%): the words grow, the things keep their
592
- size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
589
+ at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
590
+ theme's own): the words grow, the things keep their size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
593
591
 
594
592
  Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
595
593
  palette `own` shows a colour well per colour base.
@@ -619,8 +617,8 @@ whole system still resizes from one number: the root font size. `base.css` keeps
619
617
  default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
620
618
  column, its 48px taps and its type in proportion on 320–360 screens. Only
621
619
  `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
622
- An app with its own text-size setting writes `--fs-base` on `<html>` (the type
623
- base, 1.375rem = 100%; 2.2rem = 160%): only the type and its holders (the count
620
+ An app with its own text-size setting calls `setTextScale(percent)`, which writes
621
+ `--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): only the type and its holders (the count
624
622
  badge, the inline icon) grow; taps, spacing and radii keep their size.
625
623
  Components never write a raw length: spacing from `--sp-*`, control sizes from
626
624
  `--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
package/css/actions.css CHANGED
@@ -72,7 +72,7 @@
72
72
  border-radius: var(--appbar-who-radius, var(--r-chip));
73
73
  -webkit-tap-highlight-color: transparent;
74
74
  }
75
- .appbar__who:focus-visible { outline: var(--focus); outline-offset: 2px; }
75
+ .appbar__who:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
76
76
  .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
77
77
  .appbar__title { font: var(--type-label); }
78
78
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
@@ -333,3 +333,26 @@
333
333
  .card .action-note { color: var(--action-note-ink, var(--card-muted)); }
334
334
  .action-note__words { display: block; } /* ONE run of words: an icon inside sits in the line, the spaces around it kept */
335
335
  .action-note__words > .icon { display: inline-block; vertical-align: -0.2em; width: 1.25em; height: 1.25em; margin-inline: 0.15em; color: var(--action-note-icon-ink, inherit); } /* a project may colour the icon apart from the words — e.g. the system blue of the control it points at (Lh 2026-09-24) */
336
+
337
+ /* PRESS (Lh 2026-09-25): every CONTROL answers a tap — it gives under the finger and springs back, its choice fill fades in;
338
+ INFORMATION never does (no pointer, no press). One list, zero specificity: a component's own rule always wins. */
339
+ :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
340
+ .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item) {
341
+ cursor: pointer;
342
+ touch-action: manipulation;
343
+ transition: scale var(--motion-press) ease-out;
344
+ }
345
+ /* a CHOICE's fill fades in as it is picked (a button's colour never changes on a tap, so it does not fade) */
346
+ :where(.chip--pick, .tile--pick, .day, .segment__item, .calendar__day, .float-bar__item, .menu__item) {
347
+ transition: scale var(--motion-press) ease-out, background-color var(--motion-press) ease-out, color var(--motion-press) ease-out, border-color var(--motion-press) ease-out;
348
+ }
349
+ :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
350
+ .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item):active:not(:disabled, [aria-disabled="true"]) {
351
+ scale: var(--press-scale);
352
+ }
353
+ :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, a.tile, button.tile, .day, .segment__item, .calendar__day, .float-bar__item,
354
+ .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
355
+ @media (prefers-reduced-motion: reduce) {
356
+ :where(.btn, .icon-btn, .fab, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item,
357
+ .calendar__day, .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .sheet-stack__back, .dropdown__btn, .appbar__who, .menu__item):active { scale: none; }
358
+ }
package/css/base.css CHANGED
@@ -9,7 +9,7 @@
9
9
  the member wants to copy), and inputs are untouched. Native buttons and
10
10
  links are matched by element; the rest by the class that makes them one */
11
11
  button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--pick,
12
- .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .dropdown__trigger, .track__step, .pager__at {
12
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .track__step, .pager__at {
13
13
  user-select: none;
14
14
  -webkit-user-select: none;
15
15
  -webkit-touch-callout: none;
@@ -102,7 +102,7 @@ button { font: inherit; cursor: pointer; }
102
102
 
103
103
  .t-muted { color: var(--on-surface-muted); }
104
104
  .card .t-muted { color: var(--card-muted); } /* muted follows the card's ink, not the surface's */
105
- .card--3 .t-muted { color: inherit; opacity: 0.7; } /* card-muted is tuned for light cards; dim instead on the dark slot */
105
+ .card--3 .t-muted { color: inherit; opacity: var(--dim); } /* card-muted is tuned for light cards; dim instead on the dark slot */
106
106
  .t-caps { text-transform: uppercase; }
107
107
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
108
108
  /* read, not shown (Lh 2026-09-23): words a screen reader needs — a page's h1 the design leaves out — kept in the page, taking no room.
package/css/card.css CHANGED
@@ -161,8 +161,7 @@
161
161
  one is there, and the screen's gap), or twice the room below it (the
162
162
  bar's reserve) — whichever is less. --_lead is the title bar's height
163
163
  only on a screen that HAS one: without a .topbar/.appbar before it the
164
- block keeps that room (a deck and its two buttons, a card and its pager
165
- under it, need it on a short phone; Lh, 2026-09-11). */
164
+ block keeps that room (a card and its pager under it needs it on a short phone; Lh, 2026-09-11). */
166
165
  --_lead: 0rem;
167
166
  max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
168
167
  overflow-y: auto;
@@ -173,9 +172,8 @@
173
172
 
174
173
  /* ---- centred block: the same place, for one thing to read that is not a
175
174
  lone card — an empty state, a card with its quiet second button and the
176
- pager under it, a deck with its two round buttons. The block runs edge to
177
- edge and keeps the screen's inset as padding, so a deck's fanned cards can
178
- overhang and clip at the screen's edges (correct — the fold visual), and
175
+ pager under it. The block runs edge to
176
+ edge and keeps the screen's inset as padding, and
179
177
  its children stack at the screen's own rhythm (Lh, 2026-09-11):
180
178
  <div class="screen screen--fill"><header class="topbar">…</header>
181
179
  <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
@@ -248,14 +246,10 @@
248
246
  animation-timing-function: ease-out;
249
247
  mix-blend-mode: normal;
250
248
  }
251
- :root[data-slide="next"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-left; }
252
- :root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
253
- :root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
254
- :root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
255
- :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
256
- :root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
257
- :root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
258
- :root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
249
+ :root[data-slide="next"]::view-transition-old(centre-card-out), :root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
250
+ :root[data-slide="next"]::view-transition-new(centre-card-in), :root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
251
+ :root[data-slide="back"]::view-transition-old(centre-card-out), :root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
252
+ :root[data-slide="back"]::view-transition-new(centre-card-in), :root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
259
253
  @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
260
254
  @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
261
255
  @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
package/css/choice.css CHANGED
@@ -139,7 +139,7 @@
139
139
  .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
140
140
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
141
141
  .calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
142
- .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
142
+ .card--3 .calendar__wd { color: inherit; opacity: var(--dim); }
143
143
  .calendar__day {
144
144
  width: 100%;
145
145
  max-width: var(--calendar-day-max-w, var(--tap));
@@ -242,7 +242,7 @@
242
242
  .composer__input::placeholder { color: inherit; opacity: 0.4; }
243
243
  .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--composer-bar-gap, var(--sp-3)); }
244
244
  .composer__hint { font: var(--composer-hint-font, var(--type-caption)); color: var(--composer-hint-ink, var(--card-muted)); }
245
- .card--3 .composer__hint { color: inherit; opacity: 0.7; }
245
+ .card--3 .composer__hint { color: inherit; opacity: var(--dim); }
246
246
 
247
247
  /* ---- slider: one value along a track, a thumb-size knob ----
248
248
  <label class="slider"><span class="slider__label">ขนาดตัวหนังสือ</span><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label>
package/css/journey.css CHANGED
@@ -26,7 +26,7 @@
26
26
  .route__point--end .route__code::after { content: none; }
27
27
  .route__time { font: var(--route-time-font, var(--type-body)); font-variant-numeric: tabular-nums; }
28
28
  .route__place { font: var(--route-place-font, var(--type-caption)); color: var(--route-place-ink, var(--card-muted)); }
29
- .card--3 .route__place { color: inherit; opacity: 0.7; }
29
+ .card--3 .route__place { color: inherit; opacity: var(--dim); }
30
30
 
31
31
  .route__via { grid-column: 2; grid-row: 2; align-self: center; display: flex; align-items: center; opacity: 0.65; font-size: var(--route-via-font-size, var(--fs-body)); }
32
32
 
@@ -40,7 +40,7 @@
40
40
  background: var(--tile-badge-bg, var(--card-3-bg));
41
41
  color: var(--tile-badge-ink, var(--card-3-ink));
42
42
  }
43
- .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
43
+ .tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: var(--dim); }
44
44
  .tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
45
45
 
46
46
  /* plain: no fill — a date box beside a title (Jun / 18) */
@@ -58,4 +58,4 @@
58
58
  font: var(--note-row-font, var(--type-caption));
59
59
  color: var(--note-row-ink, var(--card-muted));
60
60
  }
61
- .card--3 .note-row { color: inherit; opacity: 0.7; }
61
+ .card--3 .note-row { color: inherit; opacity: var(--dim); }
package/css/layover.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /* ============================================================
2
2
  cardds/layover.css — cards that lay OVER other content:
3
3
  Modal (ask) · popover menu · expanding banner · float bar ·
4
- callout · swipe deck. The sheet (peek / half / full / page)
4
+ callout. The sheet (peek / half / full / page)
5
5
  lives in sheet.css; these compose with .sheet-stage.
6
6
  ============================================================ */
7
7
 
@@ -72,7 +72,7 @@
72
72
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
73
73
  --on-accent: var(--card-3-bg);
74
74
  }
75
- .modal__drawer .t-caption { opacity: 0.7; }
75
+ .modal__drawer .t-caption { opacity: var(--dim); }
76
76
  .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
77
77
  .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
78
78
  @media (prefers-reduced-motion: no-preference) {
@@ -144,7 +144,7 @@
144
144
  cursor: pointer;
145
145
  }
146
146
  .menu__item:hover, .menu__item:focus-visible { background: var(--ink-tint); outline: none; }
147
- .menu__item[aria-current], .menu__item.is-on { background: var(--accent); color: var(--on-accent); }
147
+ .menu__item[aria-current] { background: var(--accent); color: var(--on-accent); }
148
148
  @supports (anchor-name: --a) {
149
149
  .menu[popover] {
150
150
  position-area: block-end span-inline-end;
@@ -255,8 +255,7 @@
255
255
  }
256
256
  /* aria-current="false" is the correct way to mark the OTHER tabs, so the
257
257
  bare attribute selector lights every one of them up */
258
- .float-bar__item[aria-current]:not([aria-current="false"]),
259
- .float-bar__item.is-on { background: var(--accent); color: var(--on-accent); opacity: 1; }
258
+ .float-bar__item[aria-current]:not([aria-current="false"]) { background: var(--accent); color: var(--on-accent); opacity: 1; }
260
259
  .float-bar > .icon-btn { width: var(--bar-h); height: var(--bar-h); box-shadow: var(--shadow-float); }
261
260
 
262
261
  /* ---- callout: a small card with a tail — over a map, a media, a mascot ----
@@ -290,18 +289,7 @@
290
289
  border-left: 0;
291
290
  }
292
291
  .callout__title { font: var(--callout-title-font, var(--type-title)); }
293
- .callout__sub { font: var(--callout-sub-font, var(--type-caption)); opacity: 0.7; }
294
- /* ---- deck: swipe cards; the last child is on top ----
295
- <div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
296
- <div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
297
- .deck { display: grid; place-items: center; padding: var(--deck-pad-top, var(--sp-4)) var(--deck-pad-x, var(--sp-4)) var(--deck-pad-bottom, var(--sp-6)); }
298
- .deck > .card { grid-area: 1 / 1; width: min(100%, 20rem); min-height: 12.5rem; transition: rotate 0.3s ease, translate 0.3s ease; }
299
- .deck > .card:nth-last-child(2) { rotate: -4deg; translate: calc(-1 * var(--sp-2)) var(--sp-2); opacity: 0.85; }
300
- .deck > .card:nth-last-child(3) { rotate: 5deg; translate: var(--sp-3) var(--sp-4); opacity: 0.6; }
301
- .deck > .card:nth-last-child(n + 4) { display: none; }
302
- .deck__actions { display: flex; justify-content: center; gap: var(--deck-actions-gap, var(--sp-8)); }
303
- .deck__actions > .icon-btn { width: var(--bar-h); height: var(--bar-h); }
304
-
292
+ .callout__sub { font: var(--callout-sub-font, var(--type-caption)); opacity: var(--dim); }
305
293
  /* ---- splash: the app's mark at the human centre while it boots (Lh 2026-09-24) ----
306
294
  <section class="screen screen--fill splash"><div class="splash__group"><div class="splash__mark">{the app's lottie/img}</div>
307
295
  <p class="splash__text">…</p></div></section>
package/css/media.css CHANGED
@@ -53,7 +53,7 @@
53
53
  .quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
54
54
  .quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
55
55
  .quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
56
- .card--3 .quote__by { color: inherit; opacity: 0.7; }
56
+ .card--3 .quote__by { color: inherit; opacity: var(--dim); }
57
57
 
58
58
  /* ---- tile grid: big square tiles — icon top, label bottom ----
59
59
  <div class="tile-grid"><button class="tile"><svg class="icon"/><span class="tile__label">Light</span></button>
@@ -79,11 +79,10 @@
79
79
  font: inherit;
80
80
  text-align: left;
81
81
  position: relative;
82
- cursor: pointer;
83
82
  }
84
83
  .tile__label { font: var(--tile-label-font, var(--type-title)); }
85
84
  .tile__value { font: var(--tile-value-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
86
- .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: 0.7; }
85
+ .tile__sub { font: var(--tile-sub-font, var(--type-caption)); opacity: var(--dim); }
87
86
  .tile__text { display: grid; gap: var(--tile-text-gap, var(--sp-0)); }
88
87
  .tile--on { background: var(--tile-on-bg, var(--accent)); color: var(--tile-on-ink, var(--on-accent)); border-color: transparent; }
89
88
  .tile--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
@@ -91,7 +90,7 @@
91
90
  .tile--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
92
91
  .tile--wide { grid-column: 1 / -1; }
93
92
  /* 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
+ .tile--stat { border-color: transparent; background: none; padding-inline: 0; }
95
94
  .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; }
96
95
  /* upload: dashed, centred plus + label */
97
96
  .tile--upload {
@@ -115,7 +114,7 @@
115
114
  the corner read as a notch with a line through it rather than a lifted flap.
116
115
  The COLOUR goes transparent rather than the border going to 0, so the box
117
116
  keeps its geometry and a fold card still measures the same as the cards
118
- beside it in a deck. */
117
+ beside it. */
119
118
  .card--fold {
120
119
  border-color: transparent;
121
120
  border-bottom-right-radius: 0;
package/css/numbers.css CHANGED
@@ -33,7 +33,7 @@
33
33
  }
34
34
  .ring__value { font: var(--ring-value-font, var(--type-stat)); letter-spacing: var(--track-display); }
35
35
  .ring__label { font: var(--ring-label-font, var(--type-caption)); color: var(--ring-label-ink, var(--card-muted)); }
36
- .card--3 .ring__label { color: inherit; opacity: 0.7; }
36
+ .card--3 .ring__label { color: inherit; opacity: var(--dim); }
37
37
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
38
38
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
39
39
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
@@ -86,6 +86,7 @@
86
86
  background: color-mix(in srgb, var(--_ink) 30%, transparent);
87
87
  color: var(--_ink);
88
88
  }
89
+ .track__step > .track__dot { display: grid; place-items: center; padding: 0; font: var(--track-dot-font, var(--type-title)); font-variant-numeric: tabular-nums; } /* a number in the round (TrackStep num) */
89
90
  .track__step.is-done > .icon,
90
91
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
91
92
 
@@ -107,7 +108,7 @@
107
108
  }
108
109
  .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
109
110
  .band__foot { grid-area: foot; font: var(--band-foot-font, var(--type-caption)); color: var(--band-foot-ink, var(--card-muted)); }
110
- .card--3 .band__foot { color: inherit; opacity: 0.7; }
111
+ .card--3 .band__foot { color: inherit; opacity: var(--dim); }
111
112
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
112
113
  between each. Bands may still carry slot tints (card--2 / card--3). */
113
114
  .band-stack {
@@ -206,7 +207,7 @@
206
207
  }
207
208
  .picker::after { left: auto; right: calc(100% / 3); }
208
209
  .picker__unit { font: var(--picker-unit-font, var(--type-caption)); color: var(--picker-unit-ink, var(--card-muted)); }
209
- .card--3 .picker__unit { color: inherit; opacity: 0.7; }
210
+ .card--3 .picker__unit { color: inherit; opacity: var(--dim); }
210
211
  /* the scrolled-to item lights up by position — no JS */
211
212
  @supports (animation-timeline: view(x)) {
212
213
  .picker__item { opacity: 0.3; animation: picker-focus linear both; animation-timeline: view(x); }
package/css/people.css CHANGED
@@ -31,7 +31,7 @@
31
31
  .avatar--xl { --_size: var(--avatar-xl); } /* sheet head icon ring */
32
32
  /* outline avatar: no fill, an edge — icons in a ring, "add" slots */
33
33
  .avatar--outline { background: transparent; color: inherit; border: var(--border-w) solid var(--avatar-outline-border, var(--outline)); }
34
- .avatar--add { background: transparent; color: inherit; border: var(--border-w) dashed var(--avatar-add-border, var(--outline)); cursor: pointer; }
34
+ .avatar--add { background: transparent; color: inherit; border: var(--border-w) dashed var(--avatar-add-border, var(--outline)); }
35
35
  /* accent avatar: the "+7" tail, the selected person */
36
36
  .avatar--on { background: var(--avatar-on-bg, var(--accent)); color: var(--avatar-on-ink, var(--on-accent)); }
37
37
  /* halo: faint concentric rings behind a hero avatar (decorative) */
@@ -76,4 +76,5 @@
76
76
  }
77
77
  .avatar-pick__name { font: var(--avatar-pick-name-font, var(--type-label)); font-size: var(--avatar-pick-name-font-size, var(--fs-sm)); }
78
78
  .avatar-pick__role { font: var(--avatar-pick-role-font, var(--type-caption)); color: var(--avatar-pick-role-ink, var(--card-muted)); margin-top: calc(-1 * var(--sp-1)); }
79
- .card--3 .avatar-pick__role { color: inherit; opacity: 0.7; }
79
+ .avatar-pick__item[aria-pressed="true"] > .avatar { background: var(--avatar-pick-on-bg, var(--accent)); color: var(--avatar-pick-on-ink, var(--on-accent)); border-color: transparent; } /* the picked person */
80
+ .card--3 .avatar-pick__role { color: inherit; opacity: var(--dim); }
package/css/sheet.css CHANGED
@@ -380,9 +380,8 @@
380
380
  bottom of the body, each its title line. Every step has a thin rule on top. A step opens and folds as a 1fr ↔ 0fr grid row, so the pile rises and
381
381
  falls — no height is written. Nothing fades: done and next read at full ink. */
382
382
  .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
383
- .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; }
383
+ .sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; position: relative; }
384
384
  .sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1rem; height: 1rem; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
385
- .sheet-steps { position: relative; }
386
385
  .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
387
386
 
388
387
  .sheet-step {
@@ -400,23 +399,22 @@
400
399
  display: flex; align-items: center; gap: var(--sp-3); min-height: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
401
400
  padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
402
401
  background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
403
- border-top: var(--sheet-step-border-w, 0px) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
402
+ border-top: var(--sheet-step-border-w, 0rem) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
404
403
  }
405
- .sheet-step__summary { color: inherit; opacity: 0.8; }
406
404
  .sheet-step__title { font: var(--sheet-step-title-font, var(--type-label)); margin: 0; flex: 0 1 auto; min-width: 0; outline: none; /* the label size, not a body title: a pile of rows must stay small (Lh 2026-09-24) */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
407
405
  .sheet-step__num { /* `numbered`: the step's number in a disc BEFORE its title (Lh 2026-09-25) — a white disc on the bar, the number in the ink */
408
406
  flex: none; display: grid; place-items: center; inline-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2)); block-size: var(--sheet-step-num-size, calc(var(--tap-base) / 2));
409
407
  border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); font-variant-numeric: tabular-nums;
410
408
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
411
409
  }
412
- .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
410
+ .sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
413
411
  .sheet-step__check { margin-inline-start: auto; }
414
412
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
415
413
  .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); }
416
414
  .sheet-step__inner > * { min-width: 0; }
417
415
  .sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
418
416
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
419
- .sheet-step__go { position: absolute; inset: min(0px, (var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; padding: 0; border: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
417
+ .sheet-step__go { position: absolute; inset: min(0rem, (var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; padding: 0; border: 0; background: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
420
418
  /* the last step's finish, when the app gives one: under the steps */
421
419
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
422
420
  @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }
package/css/stack.css CHANGED
@@ -243,9 +243,6 @@
243
243
  padding-left: 0;
244
244
  transition: padding-left 0.35s ease;
245
245
  }
246
- .sheet-stack--tap > .card.is-open > .card__head {
247
- padding-left: calc(var(--tap-sm) + var(--sp-3));
248
- }
249
246
  .sheet-stack--tap .sheet-stack__back {
250
247
  position: absolute;
251
248
  left: 0;
@@ -254,6 +251,12 @@
254
251
  opacity: 0;
255
252
  visibility: hidden;
256
253
  transition: opacity 0.35s ease, visibility 0.35s;
254
+ /* SMALL (see "the back button, SMALL" below) */
255
+ width: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
256
+ height: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
257
+ min-width: 0;
258
+ min-height: 0;
259
+ padding: 0;
257
260
  }
258
261
  .sheet-stack--tap > .card.is-open .sheet-stack__back {
259
262
  opacity: 1;
@@ -313,13 +316,6 @@
313
316
  tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
314
317
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
315
318
  .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) + var(--sp-2))); }
316
- .sheet-stack--tap .sheet-stack__back {
317
- width: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
318
- height: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
319
- min-width: 0;
320
- min-height: 0;
321
- padding: 0;
322
- }
323
319
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
324
320
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2); }
325
321
 
package/css/step.css CHANGED
@@ -90,8 +90,6 @@
90
90
  .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
91
91
  .step > :is(.page, .pages) {
92
92
  flex: 0 1 auto;
93
- }
94
- .step > :is(.page, .pages) {
95
93
  min-height: 0;
96
94
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
97
95
  overflow-y: auto;
@@ -103,12 +101,12 @@
103
101
  padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
104
102
  The walk shows no scrollbar. */
105
103
  .step > .pages { margin: 0; scrollbar-width: none; touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin (Lh 2026-09-23) */
106
- .step > .pages > .page { padding-inline: var(--screen-pad); } /* the words keep the inset INSIDE it */
107
- .step > .pages > .page { margin-top: var(--_lead, 0rem); } /* the lead is each PAGE's margin (15% with no top, the screen's gap under a bar): the words start there and scroll up to the box's edge */
108
- .step > .pages::-webkit-scrollbar { display: none; }
109
104
  .step > .pages > .page {
105
+ padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
106
+ margin-top: var(--_lead, 0rem); /* the lead is each PAGE's margin (15% with no top, the screen's gap under a bar): the words start there and scroll up to the box's edge */
110
107
  padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
111
108
  }
109
+ .step > .pages::-webkit-scrollbar { display: none; }
112
110
 
113
111
  /* ============================================================
114
112
  TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
@@ -48,5 +48,6 @@
48
48
  --card-1-bg --card-1-ink --card-2-bg --card-2-ink --card-3-bg --card-3-ink (three card tones)
49
49
  --card-border --card-muted --card-2-muted
50
50
  --accent --on-accent --action-bg --action-ink --action-border
51
- --pos --neg --dot-a --dot-b */
51
+ --pos --neg --dot-a --dot-b
52
+ --dim (0.7: how far secondary words fade on a dark card, a drawer, a tile's sub line — one share for all) */
52
53
  }
package/css/tokens.css CHANGED
@@ -48,7 +48,7 @@
48
48
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
49
49
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
50
50
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
51
- --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
51
+ --bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell */
52
52
  --header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
53
53
  --card-pad-rest: var(--sp-5); /* 20 a card's padding before any variant — .card publishes it as --card-pad; the sheet's peek strip is built from it where no card is in scope */
54
54
  --card-gap-rest: var(--sp-4); /* 16 …and its gap */
@@ -95,6 +95,9 @@
95
95
  --motion-sheet: 0.35s;
96
96
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
97
97
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
98
+ --dim: 0.7; /* secondary words dimmed on a ground whose muted ink does not fit (the dark card --3, a drawer, a tile's sub line): ONE share for all */
99
+ --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
100
+ --press-scale: 0.96; /* how far it gives — a share of its size */
98
101
 
99
102
  /* ---- typography: one scale, rem so it follows the root ---- */
100
103
  --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif; /* fonts.css ships it; the rest is the fallback while it loads */
@@ -1,15 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createContext, isValidElement, useContext, useRef, useState } from 'react';
2
+ import { Children, createContext, isValidElement, useContext, useRef } from 'react';
3
+ import { useControlled } from '../state.js';
3
4
  import { cx } from '../cx.js';
4
5
  import { Icon } from '../type/Icon.js';
5
6
  /* what a Menu tells its items: the value it holds (so the matching item marks itself current) and how to pick one */
6
7
  const MenuContext = createContext(null);
7
8
  /* a menu's value is controlled (`value` + `onChange`) or its own (`defaultValue`) — like an input */
8
9
  function useValue(value, defaultValue, onChange) {
9
- const [inner, setInner] = useState(defaultValue);
10
- const current = value ?? inner;
11
- const pick = (next) => { if (value === undefined)
12
- setInner(next); if (next !== current)
10
+ const [current, setOwn] = useControlled(value, defaultValue);
11
+ const pick = (next) => { setOwn(next); if (next !== current)
13
12
  onChange?.(next); };
14
13
  return [current, pick];
15
14
  }