@usableapp/cardds 0.7.15 → 0.7.17

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 (77) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +29 -28
  3. package/cardds.css +3 -3
  4. package/css/actions.css +67 -22
  5. package/css/base.css +44 -18
  6. package/css/card.css +25 -26
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +1 -3
  10. package/css/layover.css +19 -41
  11. package/css/lists.css +1 -6
  12. package/css/media.css +9 -18
  13. package/css/numbers.css +6 -21
  14. package/css/people.css +3 -9
  15. package/css/sheet.css +15 -19
  16. package/css/stack.css +21 -29
  17. package/css/step.css +7 -12
  18. package/css/theme.template.css +2 -1
  19. package/css/tokens.css +22 -4
  20. package/dist/actions/Btn.d.ts +1 -1
  21. package/dist/actions/Dropdown.js +3 -9
  22. package/dist/actions/Fab.d.ts +1 -1
  23. package/dist/actions/Fab.js +3 -2
  24. package/dist/cardds.css +264 -267
  25. package/dist/cards/Card.d.ts +24 -8
  26. package/dist/cards/Card.js +23 -8
  27. package/dist/cards/CardHead.d.ts +0 -1
  28. package/dist/cards/CardHead.js +1 -1
  29. package/dist/choice/Calendar.js +2 -5
  30. package/dist/choice/Mood.d.ts +10 -5
  31. package/dist/choice/Mood.js +4 -2
  32. package/dist/forms/Field.js +2 -3
  33. package/dist/forms/FileBtn.js +1 -1
  34. package/dist/forms/Pin.js +7 -12
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/layover/Banner.d.ts +10 -3
  38. package/dist/layover/Banner.js +6 -3
  39. package/dist/lists/Row.d.ts +2 -1
  40. package/dist/lists/Row.js +5 -5
  41. package/dist/media/Postcard.js +3 -5
  42. package/dist/numbers/Band.d.ts +3 -1
  43. package/dist/numbers/Picker.js +3 -10
  44. package/dist/numbers/Track.d.ts +4 -2
  45. package/dist/numbers/Track.js +3 -3
  46. package/dist/people/AvatarPick.d.ts +3 -1
  47. package/dist/people/AvatarPick.js +2 -2
  48. package/dist/scaffold/ActionBar.d.ts +3 -3
  49. package/dist/scaffold/ActionBar.js +2 -2
  50. package/dist/scaffold/Centre.d.ts +2 -2
  51. package/dist/scaffold/Centre.js +2 -2
  52. package/dist/scaffold/PagerAt.d.ts +1 -1
  53. package/dist/scaffold/PagerAt.js +1 -1
  54. package/dist/scaffold/Screen.js +4 -7
  55. package/dist/scaffold/TopBar.js +3 -5
  56. package/dist/sheets/Drawer.js +3 -4
  57. package/dist/sheets/Sheet.js +9 -22
  58. package/dist/sheets/SheetStack.d.ts +2 -2
  59. package/dist/sheets/SheetStack.js +20 -13
  60. package/dist/sheets/SheetSteps.js +8 -11
  61. package/dist/sheets/StackBack.js +2 -1
  62. package/dist/sheets/StackSheet.d.ts +3 -2
  63. package/dist/sheets/StackSheet.js +7 -11
  64. package/dist/state.d.ts +24 -0
  65. package/dist/state.js +57 -0
  66. package/dist/templates/CallSheet.js +2 -4
  67. package/dist/templates/Home.d.ts +3 -2
  68. package/dist/templates/Home.js +2 -4
  69. package/dist/templates/Splash.js +3 -3
  70. package/dist/templates/Walkthrough.js +4 -5
  71. package/dist/type/Icon.js +3 -5
  72. package/dist/type/Text.d.ts +1 -1
  73. package/dist/type/textScale.d.ts +6 -0
  74. package/dist/type/textScale.js +12 -0
  75. package/package.json +7 -4
  76. package/dist/layover/Deck.d.ts +0 -9
  77. package/dist/layover/Deck.js +0 -10
