@usableapp/cardds 0.7.106 → 0.7.107

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 (95) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +16 -186
  7. package/css/finish.css +4 -4
  8. package/css/journey.css +1 -1
  9. package/css/layover.css +4 -14
  10. package/css/lists.css +3 -18
  11. package/css/media.css +1 -7
  12. package/css/parts.css +380 -0
  13. package/css/sheet.css +12 -139
  14. package/css/stack.css +15 -26
  15. package/css/stamp.css +1 -1
  16. package/css/step.css +3 -3
  17. package/css/theme.template.css +1 -1
  18. package/css/tokens.css +4 -4
  19. package/dist/actions/BarNote.d.ts +2 -2
  20. package/dist/actions/BarNote.js +2 -2
  21. package/dist/actions/Btn.d.ts +2 -2
  22. package/dist/actions/Btn.js +1 -1
  23. package/dist/actions/BtnRow.d.ts +1 -1
  24. package/dist/actions/BtnRow.js +1 -1
  25. package/dist/actions/IconBtn.d.ts +1 -1
  26. package/dist/actions/IconBtn.js +1 -1
  27. package/dist/cardds.css +461 -422
  28. package/dist/cards/Card.d.ts +6 -22
  29. package/dist/cards/Card.js +11 -22
  30. package/dist/cards/CardFeed.d.ts +1 -1
  31. package/dist/cards/CardFeed.js +5 -5
  32. package/dist/cards/CardParts.d.ts +0 -12
  33. package/dist/cards/CardParts.js +0 -12
  34. package/dist/cards/cardGo.js +1 -1
  35. package/dist/cards/cardGrounds.js +2 -2
  36. package/dist/catalog.json +1 -1
  37. package/dist/forms/Field.d.ts +1 -1
  38. package/dist/forms/Field.js +1 -1
  39. package/dist/index.d.ts +6 -5
  40. package/dist/index.js +6 -5
  41. package/dist/lists/Row.d.ts +2 -28
  42. package/dist/lists/Row.js +2 -17
  43. package/dist/media/MapArea.d.ts +2 -2
  44. package/dist/media/MapArea.js +1 -1
  45. package/dist/media/Tile.d.ts +3 -7
  46. package/dist/media/Tile.js +5 -5
  47. package/dist/parts/Body.d.ts +7 -0
  48. package/dist/parts/Body.js +11 -0
  49. package/dist/parts/End.d.ts +10 -0
  50. package/dist/parts/End.js +7 -0
  51. package/dist/parts/Foot.d.ts +17 -0
  52. package/dist/parts/Foot.js +11 -0
  53. package/dist/parts/Head.d.ts +15 -0
  54. package/dist/parts/Head.js +34 -0
  55. package/dist/parts/Lead.d.ts +10 -0
  56. package/dist/parts/Lead.js +9 -0
  57. package/dist/parts/Words.d.ts +13 -0
  58. package/dist/parts/Words.js +20 -0
  59. package/dist/parts/headCtx.d.ts +7 -0
  60. package/dist/parts/headCtx.js +9 -0
  61. package/dist/scaffold/EmptyState.d.ts +1 -1
  62. package/dist/scaffold/EmptyState.js +1 -1
  63. package/dist/scaffold/Screen.js +1 -1
  64. package/dist/sheets/Modal.d.ts +3 -7
  65. package/dist/sheets/Modal.js +3 -7
  66. package/dist/sheets/Sheet.d.ts +2 -2
  67. package/dist/sheets/Sheet.js +8 -9
  68. package/dist/sheets/SheetStack.d.ts +3 -3
  69. package/dist/sheets/SheetStack.js +4 -4
  70. package/dist/sheets/SheetSteps.d.ts +1 -1
  71. package/dist/sheets/SheetSteps.js +2 -2
  72. package/dist/sheets/StackBack.d.ts +1 -1
  73. package/dist/sheets/StackBack.js +1 -1
  74. package/dist/sheets/StackSheet.js +7 -7
  75. package/dist/sheets/sheetMeasure.js +3 -3
  76. package/dist/sheets/sheetStepsPile.js +2 -2
  77. package/dist/sheets/stackCard.js +6 -6
  78. package/dist/templates/CallSheet.js +6 -5
  79. package/dist/templates/Walkthrough.js +3 -2
  80. package/docs/reference.md +75 -36
  81. package/lib/catalog.mjs +1 -1
  82. package/lib/mcp.mjs +1 -1
  83. package/lint/rules.mjs +14 -7
  84. package/package.json +2 -1
  85. package/scripts/codemod-parts.mjs +324 -0
  86. package/dist/cards/CardFoot.d.ts +0 -15
  87. package/dist/cards/CardFoot.js +0 -9
  88. package/dist/cards/CardHead.d.ts +0 -21
  89. package/dist/cards/CardHead.js +0 -39
  90. package/dist/cards/Stat.d.ts +0 -19
  91. package/dist/cards/Stat.js +0 -14
  92. package/dist/sheets/SheetBody.d.ts +0 -5
  93. package/dist/sheets/SheetBody.js +0 -9
  94. package/dist/sheets/SheetHead.d.ts +0 -15
  95. package/dist/sheets/SheetHead.js +0 -6
