@usableapp/cardds 0.6.3 → 0.6.5

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 (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +69 -11
  3. package/css/actions.css +15 -0
  4. package/css/base.css +3 -0
  5. package/css/choice.css +11 -0
  6. package/css/layover.css +3 -0
  7. package/css/media.css +6 -0
  8. package/css/sheet.css +7 -0
  9. package/css/stack.css +55 -0
  10. package/css/step.css +44 -2
  11. package/dist/actions/Btn.d.ts +1 -1
  12. package/dist/actions/IconBtn.d.ts +1 -1
  13. package/dist/cardds.css +144 -2
  14. package/dist/choice/Check.d.ts +6 -3
  15. package/dist/choice/Check.js +7 -4
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +3 -0
  18. package/dist/scaffold/ActionBar.d.ts +3 -2
  19. package/dist/scaffold/ActionBar.js +3 -2
  20. package/dist/scaffold/AppBar.d.ts +4 -1
  21. package/dist/scaffold/AppBar.js +4 -3
  22. package/dist/scaffold/Screen.d.ts +13 -8
  23. package/dist/scaffold/Screen.js +24 -7
  24. package/dist/sheets/SheetStack.d.ts +1 -1
  25. package/dist/sheets/SheetStack.js +6 -1
  26. package/dist/sheets/SheetStage.d.ts +4 -1
  27. package/dist/sheets/SheetStage.js +2 -2
  28. package/dist/sheets/StackBack.d.ts +2 -1
  29. package/dist/sheets/StackBack.js +3 -2
  30. package/dist/sheets/StackSheet.d.ts +35 -0
  31. package/dist/sheets/StackSheet.js +24 -0
  32. package/dist/sheets/Step.d.ts +2 -2
  33. package/dist/sheets/Step.js +2 -2
  34. package/dist/templates/Home.d.ts +48 -0
  35. package/dist/templates/Home.js +40 -0
  36. package/dist/templates/Walkthrough.d.ts +42 -0
  37. package/dist/templates/Walkthrough.js +34 -0
  38. package/dist/type/Text.d.ts +3 -1
  39. package/dist/type/Text.js +2 -2
  40. package/dist/type/icons.d.ts +1 -1
  41. package/dist/type/icons.js +2 -2
  42. package/icons.svg +2 -0
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
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.6.5 — 2026-09-23
6
+
7
+ ### A screen is top · content · bottom, one flex column (Lh 2026-09-23)
8
+ - **`Screen top` / `bottom`** (new): `top` = `TopBar` / `AppBar` / `"lead"` (no bar, the words 15% down) · children = content · `bottom` = `ActionBar` / `FloatBar`. The browser measures the bar; the content takes the rest.
9
+ - **Before → after:** in a `Screen fill` the `ActionBar` and the `AppBar` are **rows of the screen's column**, not `position: fixed` over it. Put the bar INSIDE the Screen (as `bottom` or its last child). A bar after `</Screen>`, or one given `position` by hand, is now wrong (`validateScreen` in scripts/validate.mjs fails it). A bar outside a fill screen (an app shell) still works the old fixed way.
10
+ - The walk's dots (`PagerAt`) float over the foot of the pages; the pages run down to the bar.
11
+ - `Screen` warns once when a screen has both a `Sheet` and a bar.
12
+
13
+ ### Templates (new)
14
+ - **`Walkthrough`** — top `lead` · content `Pages` (media · heading · body) · bottom `ActionBar` (`controls` rows · the `cta` row — the pager by default, or a function of `{ at, count, go }` · `ctaLink`); children = layers over it.
15
+ - **`Home`** — top `AppBar` · content: `base` over a closed `SheetStack` · no bottom. `title` = the page's h1, read aloud, not shown. Only the open sheet's body is rendered.
16
+
17
+ ### New and changed
18
+ - **`StackSheet`** (new): one sheet of a SheetStack — head (back · title · icon tabs with a count) · body · foot. A tap on a tab of a closed sheet opens it on that tab; a tap on the open sheet's title folds it.
19
+ - **`StackBack`**: a small 32px disc with a chevron (was a 44px disc with an arrow); the tap target is still 44px.
20
+ - A stacked sheet with a `SheetBody` has the one sheet shape (head pinned, body scrolls, foot at the bottom). A closed stack sheet can't be text-selected.
21
+ - **`AppBar brandButton`**: the mark + the name become one button (`popoverTarget` opens a `Menu`).
22
+ - **`Toggle text`**: words shown before the switch, one tap target.
23
+ - **`Text sr`**: read by a screen reader, not shown.
24
+ - **`SheetStage over`**: a stage laid over its screen (a Modal a link opened).
25
+ - Icons `megaphone`, `log-out`.
26
+
27
+ ## 0.6.4 — 2026-09-23
28
+
29
+ - **Fold card: the corner is cut** (Lh 2026-09-23): the triangle under the flap shows what is behind the card, instead of the card's own colour, which read as a second card underneath. Nothing to change in an app.
30
+ - README: the minimum browsers (Safari/iOS 18.3, Chrome 114, Firefox 128 — early 2025) and which phones that means.
31
+ - Gallery only: Settings fold away; the palette is one button that turns through them; on a small screen the settings and the component list are a drawer; the search finds a story by the words in it.
32
+
5
33
  ## 0.6.3 — 2026-09-23
6
34
 
7
35
  - **The picture is not shrunk** (Lh 2026-09-23): the 40% cap from 0.6.2 and its knob `--step-media-share` are gone. The sticky picture is `--base-content-media-h` tall, always. For 230px: `:root { --base-content-media-h: 14.375rem; }`.
package/README.md CHANGED
@@ -74,6 +74,31 @@ import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the
74
74
  ```
75
75
 
76
76
 
77
+ **เบราว์เซอร์** (เช็ก 2026-09-23): ใช้ได้กับเบราว์เซอร์ตั้งแต่**ต้นปี 2025** ขึ้นไป
78
+
79
+ | เบราว์เซอร์ | ขั้นต่ำ | ออกเมื่อ | เครื่องที่ใช้ได้ |
80
+ |---|---|---|---|
81
+ | Safari / iOS | 18.3 | ม.ค. 2025 | iPhone XS / XR (2018) ขึ้นไป ที่อัปเดตแล้ว |
82
+ | Chrome | 114 | พ.ค. 2023 | Android ส่วนใหญ่ที่ Chrome อัปเดตอยู่ |
83
+ | Firefox | 128 | ก.ค. 2024 | — |
84
+
85
+ เก่ากว่านี้: เมนูและ dropdown เปิดไม่ได้ ส่วนเบราว์เซอร์ที่ผ่านขั้นต่ำแต่ยังไม่ใช่รุ่นล่าสุด
86
+ เมนูจะขึ้นกลางจอแทนที่จะอยู่ใต้ปุ่ม
87
+
88
+ สำหรับโปรเจกต์ที่อยากเช็กเบราว์เซอร์ก่อนเริ่ม (เช่นหน้าติดตั้ง): เช็กเฉพาะฟีเจอร์ที่**จำเป็น**
89
+ ตัวอื่น cardds มีตัวสำรองไว้แล้ว ไม่ต้องเช็ก
90
+
91
+ | จำเป็น: ไม่มีแล้วหน้าจอพัง | วิธีเช็ก |
92
+ |---|---|
93
+ | popover | `'popover' in HTMLElement.prototype` |
94
+ | `@property` | `'registerProperty' in CSS` |
95
+ | `color-mix()` | `CSS.supports('color', 'color-mix(in srgb, red, blue)')` |
96
+ | container queries | `CSS.supports('container-type: size') && CSS.supports('height: 1cqh')` |
97
+ | `:has()` | `CSS.supports('selector(:has(a))')` |
98
+
99
+ มีตัวสำรองแล้ว ไม่ต้องเช็ก: anchor positioning, view transitions, `@starting-style`, `text-wrap`
100
+ iOS 17–18.2 เช็กผ่าน แต่ยังรองรับไม่เต็ม เมนูอาจแสดงผิดเล็กน้อย
101
+
77
102
  Working with Claude in another project? The `/cardds` skill
78
103
  (`~/.claude/skills/cardds/`) carries the install and the contract, and points
79
104
  back here. It is deliberately thin — **this repo stays the source of truth**,
@@ -132,6 +157,37 @@ the card is the material, the sheet is what it is in the stack.
132
157
  one — the members are sheets and the stack is the menu, not a list of cards.
133
158
  Never bring it back, not even as an alias.
134
159
 
160
+ ## Screen: top · content · bottom
161
+
162
+ **Every screen is three parts in ONE flex column** (Lh 2026-09-23) — the words used to ask for a screen, and the props
163
+ of `Screen`:
164
+
165
+ | Part | Prop | What fills it |
166
+ |---|---|---|
167
+ | **top** | `top` | `TopBar` · `AppBar` · `"lead"` (no bar: the content starts `--screen-lead`, 15% of the screen, down) |
168
+ | **content** | children | a `Step` (`Pages` · `BaseContent` · `BaseContent` + `Sheet`) · `Centre` · `SheetStack` … |
169
+ | **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
170
+
171
+ ```tsx
172
+ <Screen fill top="lead" bottom={<ActionBar variant="pager">…</ActionBar>}>
173
+ <Step><Pages>…</Pages><PagerAt at={1} of={3} /></Step>
174
+ </Screen>
175
+ ```
176
+
177
+ - **The browser measures, nobody computes.** In `Screen fill` the content takes all the height the top and the bottom
178
+ leave (`flex: 1`); the bottom is as tall as its rows. A one-row bar gives its room straight back to the content.
179
+ No `--bar-reserve` arithmetic for a bar in the screen.
180
+ - **The bar is INSIDE the screen, its last part** — `bottom`, never a sibling after `</Screen>`, never pinned with a
181
+ hand-set `position`. The keyboard lifts the column's end (`--kb`). A `FloatBar` in `bottom` floats over the
182
+ content at the screen's foot (it lies over a map or a stack — not a row).
183
+ - **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `CardFoot`; `Screen` warns once when both
184
+ are there.
185
+ - **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
186
+ `tests/composition.spec.js`) fails a bar after `</Screen>` or a pinned bar, warns on a Sheet + ActionBar.
187
+ - **Templates** are `Screen`s with the three parts filled (`Walkthrough` = top `lead` · content `Pages` · bottom
188
+ `ActionBar`); the gallery draws those parts on a template's page. The gallery's nav groups the components by
189
+ the part they fill: *top* · *content* · *bottom*.
190
+
135
191
  ## Step markup
136
192
 
137
193
  A step is one screen, one task (see *Anatomy*). It is the other component
@@ -145,7 +201,7 @@ whose markup has rules, because the sheet's box is the step itself:
145
201
  <p class="t-body">…</p>
146
202
  </section>
147
203
  </section>
148
- <div class="action-bar action-bar--pager">…</div>
204
+ <div class="action-bar action-bar--pager">…</div> <!-- the screen's bottom: inside .screen, after the step -->
149
205
 
150
206
  <!-- acts: the words + a sheet -->
151
207
  <section class="step">
@@ -190,9 +246,10 @@ whose markup has rules, because the sheet's box is the step itself:
190
246
  breathing (`--header-gap`), and the card takes that room as its lead. A
191
247
  step that follows anything else keeps the screen's gap, and the sheet
192
248
  stops under that thing.
193
- - **The bar is outside the step**, after it. `.step` reads the bar's room
194
- from `--bar-reserve`; any parent that hosts the `.action-bar` counts
195
- (`<body>`, an app shell, a demo frame).
249
+ - **The bar is after the step, inside the screen** (the screen's bottom — *Screen: top · content · bottom*). In a
250
+ `.screen--fill` the step and the bar share one flex column and touch; the step takes the rest. (A bar outside a
251
+ fill screen — an app shell — is still fixed to the viewport and the step reads `--bar-reserve`: the old way,
252
+ kept until 0.7.0.)
196
253
  - **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
197
254
  foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
198
255
  its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
@@ -248,10 +305,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
248
305
  the inset as padding, so a deck's fanned cards may overhang and clip at the
249
306
  screen's edges (the fold visual, not a bug), and stacks its children at the
250
307
  screen's gap. Never hand-lay `top: 40%` on a screen.
251
- - **The bar is outside, as always.** Plain `.action-bar` for one move; with a
308
+ - **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
252
309
  control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
253
- control tier above the buttons and publishes the taller reserve
254
- (`--bar-reserve`) from whatever parent hosts the bar.
310
+ control tier above the buttons (it publishes the taller `--bar-reserve` the centred card still reads for its cap).
255
311
  - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
256
312
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
257
313
  step that commits still goes back to one wide button (rule 3).
@@ -271,8 +327,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
271
327
  - **A pager turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
272
328
  sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
273
329
  own foot finishes it. (Guidance — not enforced.)
274
- - A screen is **base, top menu, bottom menu, content**. The base is the
275
- surface; the menus are the chrome above and below (`.topbar` / `.appbar`,
330
+ - A screen is **top · content · bottom** on the base (*Screen: top · content · bottom*). The base is the
331
+ surface; top and bottom are the chrome above and below (`.topbar` / `.appbar`,
276
332
  `.action-bar` / `.float-bar`); content is everything between.
277
333
  - Content has three homes: **on the base**, **in a card**, **in a bottom
278
334
  sheet**. Content normally comes in cards; static data (a blurb, a legal
@@ -312,8 +368,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
312
368
  retired.) Demos: `sheet.html`, `stack.html`.
313
369
  - **A step is one screen, one task: `.step`.** What the member reads is
314
370
  words on the base (`.base-content` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
315
- `.sheet` in front of it; the screen's one move is `.action-bar`, outside
316
- the step, below it. A step where the member only READS is the card alone.
371
+ `.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
372
+ bottom, after the step. A step where the member only READS is the card alone.
317
373
  A step where the member ACTS (types, picks, shoots a photo, records) adds
318
374
  the sheet: peek shows its handle and title row, half shows half of it,
319
375
  full slides up until its top edge touches the top bar — and at every one
@@ -363,6 +419,8 @@ Demo: the *skeleton* block at the top of `elements.html`.
363
419
  room itself, as a bottom margin, reading the same `--bar-reserve`. The
364
420
  reserves use the nominal control height, so a bar whose button wraps to two
365
421
  lines can still overlap the last line of content.
422
+ **In a `.screen--fill` none of this applies to a bar in the screen** (2026-09-23): it is the column's last
423
+ part and the browser measures it — the reserves are the old way for a bar outside a fill screen.
366
424
  **Padding is published, not copied.** `.screen` sets `--screen-pad` and
367
425
  `--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
368
426
  `--card-pad` and `--card-gap`; anything that has to cancel a container's
package/css/actions.css CHANGED
@@ -64,6 +64,15 @@
64
64
  .screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
65
65
  .appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
66
66
  .appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
67
+ /* the mark + the words as ONE button (AppBar brandButton — the member's name opens their menu, Lh 2026-09-23): the same row, a tap target */
68
+ .appbar__who {
69
+ flex: 1; min-width: 0; min-height: var(--tap);
70
+ display: flex; align-items: center; gap: var(--sp-2);
71
+ padding: 0; margin: 0; border: 0; background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
72
+ border-radius: var(--appbar-who-radius, var(--r-chip));
73
+ -webkit-tap-highlight-color: transparent;
74
+ }
75
+ .appbar__who:focus-visible { outline: var(--focus); outline-offset: 2px; }
67
76
  .appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
68
77
  .appbar__title { font: var(--type-label); }
69
78
  .appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
@@ -280,7 +289,13 @@
280
289
  shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
281
290
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
282
291
  .action-bar__tier > .btn { flex: 1; }
292
+ .action-bar__tier--link { justify-content: center; } /* a Link under the buttons, e.g. "details" that opens a Modal */
283
293
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
294
+ /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
295
+ adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
296
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
297
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
298
+ :has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
284
299
 
285
300
  /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
286
301
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
package/css/base.css CHANGED
@@ -105,6 +105,9 @@ button { font: inherit; cursor: pointer; }
105
105
  .card--3 .t-muted { color: inherit; opacity: 0.7; } /* 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
+ /* 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.
109
+ The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
110
+ .t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
108
111
 
109
112
  /* ---- base content: words straight on the base, no card around them — an
110
113
  a heading, a paragraph, maybe a .cta-pack. Was .section until
package/css/choice.css CHANGED
@@ -303,3 +303,14 @@
303
303
  box-shadow: var(--shadow-float);
304
304
  }
305
305
  .slider__range:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
306
+
307
+ /* ---- a switch that names itself (Toggle text, Lh 2026-09-23 — "อยู่บ้าน" on the home's top): the words, then the switch,
308
+ one label — the whole of it is the tap target ---- */
309
+ .toggle-field {
310
+ display: inline-flex; align-items: center; gap: var(--toggle-field-gap, var(--sp-2));
311
+ min-height: var(--tap);
312
+ font: var(--toggle-field-font, var(--type-label));
313
+ cursor: pointer;
314
+ -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
315
+ flex: none;
316
+ }
package/css/layover.css CHANGED
@@ -190,6 +190,9 @@
190
190
  align-items: center;
191
191
  z-index: 30;
192
192
  }
193
+ /* in a fill screen (its `bottom`) the float bar floats over the SCREEN's foot, not the window's — it lies over the content
194
+ (a map, a stack), so it is not a row of the column; the screen is its box (Lh 2026-09-23) */
195
+ .screen--fill > .float-bar { position: absolute; }
193
196
  .sheet-stage > .float-bar { position: absolute; padding-inline: var(--sp-4); }
194
197
  /* .screen's default bottom padding clears an .action-bar; a .float-bar is
195
198
  taller (--bar-h + its own bottom offset), so reserve the difference. Any
package/css/media.css CHANGED
@@ -121,6 +121,12 @@
121
121
  background-repeat: no-repeat;
122
122
  background-position: bottom right;
123
123
  background-size: var(--sp-8) var(--sp-8);
124
+ background-origin: border-box; /* the flap sits on the true corner, not inside the (transparent) border */
125
+ /* the corner is CUT: the triangle under the flap shows what is behind the card. Painted in the card's own colour it
126
+ read as the square corner of a second card underneath (Lh 2026-09-23). The polygon reaches 3rem past the other
127
+ edges so a coloured palette's shadow is not clipped there. */
128
+ clip-path: polygon(-3rem -3rem, calc(100% + 3rem) -3rem, calc(100% + 3rem) calc(100% - var(--sp-8) - 3rem),
129
+ calc(100% - var(--sp-8) - 3rem) calc(100% + 3rem), -3rem calc(100% + 3rem));
124
130
  }
125
131
 
126
132
  /* ---- wave: audio waveform stand-in for voice rows ----
package/css/sheet.css CHANGED
@@ -243,6 +243,13 @@
243
243
  that bar — the stage keeps the bar's line free itself (--_top), so the reserve
244
244
  would be paid twice and the page would scroll by it */
245
245
  .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
246
+ /* 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 */
247
+ .sheet-stage--over, .screen > .sheet-stage--over {
248
+ position: absolute; inset: 0; z-index: 40;
249
+ width: auto; height: auto; max-width: none; margin: 0;
250
+ background: transparent;
251
+ --_top: 0rem;
252
+ }
246
253
  .appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
247
254
  .sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
248
255
  /* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
package/css/stack.css CHANGED
@@ -230,6 +230,9 @@
230
230
  gap: var(--card-gap, var(--sp-5));
231
231
  overflow: clip;
232
232
  }
233
+ /* a base that is ONE BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
234
+ loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
235
+ .sheet-stack__base:has(> .base-content:only-child) { align-content: start; }
233
236
 
234
237
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
235
238
  into it — an opacity fade in place, never a slide in from the left (the
@@ -256,3 +259,55 @@
256
259
  opacity: 1;
257
260
  visibility: visible;
258
261
  }
262
+
263
+ /* ---- a stacked sheet with a SheetBody has the ONE sheet shape (Lh 2026-09-23, the home layout): sheet head
264
+ pinned at the top · sheet body scrolls, as long as it needs · sheet foot (a CardFoot, the CTA) at the bottom.
265
+ The card becomes a flex column and stops scrolling itself — only the body does. A sheet with no SheetBody
266
+ keeps the old way: the whole open card scrolls. ---- */
267
+ .sheet-stack--tap > .card:has(> .sheet__body) { display: flex; flex-direction: column; gap: var(--card-gap); }
268
+ .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
269
+ .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
270
+ .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
271
+
272
+ /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
273
+ filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
274
+ .stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, 0rem); flex: none; } /* no gap: each tab is a 40px target already, and an open head (back · title · tabs) must fit one line — Lh 2026-09-23 */
275
+ .stack-sheet__tab { position: relative; }
276
+ .stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
277
+ position: absolute;
278
+ top: calc(-1 * var(--sp-1));
279
+ right: calc(-1 * var(--sp-1));
280
+ min-width: var(--stack-sheet-count-h, 1.25rem);
281
+ height: var(--stack-sheet-count-h, 1.25rem);
282
+ padding: 0 var(--sp-1);
283
+ font: var(--type-caption);
284
+ line-height: 1;
285
+ pointer-events: none;
286
+ }
287
+ /* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
288
+ to the opposite colour (.icon-btn--invert). */
289
+ .stack-sheet__tab:not(.icon-btn--invert) { border-color: transparent; background: transparent; box-shadow: none; color: inherit; } /* the sheet's own ink, on any paper */
290
+ /* 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
291
+ margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
292
+ .card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
293
+
294
+ /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
295
+ 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
296
+ (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
297
+ .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))); }
298
+ .sheet-stack--tap .sheet-stack__back {
299
+ width: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
300
+ height: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
301
+ min-width: 0;
302
+ min-height: 0;
303
+ padding: 0;
304
+ }
305
+ .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) */
306
+ .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); }
307
+
308
+ /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
309
+ .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
310
+ /* 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),
311
+ 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) */
312
+ .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; }
313
+ .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
package/css/step.css CHANGED
@@ -57,13 +57,16 @@
57
57
  /* no bar at all — the step is the screen's first child (a page whose one heading is the base's own, Lh 2026-09-22):
58
58
  the words start a share of the screen down — --screen-lead, 15% of its height (Lh 2026-09-22), so a tall phone gives
59
59
  more air and a short one less; a screen with a bar keeps the bar's own room */
60
- .screen > .step:first-child { --_lead: var(--screen-lead, 15cqh); }
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
  /* the bar's room — read from actions.css, never recomputed. Margin, not
63
63
  padding, so the clip cuts at the box edge and nothing shows behind the
64
64
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
65
65
  frame — the bar is fixed to the viewport wherever it sits. */
66
66
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
67
+ /* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
68
+ is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
69
+ :has(> .action-bar) .step:has(> .pager__at) { margin-bottom: calc(var(--bar-reserve) - var(--sp-6) + var(--kb, 0rem)); }
67
70
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
68
71
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
69
72
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
@@ -95,7 +98,16 @@
95
98
  }
96
99
  /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
97
100
  room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
98
- .step > .pager__at { margin-top: auto; align-self: center; padding-block: var(--sp-3); flex: none; }
101
+ .step > .pager__at {
102
+ /* it FLOATS over the reading block's foot (Lh 2026-09-23: the strip it took under the words was wasted room) — the pages
103
+ run down to the bar, the dots sit on their bottom edge, centred, on the base's ground so a line passing under stays clear of them */
104
+ position: absolute; bottom: 0; left: 50%; translate: -50% 0; z-index: 2;
105
+ padding: var(--sp-3) var(--sp-2);
106
+ border-radius: var(--r-chip);
107
+ background: var(--surface);
108
+ }
109
+ /* the reading block fills the step, and keeps the dots' height free at the end of its scroll so the last line clears them */
110
+ .step:has(> .pager__at) > :is(.base-content, .pages) { flex: 1 1 auto; padding-bottom: calc(var(--dot) + 2 * var(--sp-3)); }
99
111
  .step > :is(.base-content, .pages) {
100
112
  min-height: 0;
101
113
  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 */
@@ -113,3 +125,33 @@
113
125
  z-index: 1;
114
126
  background-color: var(--surface); /* the words pass UNDER it, never through a transparent picture */
115
127
  }
128
+
129
+ /* ============================================================
130
+ ONE FLEX COLUMN (Lh 2026-09-23: every part of a screen is laid by flex). In a .screen--fill the bar is IN the
131
+ column, after the step: the step takes what is left (flex: 1), the bar takes what its rows need — measured by the
132
+ browser, no --bar-reserve arithmetic, so a one-row bar gives its room straight back to the pages. The keyboard lifts
133
+ the column's end: the screen pads its bottom by --kb, the bar rides on it.
134
+ (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
135
+ ============================================================ */
136
+ .screen--fill > .action-bar {
137
+ position: relative; inset: auto; bottom: auto;
138
+ flex: none;
139
+ margin-top: auto; /* the column's end, even on a screen with nothing that grows (a centred card is absolute) */
140
+ max-width: none;
141
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge, as the step; the bar's own padding keeps the inset */
142
+ }
143
+ .screen--fill:has(> .action-bar) { padding-bottom: var(--kb, 0rem); }
144
+ .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
145
+ /* the step and its bar touch: the screen's gap between two stacked things is not for these two (the bar's own padding is its room) */
146
+ .screen--fill:has(> .step) > .action-bar { margin-top: calc(-1 * var(--screen-gap)); }
147
+ /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
148
+ reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
149
+ .screen--fill > .appbar {
150
+ position: relative; inset: auto; top: auto;
151
+ flex: none;
152
+ max-width: none;
153
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
154
+ margin-bottom: calc(var(--sp-2) - var(--screen-gap));
155
+ }
156
+ .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
157
+ .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
@@ -307,6 +307,6 @@ export declare function Btn({ primary, quiet, block, xl, href, className, type,
307
307
  onTransitionStart?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
308
308
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
309
309
  href: string | undefined;
310
- type: "button" | "reset" | "submit" | undefined;
310
+ type: "button" | "submit" | "reset" | undefined;
311
311
  className: string;
312
312
  }, HTMLButtonElement>;
@@ -306,6 +306,6 @@ export declare function IconBtn({ label, icon, sm, invert, href, className, type
306
306
  onTransitionStart?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
307
307
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLButtonElement> | undefined;
308
308
  href: string | undefined;
309
- type: "button" | "reset" | "submit" | undefined;
309
+ type: "button" | "submit" | "reset" | undefined;
310
310
  className: string;
311
311
  }, HTMLButtonElement>;