package/css/numbers.css CHANGED
@@ -33,7 +33,6 @@
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; }
37
36
  .ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
38
37
  .ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
39
38
  .ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
@@ -72,7 +71,7 @@
72
71
  content: "";
73
72
  flex: 1;
74
73
  height: var(--sp-3);
75
- background: color-mix(in srgb, var(--_ink) 30%, transparent);
74
+ background: var(--ink-rail); /* the track's own ink at 30% — identical to color-mix(in srgb, var(--_ink) 30%, transparent): --ink-rail already reads currentColor, and .track sets color to --_ink */
76
75
  }
77
76
  .track__step:first-child::before { display: none; }
78
77
  .track__step.is-done::before { background: var(--accent); }
@@ -83,9 +82,10 @@
83
82
  flex: none;
84
83
  padding: calc((var(--tap-lg) - var(--icon)) / 2);
85
84
  border-radius: 50%;
86
- background: color-mix(in srgb, var(--_ink) 30%, transparent);
85
+ background: var(--ink-rail);
87
86
  color: var(--_ink);
88
87
  }
88
+ .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
89
  .track__step.is-done > .icon,
90
90
  .track__step.is-done > .track__dot { background: var(--accent); color: var(--on-accent); }
91
91
 
@@ -107,7 +107,6 @@
107
107
  }
108
108
  .band__num small { font-size: 0.5em; font-weight: 700; margin-right: 0.1em; } /* unit or % */
109
109
  .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
110
  /* band stack: ONE card boundary; bands are frameless inside it, a hairline
112
111
  between each. Bands may still carry slot tints (card--2 / card--3). */
113
112
  .band-stack {
@@ -154,14 +153,6 @@
154
153
  @supports not (width: round(up, 1px, 1px)) {
155
154
  .dotgrid::before { display: none; }
156
155
  }
157
- .dotgrid--stars {
158
- background-image: none;
159
- }
160
- .dotgrid--stars,
161
- .dotgrid--stars::before {
162
- --_star: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
163
- }
164
-
165
156
  /* ---- picker: horizontal snap picker, centre value big ----
166
157
  <div class="picker"><div class="picker__rail"><button class="picker__item">100</button>…</div><span class="picker__unit">ml</span></div> */
167
158
  .picker { display: grid; gap: var(--picker-gap, var(--sp-1)); justify-items: center; position: relative; }
@@ -176,20 +167,15 @@
176
167
  grid-auto-columns: 100%;
177
168
  overflow-x: auto;
178
169
  scroll-snap-type: x mandatory;
179
- scrollbar-width: none;
180
170
  position: relative;
181
171
  }
182
- .picker__rail::-webkit-scrollbar { display: none; }
183
172
  .picker__item {
184
173
  scroll-snap-align: center;
185
- border: 0;
186
- background: transparent;
187
- color: inherit;
188
174
  padding: var(--picker-item-pad-y, var(--sp-2)) var(--picker-item-pad-x, 0);
189
175
  font: var(--picker-item-font, var(--type-stat));
190
176
  letter-spacing: var(--track-display);
191
177
  font-variant-numeric: tabular-nums;
192
- opacity: 0.3;
178
+ opacity: var(--faint);
193
179
  text-align: center;
194
180
  }
195
181
  .picker__item[aria-selected="true"] { opacity: 1; }
@@ -206,12 +192,11 @@
206
192
  }
207
193
  .picker::after { left: auto; right: calc(100% / 3); }
208
194
  .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
195
  /* the scrolled-to item lights up by position — no JS */