package/css/sheet.css CHANGED
@@ -34,8 +34,8 @@
34
34
  the listed states, and the state it lands on becomes the one a tap
35
35
  returns to.
36
36
  <button class="sheet__handle" type="button" aria-label="…"></button>
37
- <div class="card__head"><h2 class="t-h2">…</h2>…</div>
38
- <div class="sheet__body">…controls, or small content cards…</div>
37
+ <div class="head"><h2 class="words__title t-h2">…</h2>…</div>
38
+ <div class="body">…controls, or small content cards…</div>
39
39
  </article>
40
40
  Written top-down: the handle, the title row (what peek shows), then
41
41
  the body. The handle and the head stay where they are at every
@@ -107,38 +107,9 @@
107
107
  .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) var(--ease), visibility 0s var(--ease-linear) var(--motion-sheet); }
108
108
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
109
109
 
110
- /* the body — everything under the head, the one thing in a sheet that
111
- scrolls. Its scrollport runs to the card's side and bottom edges (reading
112
- --card-pad, the way .bleed does) so content scrolls clear of the edge
113
- rather than stopping inside the padding, and a .bleed strip inside it
114
- still reaches the edges. The card's own grid rhythm continues inside. */
115
- .sheet__body {
116
- flex: 1;
117
- min-height: 0;
118
- display: grid;
119
- gap: var(--card-gap);
120
- align-content: start;
121
- margin-inline: calc(-1 * var(--card-pad));
122
- padding-inline: var(--card-pad);
123
- overflow-y: auto;
124
- overscroll-behavior: contain;
125
- /* the part of the sheet that slid under the box's bottom edge cannot be
126
- seen, and the scroller has to know that: scroll-padding is exactly "the
127
- obscured part of my scrollport", so a focused field, scrollIntoView and
128
- anchor navigation all land a control in the visible part, never under
129
- the cut. A length from the box's container units (--_box), since the
130
- cut is a share of the SHEET's height, not of the body's. */
131
- scroll-padding-bottom: var(--_cover);
132
- /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
133
- scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
134
- foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
135
- when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
136
- the part that shows. */
137
- padding-bottom: var(--_cover);
138
- }
139
110
  /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
140
111
  A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
141
- page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
112
+ page's conclusion — is a .foot, the sheet's last child, on the sheet's own material. The sheet is
142
113
  full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
143
114
  foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
144
115
  slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
@@ -146,18 +117,11 @@
146
117
  length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
147
118
  takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
148
119
  stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
149
- One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
120
+ One row, ≤ 2 buttons (parts.css). <div class="foot"><button class="btn btn--primary btn--block">…</button></div> */
150
121
  /* …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(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
152
- .sheet > .card__foot > * { pointer-events: auto; }
153
- .sheet:has(> .card__foot) > .sheet__body { margin-bottom: calc(var(--_foot) - var(--card-pad)); } /* the body stops above the strip (the card's own padding is the rest) */
154
- /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
155
- box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
156
- the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
157
- .sheet > :not(.sheet__body) { flex-shrink: 0; }
158
- :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
159
- .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
160
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
122
+ .sheet:has(> .foot--link) { --_foot: calc(var(--card-gap) + var(--sheet-foot-h, calc(var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)))) + var(--card-pad) + var(--_safe-b-open)); } /* --sheet-foot-h: the foot's rows as the Sheet measured them (a button grows with the text size) */
123
+ /* the body's padding can be taller than its own box, so the pinned parts must not give way to it (parts.css) */
124
+ .sheet > :not(.body) { flex-shrink: 0; }
161
125
 
162
126
  /* grab handle — first child of the sheet, but it does not take a row: it sits in the
163
127
  card's top padding, the same air as a stack sheet's four dots (stack.css --_grip-y),
@@ -183,22 +147,6 @@
183
147
  touch-action: none;
184
148
  cursor: grab;
185
149
  }
186
- /* paper: the head keeps the sheet's own colour, the content is the theme's paper, --action-bg, the ground of every field (Lh 2026-10-05: white; Lh 2026-10-20: the theme's, never a fixed white).
187
- The body and the foot read as paper; the handle and the head stay on the card.
188
- The flex gap under the head is outside the body, so it stays the sheet's tint.
189
- The first field would sit on that colour seam. The paper's inset is --sp-6 (24px at
190
- the default spacing), the body's own top padding, and only when the sheet is paper.
191
- A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
192
- inset: the stack's own gap under the head is the card's colour, so the row still needs this paper.
193
- The paper takes the side of the controls' paper (--action-dark): on a dark sheet its words still turn to onlight. */
194
- .sheet--paper > .sheet__body {
195
- --card-bg: var(--action-bg);
196
- --dark: var(--action-dark);
197
- background: var(--card-bg);
198
- color: var(--card-ink);
199
- padding-top: var(--sp-6);
200
- }
201
- .sheet--paper > .card__foot { --card-bg: var(--action-bg); --dark: var(--action-dark); --card-foot-ink: var(--card-ink); }
202
150
  .sheet__handle--dots {
203
151
  --_dot: var(--sp-1);
204
152
  --_step: calc(var(--_dot) * 2.5);
@@ -238,42 +186,6 @@
238
186
  .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
239
187
  .sheet.is-dragging .sheet__handle { cursor: grabbing; }
240
188
 
241
- /* the head — a title row at least --tap tall, either kind, so the peek strip
242
- (--sheet-peek) is the same for every sheet:
243
- - in a step it is a plain card head (card.css): the heading left, the
244
- contextual thing at the far right
245
- - .sheet__head is the stage sheet's three-column head: a round button,
246
- the centred title and sub, a round button
247
- - a label .row with its .check / .toggle may be the head when the sheet's
248
- whole ask is that switch: peek shows it (a row is --tap tall plus its own
249
- padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
250
- .sheet > .card__head { --card-head-min-h: var(--sheet-head-h); }
251
- /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
252
- thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
253
- like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
254
- measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
255
- from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
256
- Before the Sheet has measured, and where motion is off, it is cut with …. */
257
- .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { --card-head-wrap: nowrap; }
258
- .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
259
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
260
- text-overflow: clip;
261
- animation: sheet-title-run var(--sheet-title-run-time, var(--_run, var(--motion-run))) var(--ease-in-out) infinite; /* --_run: from the overflow, 30px a second + the pauses */
262
- }
263
- @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
264
- @media (prefers-reduced-motion: reduce) {
265
- .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
266
- }
267
- .sheet__head {
268
- display: grid;
269
- grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
270
- align-items: center;
271
- gap: var(--sp-3);
272
- min-height: var(--tap);
273
- text-align: center;
274
- }
275
- .sheet__title { font: var(--sheet-title-font, var(--type-h2)); letter-spacing: var(--track-h2); margin: 0; }
276
- .sheet__sub { font: var(--sheet-sub-font, var(--type-caption)); color: var(--sheet-sub-ink, var(--card-muted)); }
277
189
 
278
190
  /* ============================================================
279
191
  .sheet-stage — the overlay stage: a page-height box the sheet
@@ -420,11 +332,11 @@
420
332
  }
421
333
 
422
334
  /* ---- the map IN FLOW (Lh 2026-10-17): `.map--inline` — a box in a card, or the whole of a sheet's body ----
423
- <div class="sheet__body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
335
+ <div class="body"><div class="map map--inline"><div>…the engine…</div><label class="map-pick" …>…</label>…
424
336
  <span class="map__ring" …></span></div></div>
425
337
  Choosing where to meet without leaving the sheet: the open sheet becomes the map and its head stays on top (a stage
426
338
  behind a half sheet covers the head and changes the look). No stage, so no band and no bar: the ring is at the centre
427
- of the map's OWN box and the hint at its own top. In a card it is a rounded media box; as a SheetBody's only child it
339
+ of the map's OWN box and the hint at its own top. In a card it is a rounded media box; as a sheet's Body's only child it
428
340
  fills the body (the part above the foot), however far the sheet slid. */