211
196
  @supports (animation-timeline: view(x)) {
212
- .picker__item { opacity: 0.3; animation: picker-focus linear both; animation-timeline: view(x); }
197
+ .picker__item { animation: picker-focus linear both; animation-timeline: view(x); } /* opacity: var(--faint) already set above — the animation alone is new here */
213
198
  @keyframes picker-focus {
214
- 0%, 100% { opacity: 0.3; }
199
+ 0%, 100% { opacity: var(--faint); }
215
200
  45%, 55% { opacity: 1; }
216
201
  }
217
202
  }
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) */
@@ -57,10 +57,8 @@
57
57
  display: flex;
58
58
  gap: var(--avatar-pick-gap, var(--sp-4));
59
59
  overflow-x: auto;
60
- scroll-snap-type: x proximity;
61
- scrollbar-width: none; /* starts at the card's padding edge, like every row */
60
+ scroll-snap-type: x proximity; /* starts at the card's padding edge, like every row */
62
61
  }
63
- .avatar-pick::-webkit-scrollbar { display: none; }
64
62
  .avatar-pick__item {
65
63
  scroll-snap-align: start;
66
64
  flex: none;
@@ -68,12 +66,8 @@
68
66
  display: grid;
69
67
  justify-items: center;
70
68
  gap: var(--avatar-pick-item-gap, var(--sp-2));
71
- border: 0;
72
- background: transparent;
73
- color: inherit;
74
- padding: 0;
75
69
  text-align: center;
76
70
  }
77
71
  .avatar-pick__name { font: var(--avatar-pick-name-font, var(--type-label)); font-size: var(--avatar-pick-name-font-size, var(--fs-sm)); }
78
72
  .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; }
73
+ .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 */
package/css/sheet.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* ============================================================
2
2
  cardds/sheet.css — sheet single: one card as a bottom sheet.
3
- Four states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full
3
+ Five states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full · .sheet--away
4
4
 
5
5
  A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
6
6
  (stack.css): it is ALWAYS the full height of the box that holds it —
@@ -41,8 +41,8 @@
41
41
  the body. The handle and the head stay where they are at every
42
42
  state; the body is what scrolls — a sheet is a card whose head is
43
43
  pinned, not a scrolling column with a title somewhere in it.
44
- cardds.js adds the handle's click / drag / keyboard and the focus
45
- raise; without it every state still renders.
44
+ The Sheet component adds the handle's click / drag / keyboard and the
45
+ focus raise; without it every state still renders.
46
46
  ============================================================ */
47
47
 