429
341
  .map--inline {
430
342
  --_top: 0rem;
@@ -435,8 +347,8 @@
435
347
  overflow: hidden;
436
348
  }
437
349
  .map--bleed { margin-inline: calc(-1 * var(--card-pad, 0rem)); border-radius: 0; } /* edge to edge is a band, and a band has no corner; --map-radius is for the box in the inset */
438
- .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
439
- .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
350
+ /* a map that is a body's only child fills it; the body turns to a column for it (parts.css) */
351
+ .body > .map--inline:only-child { flex: 1 0 auto; }
440
352
 
441
353
  /* ---- map pick: a place on the map the member taps to choose it (Lh 2026-10-17) ----
442
354
  <label class="map-pick" style="top: 30%; left: 40%"><input type="radio" name="meet" value="library">
@@ -525,7 +437,6 @@
525
437
  the form's own spacing, from the top (never spread or centred: the keyboard needs the room under it); the next steps wait in a pile at the
526
438
  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
527
439
  falls — no height is written. Nothing fades: done and next read at full ink. */
528
- .sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
529
440
  .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; }
530
441
  .sheet-steps__holder { position: absolute; top: 0; left: 0; width: var(--browser-text); height: var(--browser-text); padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: var(--browser-text); } /* 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 */
531
442
  .sheet-steps__track { list-style: none; margin: 0; padding: 0 0 var(--sheet-steps-end, var(--sp-6)); flex: 1; /* room under the LAST step: scrolled to the end it never touches the foot's button (Lh 2026-09-28, #153) */ display: flex; flex-direction: column; }
@@ -561,7 +472,7 @@
561
472
  .sheet-step__check { margin-inline-start: auto; }
562
473
  /* a step's child that bleeds (ChipRow bleed, MapArea bleed, a Banner) reaches the SHEET's edges (Lh 2026-10-19, the ask sheet's
563
474
  จุดนัด step: the map edge to edge under the step's buttons). The fold's clip is the step body's box, which sat inside the sheet's
564
- inset, so the body takes that inset back and pads by it again, the way .sheet__body does; the inner keeps its own box and its own
475
+ inset, so the body takes that inset back and pads by it again, the way a sheet's .body does; the inner keeps its own box and its own
565
476
  inset (--sheet-step-inner-pad-x: the inset BEYOND the sheet's) and publishes the whole, the sheet's plus its own, as --card-pad,
566
477
  so a .bleed cancels it exactly. The sum is computed on the body, before the inner's --card-pad takes over; outside a card there is
567
478
  no --card-pad and the step keeps its own inset (0px, as .bleed reads it). The inner reads the knob itself, not the sum: a theme's
@@ -578,45 +489,7 @@
578
489
  /* ==== the step bar answers a press, as PRESS in actions.css answers every other control (#62: a block is written in its own file) ==== */
579
490
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
580
491
 
581
- /* ==== a body that holds only an EmptyState stands it at --empty-state-at of its room, as a Step does (step.css) (#62: a block is written in its own file) ==== */
582
- .sheet__body:has(> .empty-state:only-child) { display: flex; flex-direction: column; gap: 0; }
583
- .sheet__body:has(> .empty-state:only-child)::before { content: ""; flex: calc(var(--empty-state-at, 0) * 5) 1 0; }
584
- .sheet__body:has(> .empty-state:only-child)::after { content: ""; flex: calc(5 - var(--empty-state-at, 0) * 5) 1 0; }
585
492
 