48
48
  .sheet {
@@ -70,7 +70,7 @@
70
70
  position: absolute;
71
71
  inset: 0;
72
72
  top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
73
- translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: cardds.js, only while the handle is held */
73
+ translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: the Sheet component, only while the handle is held */
74
74
  transition: translate var(--motion-sheet) ease;
75
75
 
76
76
  /* handle · head · body: a column, the body takes what is left and scrolls
@@ -81,6 +81,7 @@
81
81
  border-radius: var(--r-card) var(--r-card) 0 0;
82
82
  border-bottom: 0;
83
83
  box-shadow: var(--sheet-shadow, var(--shadow-sheet));
84
+ --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
84
85
  }
85
86
  .sheet--full { --_y: 0%; --_cover: 0rem; }
86
87
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
@@ -146,9 +147,8 @@
146
147
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
147
148
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
148
149
  One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
149
- .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
150
150
  /* …and the link row under the button, when there is one (its line + the gap over it) */
151
- .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + calc(var(--tap-base) * 7 / 12) + var(--card-pad)); }
151
+ .sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
152
152
  .sheet > .card__foot {
153
153
  position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
154
154
  align-items: end;
@@ -174,7 +174,7 @@
174
174
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
175
175
  the thumb's grab area for the whole strip above the title: a pseudo widens
176
176
  its hit box to --tap tall and wider than the bar without changing the
177
- picture. touch-action: none hands a touch drag to cardds.js instead of to
177
+ picture. touch-action: none hands a touch drag to the Sheet component instead of to
178
178
  the scroller behind it. */
179
179
  .sheet__handle {
180
180
  position: relative;
@@ -248,7 +248,7 @@
248
248
  .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
249
249
  /* a stage laid OVER its screen (a Modal a link opened, Lh 2026-09-23): the page shows through, the stage covers it and its bar */
250
250
  .sheet-stage--over, .screen > .sheet-stage--over {
251
- position: absolute; inset: 0; z-index: 40;
251
+ position: absolute; inset: 0; z-index: var(--z-top);
252
252
  width: auto; height: auto; max-width: none; margin: 0;
253
253
  background: transparent;
254
254
  --_top: 0rem;
@@ -305,7 +305,7 @@
305
305
  translate: -50% -50%;
306
306
  border-radius: 50%;
307
307
  border: var(--border-w) solid var(--map-ring-border, var(--accent));
308
- background: var(--map-ring-bg, color-mix(in srgb, var(--accent) 22%, transparent));
308
+ background: var(--map-ring-bg, var(--accent-soft));
309
309
  box-shadow: var(--map-ring-shadow, var(--shadow-float));
310
310
  pointer-events: none;
311
311
  z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
@@ -339,9 +339,6 @@
339
339
  z-index: 1;
340
340
  }
341
341
 
342
- /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
343
- edge shows, so it gets its corners and border back; full still stops at
344
- the top bar's line, because the top edge never moved. */
345
342
  /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
346
343
  foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
347
344
  scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
@@ -380,9 +377,8 @@
380
377
  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
378
  falls — no height is written. Nothing fades: done and next read at full ink. */
382
379
  .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; }
380
+ .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
381
  .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
382
  .sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
387
383
 
388
384
  .sheet-step {
@@ -390,6 +386,7 @@
390
386
  display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
391
387
  flex: 0 0 auto;
392
388
  transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
389
+ --_row-h: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* the bar's own floor, read once (__head and __go below share it) */
393
390
  }
394
391
  /* the open step GROWS into the room left, so the steps not done yet wait in a pile at the bottom — and a move is an ACCORDION:
395
392
  in one beat the step that was open folds while the new one unfolds (flex-grow and the 1fr row both ease), the piles slide
@@ -397,26 +394,25 @@
397
394
  .sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
398
395
  .sheet-step + .sheet-step { margin-top: var(--sheet-steps-gap, var(--sp-1)); } /* the folded bars read as a pile, a little ground between them (Lh 2026-09-25) */
399
396
  .sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the white room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
400
- 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 */
397
+ display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
401
398
  padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
402
399
  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 */
400
+ 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
401
  }
405
- .sheet-step__summary { color: inherit; opacity: 0.8; }
406
402
  .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
403
  .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
404
  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
405
  border-radius: 50%; font: var(--sheet-step-num-font, var(--type-caption)); font-variant-numeric: tabular-nums;
410
406
  background: var(--sheet-step-num-bg, var(--card-bg)); color: var(--sheet-step-num-ink, inherit);
411
407
  }
412
- .sheet-step__summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
408
+ .sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
413
409
  .sheet-step__check { margin-inline-start: auto; }
414
410
  .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
415
411
  .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); }
416
412
  .sheet-step__inner > * { min-width: 0; }
417
413
  .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
414
  .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; }
415
+ .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
420
416
  /* the last step's finish, when the app gives one: under the steps */
421
417
  .sheet-steps__finish { display: grid; padding-top: var(--card-gap); }
422
418
  @media (prefers-reduced-motion: reduce) { .sheet-step { transition: none; } }
package/css/stack.css CHANGED
@@ -32,7 +32,7 @@
32
32
  bottom, the headroom above is the base's (block below).
33
33
  Direct children only, five at most (slots are :nth-child below):
34
34
  first = the back of the stack, last = the front. README "Sheet stack
35
- markup". cardds.js opens and closes a --tap stack; without it the
35
+ markup". The SheetStack component opens and closes a --tap stack; without it the
36
36
  stack still renders, it just won't open.
37
37
  ============================================================ */
38
38
 
@@ -45,14 +45,14 @@
45
45
  track is being sized), so a long sheet grew the track past the screen and
46
46
  every sheet's 100% then resolved against that — the bottom of every open
47
47
  sheet sat below the screen, unreachable (timebank home, 2026-09-13). */
48
- .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; }
49
-
50
48
  /* the stack breaks out of the screen inset, edge to edge, always: the peek
51
49
  strips and the opened sheet span the screen's full width whatever the
52
50
  viewport (it used to bleed only under a 27em media query — a stack in a
53
51
  phone frame on a desktop kept the inset, and read as a narrower card;
54
- Lh, 2026-09-11) */
55
- .sheet-stack { margin-inline: calc(-1 * var(--screen-pad)); }
52
+ Lh, 2026-09-11). --_n: how many sheets the stack holds, read from the DOM
53
+ by :has below so the closed geometry needs no script and no per-stack
54
+ declaration. */
55
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
56
56
  .sheet-stack > .card {
57
57
  grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
58
58
  border-bottom-left-radius: 0;
@@ -69,9 +69,6 @@
69
69
  .sheet-stack > .card:not(:has(~ .card)) {
70
70
  align-self: stretch; /* the front card fills the remaining stack height */
71
71
  }
72
- /* --_n: how many sheets the stack holds, read from the DOM by :has so the
73
- closed geometry needs no script and no per-stack declaration */
74
- .sheet-stack { --_n: 1; }
75
72
  .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
76
73
  .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
77
74
  .sheet-stack:has(> .card:nth-child(4)) { --_n: 4; }
@@ -118,7 +115,6 @@
118
115
  /* fanned stack: a toggle opens the stack wider so every peek shows more */
119
116
  .sheet-stack--fanned { --stack-step: 9.5rem; }
120
117
 
121
- .sheet-stack > .card:has(~ .card) { cursor: pointer; }
122
118
  /* a peeked card in a plain stack hides its own body. A --tap stack does
123
119
  NOT: there, every card is full height and full content always, and a
124
120
  peek is simply the strip of it the next card doesn't cover — the way a
@@ -164,9 +160,10 @@
164
160
  by n steps (clipped by .screen--fill), so translating it up by exactly
165
161
  that much lands it filling the stack precisely. */
166
162
  overflow: clip;
167
- transition: translate 0.35s ease; /* the only animated property here */
163
+ transition: translate var(--motion-sheet) ease; /* the only animated property here */
168
164
  transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
169
165
  except a staggered is-before card, set in JS as it tucks or un-tucks */
166
+ --_back-size: var(--stack-back-size, calc(var(--tap-base) * 2 / 3)); /* read once, shared by the back button and the head's padding below */
170
167
  }
171
168
  .sheet-stack--tap > .card.is-open {
172
169
  /* up by its own resting offset — pure movement, nothing resizes */
@@ -188,9 +185,8 @@
188
185
  overscroll-behavior: contain;
189
186
  }
190
187
  /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