586
- /* ---- where it sits in what stack.css draws (docs/reference.md rule 15) ---- */
587
- /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
588
- A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
589
- card's colour, so without the paper's inset the first row sits on that seam. */
590
- .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0rem); transition: padding-top var(--motion-sheet) var(--ease); }
591
- /* the head's band is as slim as it can be (Lh 2026-10-06: 20 over · 48 head · 20 under read as a heavy bar): the paper starts
592
- --sp-3 under the head, not a whole --card-gap (--sp-2 at first; Lh added 4 the same day). The body is pulled up by the difference — the card's gap is left alone, the
593
- stack's step and the body's own rows read it. */
594
- .sheet-stack > .card.sheet--paper > .sheet__body {
595
- margin-top: calc(var(--sp-3) - var(--card-gap));
596
- padding-top: calc(var(--sp-6) + var(--_refresh, 0rem));
597
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
598
- }
599
- /* a pull follows the finger at once, on paper too: the rule above is as strong as the stack's own `is-refreshing` one and comes later */
600
- .sheet-stack > .card.sheet--paper > .sheet__body.is-refreshing { transition: none; }
601
- /* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
602
- cover the corners, and a paper body showed there beside the head. Clear, the card's own colour
603
- shows in that pocket. Open, the body is paper, and the first row sits --sp-6 down, the same paper
604
- inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
605
- would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
606
- into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
607
- sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
608
- at the moment the sheet stops. Rising, the body is paper at once. */
609
- .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
610
- background-color: transparent;
611
- transition:
612
- padding-top var(--motion-sheet) var(--ease),
613
- background-color var(--motion-quick) var(--ease) calc(var(--stagger, 0) * var(--motion-stagger) + var(--motion-sheet) - var(--motion-quick));
614
- }
615
- @media (prefers-reduced-motion: reduce) { .sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
616
- transition: padding-top var(--motion-sheet) var(--ease), background-color 0s var(--ease-linear);
617
- } }
618
493
  /* ---- where it sits in what step.css draws (docs/reference.md rule 15) ---- */
619
494
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
620
495
 
621
- /* ---- where it sits in what stack.css draws: a body pulled to refresh holds still (docs/reference.md rule 15) ---- */
622
- .sheet-stack > .card > .sheet__body.is-refreshing { transition: none; }
package/css/stack.css CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
20
20
  <article class="card card--2">
21
- <div class="card__head">
21
+ <div class="head">
22
22
  <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
23
23
  <h2 class="t-h2 t-caps">Shopping</h2>
24
24
  <span class="icon-row">…</span> (optional trailing icons)
@@ -73,7 +73,7 @@
73
73
 
74
74
  /* NOT a skeleton head: a peek is one line at its own size, and the display
75
75
  stack deliberately runs a second line under the next card. */
76
- .sheet-stack > .card > .card__head :is(h1, h2, h3) {
76
+ .sheet-stack > .card > .head :is(h1, h2, h3) {
77
77
  font-size: var(--fs-h1);
78
78
  line-height: var(--lh-solid);
79
79
  letter-spacing: var(--track-h2);
@@ -85,7 +85,7 @@
85
85
  /* display stack: the giant title IS the peek — line one shows, line two is
86
86
  clipped by the next card (recents / library lists) */
87
87
  .sheet-stack--display { --stack-step: calc(var(--tap-base) * 13 / 6); }
88
- .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
88
+ .sheet-stack--display > .card > .head :is(h1, h2, h3) {
89
89
  font: var(--type-display);
90
90
  letter-spacing: var(--track-display);
91
91
  /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
@@ -99,7 +99,7 @@
99
99
  NOT: there, every card is full height and full content always, and a
100
100
  peek is simply the strip of it the next card doesn't cover — the way a
101
101
  card in a real deck is never a shorter card, just a mostly hidden one. */
102
- .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
102
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.head) { display: none; }
103
103
 
104
104
  /* ---- .sheet-stack--tap — tap a peeked sheet open; a back button returns it. How its sheets move (one property: translate) is
105
105
  told over their rules, where they sit, in card.css. ---- */
@@ -247,7 +247,7 @@
247
247
  slide read as a second thing moving while the sheet itself moved; Lh,
248
248
  2026-09-11). Only the sheet travels; the button appears where it is. */
249
249
  /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
250
- :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
250
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .head > .sheet-stack__back {
251
251
  position: absolute;
252
252
  left: 0;
253
253
  top: 50%;
@@ -262,35 +262,24 @@
262
262
  min-height: 0;
263
263
  padding: 0;
264
264
  }
265
- .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
265
+ .sheet-stack--tap > .card.is-open > .head > .sheet-stack__back {
266
266
  opacity: 1;
267
267
  visibility: visible;
268
268
  }
269
269
 
270
- .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
271
- .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
272
- /* a paper stack's foot is paper (card.css paints it): its ink reads the action's side */
273
- .sheet-stack > .card.sheet--paper > .card__foot { --dark: var(--action-dark); }
270
+ .sheet-stack > .card:has(> .body) > :not(.body) { flex-shrink: 0; }
274
271
 
275
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
276
273
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
277
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) */
278
- /* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
279
- (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 */
280
- .stack-sheet__title {
281
- flex: 1; min-width: 0; overflow-x: clip; overflow-y: visible; white-space: nowrap; overflow-wrap: normal; /* clip SIDEWAYS only:
282
- hidden clipped the Thai tone marks above a line-height-1 line (เพื่อน lost its ไม้เอก — Lh 2026-09-24) */
283
- translate: 0 var(--stack-sheet-title-shift, -0.1em); /* FC Pride's line box carries more room above its letters than below: the
284
- letters sat ~0.1em under the middle of the back disc and the tabs — lifted onto it (Lh 2026-09-24) */
285
- }
275
+ /* the title's run (the title's own rules are parts.css's, in a stack sheet) */
286
276
  .stack-sheet__run { display: inline-block; }