191
- down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself */
192
- .sheet-stack--tap > .card { scrollbar-width: none; }
193
- .sheet-stack--tap > .card::-webkit-scrollbar { display: none; }
188
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
189
+ (the hiding rule is base.css's, shared with every other scrolling track) */
194
190
  .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
195
191
  .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
196
192
 
@@ -209,7 +205,7 @@
209
205
  lands flush at the top. is-before tucks one step below its resting place.
210
206
  ============================================================ */
211
207
  .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
212
- .sheet-stack--closed > .card { translate: 0 var(--_headroom); cursor: pointer; }
208
+ .sheet-stack--closed > .card { translate: 0 var(--_headroom); }
213
209
  .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
214
210
  .sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
215
211
  .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
@@ -241,10 +237,7 @@
241
237
  .sheet-stack--tap .card__head {
242
238
  position: relative;
243
239
  padding-left: 0;
244
- transition: padding-left 0.35s ease;
245
- }
246
- .sheet-stack--tap > .card.is-open > .card__head {
247
- padding-left: calc(var(--tap-sm) + var(--sp-3));
240
+ transition: padding-left var(--motion-sheet) ease;
248
241
  }
249
242
  .sheet-stack--tap .sheet-stack__back {
250
243
  position: absolute;
@@ -253,7 +246,13 @@
253
246
  translate: 0 -50%;
254
247
  opacity: 0;
255
248
  visibility: hidden;
256
- transition: opacity 0.35s ease, visibility 0.35s;
249
+ transition: opacity var(--motion-sheet) ease, visibility var(--motion-sheet);
250
+ /* SMALL (see "the back button, SMALL" below) */
251
+ width: var(--_back-size);
252
+ height: var(--_back-size);
253
+ min-width: 0;
254
+ min-height: 0;
255
+ padding: 0;
257
256
  }
258
257
  .sheet-stack--tap > .card.is-open .sheet-stack__back {
259
258
  opacity: 1;
@@ -312,20 +311,13 @@
312
311
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
313
312
  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
313
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
315
- .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
- }
314
+ .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
323
315
  .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
- .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); }
316
+ .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
325
317
 
326
318
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
327
319
  .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
328
320
  /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
329
321
  no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
330
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
322
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
331
323
  .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
package/css/step.css CHANGED
@@ -59,9 +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
- /* 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)
63
- is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
64
-
65
62
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
66
63
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
67
64
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
@@ -81,8 +78,7 @@
81
78
  --card-gap: var(--sp-4);
82
79
  gap: var(--card-gap);
83
80
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
84
- align-content: start;
85
- color: var(--on-surface);
81
+ color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
86
82
  }
87
83
  /* the reading block is the .page — or the .pages that holds several of them, a walk of words
88
84
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
@@ -90,23 +86,22 @@
90
86
  .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
91
87
  .step > :is(.page, .pages) {
92
88
  flex: 0 1 auto;
93
- }
94
- .step > :is(.page, .pages) {
95
89
  min-height: 0;
96
- 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
90
  overflow-y: auto;
98
91
  overscroll-behavior: contain;
99
92
  }
93
+ /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
94
+ the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
95
+ .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; }
100
96
 
101
97
  /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
102
98
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
103
99
  padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
104
100
  The walk shows no scrollbar. */
105
- .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; }
101
+ .step > .pages { 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, the default (Lh 2026-09-23) */
109
102
  .step > .pages > .page {
103
+ padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
104
+ 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
105
  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
106
  }
112
107
 
@@ -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 */
@@ -93,8 +93,26 @@
93
93
 
94
94
  /* ---- motion ---- */
95
95
  --motion-sheet: 0.35s;
96
+ --motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
96
97
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
97
98
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
99
+ --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 */
100
+ --rest: 0.6; /* an unpicked choice at rest: .day, .segment__item, .float-bar__item */
101
+ --faint: 0.3; /* further still: rails, far dots, unselected picker items */
102
+ --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
103
+ --press-scale: 0.96; /* how far a finger-sized control gives — a share of its size */
104
+ --press-dim: 0.6; /* how far a card-wide one (a row, a summary line, a card) fades under the finger */
105
+ --press-shade: 0.93; /* how far something with a thing behind it (a peeked sheet, a postcard) darkens under the finger */
106
+ --off: 0.4; /* a disabled control's fade */
107
+
108
+ /* ---- z-index: the few layers that stack ABOVE ordinary content, low to high ---- */
109
+ --z-bar: 30; /* .action-bar, .float-bar — above .sheet-stack sheets (--level 1-5) */
110
+ --z-fab: 31; /* .fab — above the bars it floats near */
111
+ --z-top: 40; /* .appbar, a stage laid OVER its screen (.sheet-stage--over) — topmost, card animations never cover it */
112
+
113
+ /* ---- shared private sizes (not a knob: two components share the exact same
114
+ literal by design and must not drift apart) ---- */
115
+ --_cover-min-h: 22.5rem; /* .card--cover's min-height; --postcard-min-h falls back to it (media.css) */
98
116
 
99
117
  /* ---- typography: one scale, rem so it follows the root ---- */
100
118
  --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif; /* fonts.css ships it; the rest is the fallback while it loads */
@@ -209,7 +227,7 @@
209
227
 
210
228
  /* ============================================================
211
229
  REGISTERED TOKENS — the two sheet lengths are typed, so that
212
- cardds.js can read them back RESOLVED: getComputedStyle returns a
230
+ the Sheet component can read them back RESOLVED: getComputedStyle returns a
213
231
  registered <length> in px and a <number> as a number, where an
214
232
  unregistered custom property only ever reads back as its text
215
233
  ("calc(1.25rem + …)"). Drag-to-snap needs to know where the states
@@ -221,7 +239,7 @@
221
239
  falls back to when its own declaration cannot resolve.
222
240
  ============================================================ */
223
241
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
224
- @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; }
225
- @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */ /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
242
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
243
+ @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
226
244
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
227
245
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
@@ -19,6 +19,7 @@ export interface BtnProps extends Omit<ComponentProps<'button'>, 'form'> {
19
19
  * In a CardFoot up to four share the line; in an ActionBar it takes the width. An Icon xs may lead or trail the label.
20
20
  */
21
21
  export declare function Btn({ primary, quiet, block, form, xl, href, className, type, ...rest }: BtnProps): import("react").DetailedReactHTMLElement<{
22
+ onChange?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
22
23
  name?: string | undefined | undefined;
23
24
  slot?: string | undefined | undefined;
24
25
  style?: import("react").CSSProperties | undefined;
@@ -104,7 +105,6 @@ export declare function Btn({ primary, quiet, block, form, xl, href, className,
104
105
  onFocusCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
105
106
  onBlur?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
106
107
  onBlurCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
107
- onChange?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
108
108
  onChangeCapture?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
109
109
  onBeforeInput?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
110
110
  onBeforeInputCapture?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
@@ -1,18 +1,12 @@
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
- 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)
13
- onChange?.(next); };
14
- return [current, pick];
15
- }
9
+ const useValue = (value, defaultValue, onChange) => useControlled(value, defaultValue, onChange);
16
10
  /**
17
11
  * Dropdown — THE select: a pill trigger + its Menu popover anchored under it (native popover: light dismiss, Esc). Never a native
18
12
  * `<select>`. ONE component, like `<select>` is — the pill and the panel are its own anatomy. It holds a VALUE, controlled like an
@@ -5,5 +5,5 @@ export interface FabProps extends ComponentProps<'button'> {
5
5
  icon?: IconName;
6
6
  invert?: boolean;
7
7
  }
8
- /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar). */
8
+ /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar): an IconBtn with the fab's place. */
9
9
  export declare function Fab({ label, icon, invert, className, children, ...rest }: FabProps): import("react").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  import { Icon } from '../type/Icon.js';
4
- /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar). */
4
+ import { IconBtn } from './IconBtn.js';
5
+ /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar): an IconBtn with the fab's place. */
5
6
  export function Fab({ label, icon = 'plus', invert = true, className, children, ...rest }) {
6
- return (_jsx("button", { type: "button", "aria-label": label, className: cx('icon-btn fab', invert && 'icon-btn--invert', className), ...rest, children: children ?? _jsx(Icon, { name: icon }) }));
7
+ return _jsx(IconBtn, { label: label, invert: invert, className: cx('fab', className), ...rest, children: children ?? _jsx(Icon, { name: icon }) });
7
8
  }