287
- .stack-sheet__title.is-long .stack-sheet__run {
277
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run {
288
278
  animation: stack-sheet-run var(--stack-sheet-run-time, var(--_run, var(--motion-run))) var(--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 */
289
279
  }
290
280
  @keyframes stack-sheet-run { 0%, 20% { translate: 0; } 85%, 100% { translate: calc(-1 * var(--_over)); } }
291
281
  @media (prefers-reduced-motion: reduce) {
292
- .stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
293
- .stack-sheet__title.is-long { text-overflow: ellipsis; }
282
+ .stack-sheet > .head > .words__title.is-long > .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
294
283
  .stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
295
284
  }
296
285
  .stack-sheet__tab {
@@ -324,23 +313,23 @@
324
313
  /* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
325
314
  alone. The fill fades in with the rise — the tab that was pressed, or the one the sheet opens on — and fades out as the
326
315
  sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
327
- .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
316
+ .sheet-stack--tap > .card:not(.is-open) > .head .stack-sheet__tab.icon-btn--invert {
328
317
  border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
329
318
  }
330
319
  /* the last tab's ICON lines up with the inset, not its button's edge: an unchosen tab shows the glyph only, so the button's own
331
320
  margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
332
- .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
321
+ .head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
333
322
 
334
323
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
335
324
  while open; the last tab then sits on the inset (no reach into the padding) */
336
325
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
337
- .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
326
+ .sheet-stack--tap > .card.is-open > .head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
338
327
  .sheet-stack--tap .sheet-stack__back > .icon { --icon-size: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
339
328
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
340
329
 
341
330
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
342
- .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
343
- .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) */
331
+ .sheet-stack--tap > .card.is-open > .head > :is(h1, h2, h3) { cursor: pointer; }
332
+ .sheet-stack--tap > .card.is-open > :not(.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) */
344
333
 
345
334
  /* ---- where it sits in what base.css draws (docs/reference.md rule 15) ---- */
346
335
  /* min-height: 0 — a flex item's floor is its content, and the stack's content
package/css/stamp.css CHANGED
@@ -277,7 +277,7 @@
277
277
  }
278
278
 
279
279
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
280
- .card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
280
+ .head > .stamp { grid-column: 2; grid-row: 1 / span 2; margin-inline: 0; }
281
281
  /* ---- where it sits in what people.css draws (docs/reference.md rule 15) ---- */
282
282
  /* a stamp in the stack is the stamp's small size (--stamp-sm-size: one rule in stamp.css, 48 × 60), lapped like the heads. Measured 2026-10-11 in a Row's
283
283
  lead with a title, a meta line and an end button: at 375 the meta line breaks in two, accepted by Lh (a lap of 16 would hold it; the lap stays 8)
package/css/step.css CHANGED
@@ -3,12 +3,12 @@
3
3
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
4
4
  step asks for it (a sheet, sheet.css). The move: a page of words has
5
5
  an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
6
- a step with a sheet has none: the sheet's own .card__foot finishes it
6
+ a step with a sheet has none: the sheet's own .foot finishes it
7
7
  (see "LAYERS" below).
8
8
 
9
9
  <section class="step">
10
10
  <section class="page">…the words…</section> ← first
11
- <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
11
+ <article class="card sheet sheet--half">…the controls… <div class="foot">…</div></article> ← last, optional
12
12
  </section>
13
13
  <div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
14
14
 
@@ -60,7 +60,7 @@
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
62
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
63
- move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
63
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .foot
64
64
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
65
65
  own screens are composed (the guide); the framework does not police it — a component never changes
66
66
  because another one is there. */
@@ -49,7 +49,7 @@
49
49
  --ink-onlight --ink-onlight-muted --ink-ondark --ink-ondark-muted
50
50
  ---- sides: each ground says only whether it is dark (1) or light (0); its words and glyphs follow ----
51
51
  --surface-dark (0) --card-1-dark (0) --card-2-dark (0) --card-3-dark (1) --card-4-dark (0) --card-5-dark (1)
52
- --accent-dark (1, the brand fill) --action-dark (0, the paper of buttons, chips, fields) --neg-dark (1, a falling Delta)
52
+ --accent-dark (1, the brand fill) --action-dark (0, the paper of buttons, chips, fields) --neg-dark (1, a falling number)
53
53
  A dark mode is these sides turned to 1 with their fills darkened, in a [data-mode="dark"] block of your own: no rule per piece.
54
54
  A surface of your own: give it a fill and `--dark: 1` (or 0), and its --card-ink / --card-muted that side's set. */
55
55
 
package/css/tokens.css CHANGED
@@ -248,7 +248,7 @@
248
248
  --card-1-dark: 0; --card-2-dark: 0; --card-3-dark: 1; --card-4-dark: 0; --card-5-dark: 1;
249
249
  --accent-dark: 1; /* the brand fill: a primary button, a picked chip, a picked day */
250
250
  --action-dark: 0; /* the paper fill of neutral controls: a button, a chip, a field, a plain tile */
251
- --neg-dark: 1; /* a falling Delta's fill */
251
+ --neg-dark: 1; /* the fill of a falling number (--neg) */
252
252
 
253
253
  /* app background ("earth-toned surface or shiny color, up to the app") */
254
254
  --surface: var(--color-surface, #f2f2f0);
@@ -354,11 +354,11 @@
354
354
  (card.css, media.css…) and is named in this list, which gives it the inks every piece on it reads: --card-ink (words, icons,
355
355
  marks, dots: currentColor), --card-muted (secondary words), the four signals, --on-accent (the ink on the accent fill, whose
356
356
  side is --accent-dark) and --accent-ink (the brand as a glyph). A piece with no fill of its own reads only these, so a dark
357
- surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (a stat
357
+ surface turns everything on it to ondark with no rule per piece. A piece with no fill that stands in for a surface (an upload
358
358
  tile) takes its ground's side: `--dark: inherit`. inkGrounds() measures it; docs/reference.md "Ink follows its ground" */
359
359
  :where(body, [data-palette], [data-mode], .card, .tile, .modal__drawer, .postcard__front, .postcard__back, .sticky, .post-paper,
360
- .float-bar__pill, .snackbar__body, .delta, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.sheet__body, .card__foot),
361
- .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.card__head, .card__foot), .card--bands > .card__face > :is(.card__head, .card__foot)) {
360
+ .float-bar__pill, .snackbar__body, .track, .tile-badge, .avatar, .bubble__body, .map__hint, .map__pin, .sheet--paper > :is(.body, .foot),
361
+ .postcard__band, .track__step, .toggle, .chip, .card__face, .card--bands > :is(.head, .foot), .card--bands > .card__face > :is(.head, .foot)) {
362
362
  --card-ink: color-mix(in srgb, var(--ink-ondark) calc(var(--dark) * 100%), var(--ink-onlight));
363
363
  --card-muted: color-mix(in srgb, var(--ink-ondark-muted) calc(var(--dark) * 100%), var(--ink-onlight-muted));
364
364
  --on-accent: color-mix(in srgb, var(--ink-ondark) calc(var(--accent-dark) * 100%), var(--ink-onlight));
@@ -5,7 +5,7 @@ export interface BarNoteProps extends ComponentProps<'div'> {
5
5
  * BarNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
6
6
  * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
7
7
  * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
8
- * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
9
- * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
8
+ * A member of the BOTTOM: a `Foot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
9
+ * it (Foot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
10
10
  */
11
11
  export declare function BarNote({ className, children, ...rest }: BarNoteProps): import("react").JSX.Element;
@@ -4,8 +4,8 @@ import { cx } from '../cx.js';
4
4
  * BarNote — the words that stand WHERE THE MAIN BUTTON WOULD BE when there is nothing to press: a block exactly the main button's
5
5
  * size (full width, `--btn-h` tall), lying on the ground — no fill, no border, no rounding — holding one line of muted words, an
6
6
  * inline `Icon` allowed ("กดที่ <Icon name="share" /> ด้านล่างนี้" — the member does the thing elsewhere). It does nothing on a tap.
7
- * A member of the BOTTOM: a `CardFoot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
8
- * it (CardFoot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
7
+ * A member of the BOTTOM: a `Foot`'s row or an `ActionBar`'s, in place of the button, never beside one; a `link` still goes under
8
+ * it (Foot `link`, ActionBar `ctaLink`). One fixed shape — never styled per use (Lh 2026-09-24).
9
9
  */
10
10
  export function BarNote({ className, children, ...rest }) {
11
11
  return _jsx("div", { role: "note", className: cx('action-note', className), ...rest, children: _jsx("span", { className: "action-note__words", children: children }) });
@@ -7,7 +7,7 @@ export interface BtnProps extends Omit<ComponentProps<'button'>, 'form'> {
7
7
  /** full width */
8
8
  block?: boolean;
9
9
  /** a form's button: sized to its words with wide side padding, centred on its own row — it reads as part of the form above it,
10
- * not a page CTA (Lh 2026-09-25). In a card's foot (`CardFoot`, `Card foot`, `FeedCard foot`) it keeps that size instead of
10
+ * not a page CTA (Lh 2026-09-25). In a card's foot (`Foot`, `FeedCard foot`) it keeps that size instead of
11
11
  * filling the row. Replaces the native `form` attribute (a button outside its <form> — not a cardds pattern). */
12
12
  form?: boolean;
13
13
  /** the one giant call to action in a tinted card ("Taxi!") */
@@ -17,7 +17,7 @@ export interface BtnProps extends Omit<ComponentProps<'button'>, 'form'> {
17
17
  }
18
18
  /**
19
19
  * Btn — the pill button (48px+ tall). Plain by default; `primary` for the one move; `quiet` on tinted cards.
20
- * 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
+ * In a Foot up to four share the line; in an ActionBar it takes the width. An Icon xs may lead or trail the label.
21
21
  */
22
22
  export declare function Btn({ primary, quiet, block, form, xl, href, className, type, ...rest }: BtnProps): import("react").DetailedReactHTMLElement<{
23
23
  translate?: "yes" | "no" | undefined | undefined;
@@ -2,7 +2,7 @@ import { createElement } from 'react';
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
4
  * Btn — the pill button (48px+ tall). Plain by default; `primary` for the one move; `quiet` on tinted cards.
5
- * 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.
5
+ * In a Foot up to four share the line; in an ActionBar it takes the width. An Icon xs may lead or trail the label.
6
6
  */
7
7
  export function Btn({ primary, quiet, block, form, xl, href, className, type, ...rest }) {
8
8
  return createElement(href ? 'a' : 'button', {
@@ -1,5 +1,5 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  export interface BtnRowProps extends ComponentProps<'div'> {
3
3
  }
4
- /** BtnRow — twin CTAs inside a card's content sharing the width (Log time · Offer). In a foot use CardFoot instead. */
4
+ /** BtnRow — twin CTAs inside a card's content sharing the width (Log time · Offer). In a foot use Foot instead. */
5
5
  export declare function BtnRow({ className, ...rest }: BtnRowProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** BtnRow — twin CTAs inside a card's content sharing the width (Log time · Offer). In a foot use CardFoot instead. */
3
+ /** BtnRow — twin CTAs inside a card's content sharing the width (Log time · Offer). In a foot use Foot instead. */
4
4
  export function BtnRow({ className, ...rest }) {
5
5
  return _jsx("div", { className: cx('btn-row', className), ...rest });
6
6
  }
@@ -20,7 +20,7 @@ export interface IconBtnProps extends ComponentProps<'button'> {
20
20
  href?: string;
21
21
  }
22
22
  /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move, `primary` for the
23
- * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<CardFoot round>`); a short WORD may stand in one
23
+ * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<Foot round>`); a short WORD may stand in one
24
24
  * as its child (`<IconBtn size="xl" primary label="ช่วยเหลือ">ช่วย</IconBtn>`): it grows with the member's text size up to the circle's share.
25
25
  * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
26
26
  * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */
@@ -4,7 +4,7 @@ import { cx } from '../cx.js';
4
4
  import { warnOnce } from '../state.js';
5
5
  import { Icon } from '../type/Icon.js';
6
6
  /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move, `primary` for the
7
- * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<CardFoot round>`); a short WORD may stand in one
7
+ * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<Foot round>`); a short WORD may stand in one
8
8
  * as its child (`<IconBtn size="xl" primary label="ช่วยเหลือ">ช่วย</IconBtn>`): it grows with the member's text size up to the circle's share.
9
9
  * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
10
10
  * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */