@usableapp/cardds 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # cardds
2
2
 
3
+ > **For:** people and AI alike — this is the reference, the one place a rule of the system is written.
4
+ > The other docs only point here: `CLAUDE.md` (AI working IN this repo — decisions and gotchas),
5
+ > `docs/guides/building-with-cardds.md` (AI composing screens in React; shipped to Claude Design),
6
+ > the `/cardds` skill (AI in another project — install + contract). To LOOK at the system: `npm run dev`.
7
+
3
8
  Card-first mobile design system, **React-first**: `src/` is the component
4
9
  library — one thin component per pattern, emitting exactly the markup the CSS
5
10
  documents. The CSS (`css/*.css`)
6
11
  stays the only truth: a component never styles anything, it only picks classes
7
12
  from props. The CSS also works alone, with no build, for a host that wants
8
- class names rather than components. The demo is `/gallery` (`npm run dev`):
9
- every component's stories, live, searchable; `tests/fixtures/` holds the
10
- HTML pages the acceptance tests drive (not a demo — nobody reads them).
13
+ class names rather than components. The demo is the gallery (`npm run dev`):
14
+ every component's stories, live, searchable.
11
15
 
12
16
  Repo: **https://github.com/everysundays/cardds** (private — this is the
13
17
  source of truth; every consuming project vendors a copy from here, never
@@ -25,29 +29,29 @@ licences (`fonts/LICENSES.md`).
25
29
  npm install
26
30
  npm run dev # the gallery on http://localhost:5174 — every story, searchable
27
31
  npm run build # the library: icons → src/type/icons.ts · tsc → dist/ (ESM + .d.ts) · dist/cardds.css (flattened)
28
- npm test # Playwright: the step/sheet/centre geometry on tests/fixtures/, the app's screens and the four handoff upgrades
32
+ npm test # Playwright: the tokens and the step/sheet/centre geometry on tests/fixtures/, the gallery's stories and behaviours
29
33
  ```
30
34
 
31
35
  ```tsx
32
- import { Screen, TopBar, Step, BaseContent, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from 'cardds';
33
- import 'cardds/dist/cardds.css'; // or cardds/cardds.css with the css/ folder beside it
36
+ import { Screen, TopBar, Step, BaseContent, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
37
+ import '@usableapp/cardds/dist/cardds.css';
38
+ import './theme.css'; // the project's own look, after the system's — see "A project's theme"
34
39
  ```
35
40
 
36
41
  `src/index.ts` imports `cardds.js` for its side effects, so a React host gets
37
42
  the behaviours for free: the `.sheet` handle (tap, drag, keyboard) with the
38
43
  focus and on-screen-keyboard raises, `.sheet-stack--tap` opening and closing,
39
44
  the `.pin` auto-advance (a digit moves on, Backspace moves back, a paste fills
40
- the code), a card heading that needs more than two lines squeezing its type
41
- (see *Anatomy*), and the `.dropdown` writing its picked value back. `Icon`
45
+ the code), the modal's close, and the `.dropdown` writing its picked value back. `Icon`
42
46
  inlines the 56 symbols of `icons.svg` at build time, so no sprite file ships.
43
47
 
44
- **The gallery** (`gallery/`): Vite + React Router, `cardds` aliased to `src/`
45
- so the gallery and the package share one truth with no build between them.
46
- `/` lists every story of every component, `/:Name` one component. (The
47
- Commons Time Bank app that used to live here — twelve screens from
48
- `docs/handoff/commons-time-bank-SPEC.md` — was dropped on 2026-09-16; nobody
49
- looked at it, the gallery is the demo. The type scale is the `Text · Scale`
50
- story.)
48
+ **The gallery** (`gallery/`, `npm run dev` → http://localhost:5174): Vite + React Router, `cardds`
49
+ aliased to `src/`, so the gallery and the package share one truth with no build between them.
50
+ `/` every story of every component, `/:Name` one component — each `.design-sync/previews/<Name>.tsx`
51
+ story live in a 375×812 phone cell, grouped by `src/<group>/`, searchable; switches for the palette
52
+ (`own` = a colour well per colour base), inspect (box outlines + dimensions), a mock keyboard (focus a field:
53
+ it comes up over the phone cell and publishes `--kb` there — to SEE what a keyboard leaves of the screen; it
54
+ types nothing), text size, and a slider per size base. Nothing is hand-listed: add a preview file or a story and it shows.
51
55
 
52
56
  **CSS only** (no React, no build):
53
57
 
@@ -62,12 +66,7 @@ popover) but won't update its label, a stack still displays but won't open, a
62
66
  sheet renders every state but only moves when a class changes, a pin is six
63
67
  plain inputs.
64
68
 
65
- The one place to look is `/gallery` (`npm run dev` → `http://localhost:5174/gallery`):
66
- every `.design-sync/previews/<Name>.tsx` story, live, grouped by `src/<group>/`,
67
- searchable, in a 375×812 phone cell, with a palette switch and an inspect
68
- switch (box outlines + dimensions on hover). `tests/fixtures/*.html` (ex
69
- `demo/`) are the pages `tests/step.spec.js` and `tests/centre.spec.js` measure;
70
- `tests/gallery.spec.js` drives the gallery. Nothing here is verified by eye.
69
+ `tests/fixtures/*.html` are the pages the geometry tests measure — not a demo. Nothing here is verified by eye.
71
70
 
72
71
  ### In another project
73
72
 
@@ -83,7 +82,6 @@ import { Card, CardHead, Btn } from '@usableapp/cardds';
83
82
  import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the package
84
83
  ```
85
84
 
86
- Browse what exists at `npm run dev` → `/gallery` in this repo (every component, every story, searchable).
87
85
 
88
86
  **CSS only:** there's no build and no dependencies, so installing is cloning once, then
89
87
  copying the static files into your project — never edit the clone, never
@@ -113,13 +111,9 @@ on GitHub) — set that up as an SSH host alias if you already use a different
113
111
  account for `git@github.com` day to day, the way this machine's `~/.ssh/config`
114
112
  does it for other `everysundays` repos.
115
113
 
116
- **Recording what you copied matters.** A vendored copy with no record of which
117
- commit it came from can't be told apart from a fork. timebank's
118
- `scripts/cardds-vendor.mjs` is the reference: `npm run cardds:sync` copies the
119
- files above from a local checkout (`CARDDS_SRC`, default `~/Sites/cardds`) and
120
- writes `VENDORED.json` — commit hash, date, a sha256 per file — so
121
- `npm run check:cardds` can catch a hand edit or a stale copy. `git pull` that
122
- checkout before syncing to pick up what's been pushed here.
114
+ **Record what you copied.** A vendored copy with no record of the commit it came
115
+ from can't be told apart from a fork: write the commit hash (and a sha256 per
116
+ file) beside the copy, so a hand edit or a stale copy can be caught.
123
117
 
124
118
  Working with Claude in another project? The `/cardds` skill
125
119
  (`~/.claude/skills/cardds/`) carries the install and the contract, and points
@@ -204,9 +198,10 @@ whose markup has rules, because the sheet's box is the step itself:
204
198
  …cardds controls (.field / .pin / .composer / .rows + .check / .segment /
205
199
  .chip-grid), or small content cards (.card--sm)…
206
200
  </div>
201
+ <div class="card__foot"><button class="btn btn--primary btn--block">…the one button that finishes the sheet…</button></div>
207
202
  </article>
208
203
  </section>
209
- <div class="action-bar">…one wide button…</div>
204
+ <!-- no .action-bar: a bar is the base's, never in front of a sheet -->
210
205
  ```
211
206
 
212
207
  - **`.step` needs a bounded column**: a direct child of `.screen--fill`, or
@@ -239,9 +234,18 @@ whose markup has rules, because the sheet's box is the step itself:
239
234
  - **The bar is outside the step**, after it. `.step` reads the bar's room
240
235
  from `--bar-reserve`; any parent that hosts the `.action-bar` counts
241
236
  (`<body>`, an app shell, a demo frame).
242
- - **A sheet inside a step never carries the step's action.** The bar is the
243
- one move. A sheet gets its own buttons only on a screen that has no bar —
244
- the map sheet in `sheet.html`, whose "Directions" is that screen's move.
237
+ - **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
238
+ foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
239
+ its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
240
+ needs to: the body's bottom padding is the part that slid under (`--_cover`), so the last row comes up to
241
+ the button at half and 3q as it does at full.
242
+ - **Layers: the base, the sheet over it, the top bar** (Lh 2026-09-20). The `.action-bar` belongs to the
243
+ BASE — the move of a page of words (a pager, or one wide button). It is never in front of a sheet. A
244
+ screen with a sheet finishes ON the sheet: a `.card__foot`, the sheet's last child — ONE row, the page's
245
+ conclusion. The foot stands on the screen's bottom edge at half, 3q and full alike (a layer as tall as
246
+ the sheet, slid up by the share the sheet slid down — `sheet.css`), and is gone at peek. Nothing inside
247
+ the sheet's body commits on its own (a `Composer` there takes no `send`). A bar written beside a sheet anyway
248
+ stands down — no pointer (`actions.css`), `inert` (`cardds.js`): a net for a mistake, not a layout.
245
249
  - **Never write a sheet's height, and never a `.sheet-stage` inside a step.**
246
250
  State is a class (`sheet--peek` / `--half` / `--full`); the sheet is always
247
251
  the step's full height and only slides. `carddsSheetSet(sheet, state)` is
@@ -290,6 +294,23 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
290
294
  - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
291
295
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
292
296
  step that commits still goes back to one wide button (rule 3).
297
+ - **BaseContent that moves** (Lh 2026-09-20) — two ways, never both on one screen:
298
+ - *Pages of words:* `.step > .pages > .base-content` (one `aria-current="step"`, the rest `inert`) — every page
299
+ one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
300
+ - *Called sheets:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`), each naming a
301
+ sheet: `<button aria-controls="signin">` … `<article id="signin" class="card sheet sheet--away" inert
302
+ data-sheet-states="peek full">`. Each sheet holds that way's WHOLE form and its own submit on its foot, leading
303
+ to the next page; the base carries no control and the screen no bar. `.sheet--away` = not on the screen (slid
304
+ out, hidden once gone). Nothing waits at peek. A tap raises its sheet to full and sends any other away; the
305
+ handle takes it down to peek and never further — the words and the other buttons are back in reach (the step
306
+ publishes `--sheet-room`, the peek strip, so they scroll clear of it); the same button raises it from peek to full.
307
+ **Never a form taken apart** — one sheet per field, a "send" left on the base: a form lives whole in ONE sheet.
308
+ Fixture `tests/fixtures/calls.html`, tests `tests/calls.spec.js`, stories `Step · Walks` / `Step · Calls`.
309
+ - **A pager turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
310
+ sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
311
+ own foot finishes it. Should a pager meet a sheet anyway it stands
312
+ down while the sheet is on the screen, a peeking one included: dimmed, no pointer
313
+ (`actions.css`), `inert` (`cardds.js`).
293
314
 
294
315
  ## Files
295
316
 
package/cardds.js CHANGED
@@ -207,16 +207,19 @@ function carddsSheetStates(sheet) {
207
207
  }
208
208
 
209
209
  function carddsSheetState(sheet) {
210
+ if (sheet.classList.contains('sheet--away')) return 'away'; // a called sheet not on the screen — a state to set, never one to snap or tap to
210
211
  return CARDDS_SHEET_STATES.find(s => sheet.classList.contains('sheet--' + s)) ?? 'half'; // the CSS default
211
212
  }
212
213
 
213
214
  function carddsSheetSet(sheet, state) {
214
- if (!CARDDS_SHEET_STATES.includes(state)) return;
215
+ if (state !== 'away' && !CARDDS_SHEET_STATES.includes(state)) return;
215
216
  const was = carddsSheetState(sheet);
216
- CARDDS_SHEET_STATES.forEach(s => sheet.classList.toggle('sheet--' + s, s === state));
217
+ [...CARDDS_SHEET_STATES, 'away'].forEach(s => sheet.classList.toggle('sheet--' + s, s === state));
218
+ sheet.inert = state === 'away';
217
219
  /* where a tap from peek returns to: the non-peek state it is going to, or the
218
220
  one it is leaving (an authored state never passed through here before) */
219
- const home = state !== 'peek' ? state : was !== 'peek' ? was : null;
221
+ const rests = s => s !== 'peek' && s !== 'away';
222
+ const home = rests(state) ? state : rests(was) ? was : null;
220
223
  if (home) sheet.dataset.sheetHome = home;
221
224
  if (was !== state) sheet.dispatchEvent(new CustomEvent('cardds:sheet', { bubbles: true, detail: { state } }));
222
225
  }
@@ -375,7 +378,9 @@ function carddsSheetReveal(el, atLeast) {
375
378
  const box = sheet.parentElement.getBoundingClientRect(); // the box that clips it: .step or .sheet-stage
376
379
  const vv = window.visualViewport;
377
380
  const cut = vv ? Math.min(box.bottom, vv.offsetTop + vv.height) : box.bottom;
378
- const need = el.getBoundingClientRect().height;
381
+ const foot = sheet.querySelector(':scope > .card__foot'); // the sheet's own button stands on the cut (sheet.css): the control has to clear it too
382
+ const button = foot?.firstElementChild; // the foot is a layer as tall as the sheet: what the control must clear is its strip
383
+ const need = (el.closest('.field') ?? el).getBoundingClientRect().height + (button ? box.bottom - button.getBoundingClientRect().top : 0); // from the button's top to the box's bottom edge, where it stands
379
384
  let to = states[states.length - 1];
380
385
  for (let i = from; i < states.length; i++) {
381
386
  if (cut - (restTop + stops[states[i]] + bodyTop) >= need) { to = states[i]; break; }
@@ -414,3 +419,60 @@ document.addEventListener('click', e => {
414
419
  stage.dispatchEvent(new CustomEvent('cardds:modal', { bubbles: true, detail: { answer: btn.dataset.answer ?? null } }));
415
420
  });
416
421
  });
422
+
423
+ /* the two calls an app makes are on window: a bundler loads this file as a module, where a top-level
424
+ function is not a global — and README has always said "call carddsSheetSet(sheet, state)". */
425
+ if (typeof window !== 'undefined') Object.assign(window, { carddsSheetSet, carddsSheetReveal });
426
+
427
+ /* ---- called sheets (sheet.css): a button in a step's words names a sheet with aria-controls ----
428
+ (The base offers ways in — sign in · new phone · join — each sheet holds that way's whole form and
429
+ its own submit on its foot.) A tap on it sends every other sheet of that step away and raises its own to the top state it snaps
430
+ to (full) — from away, and from peek alike. The step then publishes --sheet-room, the strip the
431
+ sheet keeps at peek, so the words can scroll their buttons clear of it (step.css); the buttons say
432
+ aria-expanded. */
433
+ function carddsSheetCall(sheet) {
434
+ const step = sheet.parentElement;
435
+ step.querySelectorAll(':scope > .sheet').forEach(s => { if (s !== sheet && carddsSheetState(s) !== 'away') carddsSheetSet(s, 'away'); });
436
+ const states = carddsSheetStates(sheet);
437
+ const top = states.includes('full') ? 'full' : states[states.length - 1];
438
+ const was = carddsSheetState(sheet);
439
+ if (was !== top) carddsSheetSet(sheet, top);
440
+ if (was === 'away') sheet.querySelector(':scope > .sheet__handle')?.focus({ preventScroll: true });
441
+ }
442
+ function carddsStepSync(step) {
443
+ const callers = [...step.querySelectorAll('[aria-controls]')].filter(b => document.getElementById(b.getAttribute('aria-controls'))?.matches('.step > .sheet'));
444
+ if (!callers.length) return; // a plain step: one sheet, nothing calls it, nothing to publish
445
+ const on = [...step.querySelectorAll(':scope > .sheet')].find(s => carddsSheetState(s) !== 'away');
446
+ if (on) step.style.setProperty('--sheet-room', getComputedStyle(on).getPropertyValue('--sheet-peek')); // registered: reads back in px
447
+ else step.style.removeProperty('--sheet-room');
448
+ callers.forEach(b => b.setAttribute('aria-expanded', String(carddsSheetState(document.getElementById(b.getAttribute('aria-controls'))) !== 'away')));
449
+ }
450
+ document.addEventListener('click', e => {
451
+ const caller = e.target.closest('[aria-controls]');
452
+ const sheet = caller && document.getElementById(caller.getAttribute('aria-controls'));
453
+ if (sheet && sheet.matches('.step > .sheet')) carddsSheetCall(sheet);
454
+ });
455
+ document.addEventListener('cardds:sheet', e => { const step = e.target.closest?.('.step'); if (step) carddsStepSync(step); });
456
+
457
+ /* ---- bar guard: a bar is the base's, never in front of a sheet (actions.css, Lh 2026-09-20) ----
458
+ A sheet on the screen covers the bar and CSS takes the pointer away; `inert` takes the keyboard and
459
+ the accessibility tree too. Re-checked when a sheet changes state and when the page changes. */
460
+ function carddsPagerGuard() {
461
+ document.querySelectorAll('.action-bar').forEach((bar) => {
462
+ const host = bar.parentElement;
463
+ bar.inert = !!(host && host.querySelector('.step > .sheet:not(.sheet--away)'));
464
+ });
465
+ }
466
+ if (typeof document !== 'undefined') {
467
+ document.addEventListener('cardds:sheet', carddsPagerGuard);
468
+ const start = () => {
469
+ carddsPagerGuard();
470
+ let queued = false;
471
+ new MutationObserver(() => {
472
+ if (queued) return;
473
+ queued = true;
474
+ requestAnimationFrame(() => { queued = false; carddsPagerGuard(); });
475
+ }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
476
+ };
477
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
478
+ }
package/css/actions.css CHANGED
@@ -239,6 +239,14 @@
239
239
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
240
240
  </div> — or the same two in an .action-bar__tier--pager under a control */
241
241
  .action-bar--pager { align-items: center; }
242
+ /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
243
+ them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
244
+ (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
245
+ foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
246
+ past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
247
+ is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
248
+ the eye cannot see. */
249
+ :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
242
250
  /* the page dots: under the card in a .screen__centre group; tight gaps */
243
251
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
244
252
  .pager__at > * {
package/css/card.css CHANGED
@@ -209,7 +209,7 @@
209
209
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
210
210
  simply switches. Only the card is named; the rest of the page must not
211
211
  cross-fade. */
212
- .screen__centre > .card, .pages > .card[aria-current] { view-transition-name: centre-card; }
212
+ .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
213
213
 
214
214
  /* ---- pages: the cards of a walk, all one height ----
215
215
  A sequence the member walks (an introduction) reads steadier when every card
@@ -223,8 +223,14 @@
223
223
  <article class="card" inert>…</article>
224
224
  </div><div class="pager__at">…</div></div> */
225
225
  .pages { display: grid; }
226
- .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
227
- .pages > .card[inert] { visibility: hidden; }
226
+ .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
227
+ .pages > [inert] { visibility: hidden; }
228
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
229
+ .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
230
+ <section class="step"><div class="pages">
231
+ <section class="base-content" aria-current="step">…</section>
232
+ <section class="base-content" inert>…</section>
233
+ </div></section> <div class="action-bar action-bar--pager">← →</div> */
228
234
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
229
235
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
230
236
  animation-duration: var(--motion-slide);
package/css/sheet.css CHANGED
@@ -88,6 +88,19 @@
88
88
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
89
89
  .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
90
90
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
91
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
92
+ .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
93
+ changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
94
+ with `inert`. A button names its sheet — <button aria-controls="when"> … <article id="when" class="card
95
+ sheet sheet--away" inert data-sheet-states="peek full"> — and cardds.js does the rest: a tap slides that
96
+ sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
97
+ words and the other buttons are back in reach; the same button again raises it from peek to full.
98
+ Nothing waits at peek before the first call. No bar on such a screen (actions.css).
99
+ WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
100
+ that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
101
+ carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
102
+ a form lives whole in one sheet. */
103
+ .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
91
104
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
92
105
 
93
106
  /* the body — everything under the head, the one thing in a sheet that
@@ -112,9 +125,45 @@
112
125
  the cut. A length from the box's container units (--_box), since the
113
126
  cut is a share of the SHEET's height, not of the body's. */
114
127
  scroll-padding-bottom: var(--_cover);
128
+ /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
129
+ scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
130
+ foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
131
+ when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
132
+ the part that shows. */
133
+ padding-bottom: var(--_cover);
115
134
  }
135
+ /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
136
+ A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
137
+ page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
138
+ full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
139
+ foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
140
+ slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
141
+ 3q and full alike while the sheet slides behind them. A percentage of the same height on both — not a
142
+ length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
143
+ takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
144
+ stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
145
+ One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
146
+ .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
147
+ .sheet > .card__foot {
148
+ 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 */
149
+ align-items: end;
150
+ padding: 0 var(--card-pad) var(--card-pad);
151
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
152
+ pointer-events: none;
153
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
154
+ transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s;
155
+ }
156
+ .sheet > .card__foot > * { pointer-events: auto; }
157
+ .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) */
158
+ .sheet.is-dragging > .card__foot { transition: none; }
159
+ :is(.sheet--peek, .sheet--away) > .card__foot { opacity: 0; visibility: hidden; transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
160
+ /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
161
+ box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
162
+ the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
163
+ .sheet > :not(.sheet__body) { flex-shrink: 0; }
164
+ :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
116
165
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
117
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
166
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
118
167
 
119
168
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
120
169
  the thumb's grab area for the whole strip above the title: a pseudo widens
@@ -275,6 +324,12 @@
275
324
  /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
276
325
  edge shows, so it gets its corners and border back; full still stops at
277
326
  the top bar's line, because the top edge never moved. */
327
+ /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
328
+ foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
329
+ scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
330
+ the sheet gives it up itself, as --_lift — the same token a raised sheet floats on. */
331
+ .sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
332
+ .sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
278
333
  .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
279
334
  .sheet-stage__foot {
280
335
  position: absolute;
package/css/step.css CHANGED
@@ -1,14 +1,16 @@
1
1
  /* ============================================================
2
2
  cardds/step.css — .step: one screen, one task.
3
3
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
4
- step asks for it (a sheet, sheet.css), and the screen's one
5
- move (.action-bar, actions.css) OUTSIDE the step, below it.
4
+ step asks for it (a sheet, sheet.css). The move: a page of words has
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
7
+ (see "LAYERS" below).
6
8
 
7
9
  <section class="step">
8
10
  <section class="base-content">…the words…</section> ← first
9
- <article class="card sheet sheet--half">…the controls…</article> ← last, optional
11
+ <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
10
12
  </section>
11
- <div class="action-bar">…</div>
13
+ <div class="action-bar">…</div> ← only when there is no sheet
12
14
 
13
15
  The step is the box the sheet slides in. It fills the screen
14
16
  column between the top bar's reserve (.screen already pads for
@@ -58,6 +60,10 @@
58
60
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
59
61
  frame — the bar is fixed to the viewport wherever it sits. */
60
62
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
63
+ /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
64
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
65
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
66
+ beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
61
67
 
62
68
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
63
69
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -67,14 +73,23 @@
67
73
  gets the flex rule: it hugs its content, and when the column runs out (a
68
74
  short phone, large text) it shrinks and scrolls inside itself instead of
69
75
  pushing the bar or the sheet */
70
- .step > .base-content {
76
+ .step > .base-content, .step > .pages > .base-content {
71
77
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
72
78
  --card-gap: var(--sp-4);
73
79
  gap: var(--card-gap);
74
80
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
75
81
  align-content: start;
76
82
  color: var(--on-surface);
83
+ }
84
+ /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
85
+ (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
86
+ on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
87
+ nothing without one. */
88
+ .step > :is(.base-content, .pages) {
89
+ padding-bottom: var(--sheet-room, 0rem);
77
90
  flex: 0 1 auto;
91
+ }
92
+ .step > :is(.base-content, .pages) {
78
93
  min-height: 0;
79
94
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
80
95
  overflow-y: auto;
package/dist/cardds.css CHANGED
@@ -724,7 +724,7 @@ button { font: inherit; cursor: pointer; }
724
724
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
725
725
  simply switches. Only the card is named; the rest of the page must not
726
726
  cross-fade. */
727
- .screen__centre > .card, .pages > .card[aria-current] { view-transition-name: centre-card; }
727
+ .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
728
728
 
729
729
  /* ---- pages: the cards of a walk, all one height ----
730
730
  A sequence the member walks (an introduction) reads steadier when every card
@@ -738,8 +738,14 @@ button { font: inherit; cursor: pointer; }
738
738
  <article class="card" inert>…</article>
739
739
  </div><div class="pager__at">…</div></div> */
740
740
  .pages { display: grid; }
741
- .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
742
- .pages > .card[inert] { visibility: hidden; }
741
+ .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
742
+ .pages > [inert] { visibility: hidden; }
743
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
744
+ .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
745
+ <section class="step"><div class="pages">
746
+ <section class="base-content" aria-current="step">…</section>
747
+ <section class="base-content" inert>…</section>
748
+ </div></section> <div class="action-bar action-bar--pager">← →</div> */
743
749
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
744
750
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
745
751
  animation-duration: var(--motion-slide);
@@ -1159,6 +1165,19 @@ button { font: inherit; cursor: pointer; }
1159
1165
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1160
1166
  .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1161
1167
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1168
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1169
+ .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
1170
+ changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
1171
+ with `inert`. A button names its sheet — <button aria-controls="when"> … <article id="when" class="card
1172
+ sheet sheet--away" inert data-sheet-states="peek full"> — and cardds.js does the rest: a tap slides that
1173
+ sheet up to full and sends any other one away; the handle takes it down to peek, never further, so the
1174
+ words and the other buttons are back in reach; the same button again raises it from peek to full.
1175
+ Nothing waits at peek before the first call. No bar on such a screen (actions.css).
1176
+ WHAT IT IS FOR: the base offers WAYS IN — sign in · new phone · join — and each button calls a sheet
1177
+ that holds that way's WHOLE form, its submit on the sheet's foot, leading to the next page. The base
1178
+ carries no control. It is NOT for a form taken apart — one sheet per field, a "send" left on the base:
1179
+ a form lives whole in one sheet. */
1180
+ .sheet--away { --_y: 100%; --_cover: var(--_box); visibility: hidden; transition: translate var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
1162
1181
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1163
1182
 
1164
1183
  /* the body — everything under the head, the one thing in a sheet that
@@ -1183,9 +1202,45 @@ button { font: inherit; cursor: pointer; }
1183
1202
  the cut. A length from the box's container units (--_box), since the
1184
1203
  cut is a share of the SHEET's height, not of the body's. */
1185
1204
  scroll-padding-bottom: var(--_cover);
1186
- }
1205
+ /* …and its content has to be able to LEAVE that part: the same length as bottom padding, so the last row
1206
+ scrolls up to the visible edge at half and 3q as it does at full — everything between the head and the
1207
+ foot can be reached at any state (Lh, 2026-09-21). Padding, not a shorter box: nothing that shows moves
1208
+ when the state changes, only how far the body scrolls; a child that fills the body (a composer) fills
1209
+ the part that shows. */
1210
+ padding-bottom: var(--_cover);
1211
+ }
1212
+ /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
1213
+ A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
1214
+ page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
1215
+ full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
1216
+ foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
1217
+ slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
1218
+ 3q and full alike while the sheet slides behind them. A percentage of the same height on both — not a
1219
+ length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
1220
+ takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
1221
+ stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
1222
+ One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1223
+ .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
1224
+ .sheet > .card__foot {
1225
+ 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 */
1226
+ align-items: end;
1227
+ padding: 0 var(--card-pad) var(--card-pad);
1228
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1229
+ pointer-events: none;
1230
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1231
+ transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s;
1232
+ }
1233
+ .sheet > .card__foot > * { pointer-events: auto; }
1234
+ .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) */
1235
+ .sheet.is-dragging > .card__foot { transition: none; }
1236
+ :is(.sheet--peek, .sheet--away) > .card__foot { opacity: 0; visibility: hidden; transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s linear var(--motion-sheet); }
1237
+ /* that padding can be taller than the body's own box (a short box: a small phone with the keyboard up) and a
1238
+ box is never smaller than its padding — so the pinned parts must not give way to it: the body runs past
1239
+ the sheet's clipped edge instead, where nothing shows anyway. At peek and away no part of the body shows. */
1240
+ .sheet > :not(.sheet__body) { flex-shrink: 0; }
1241
+ :is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
1187
1242
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1188
- .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
1243
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
1189
1244
 
1190
1245
  /* grab handle — first child of the sheet. Drawn as a small bar, but it is
1191
1246
  the thumb's grab area for the whole strip above the title: a pseudo widens
@@ -1346,6 +1401,12 @@ button { font: inherit; cursor: pointer; }
1346
1401
  /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
1347
1402
  edge shows, so it gets its corners and border back; full still stops at
1348
1403
  the top bar's line, because the top edge never moved. */
1404
+ /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
1405
+ foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
1406
+ scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
1407
+ the sheet gives it up itself, as --_lift — the same token a raised sheet floats on. */
1408
+ .sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
1409
+ .sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
1349
1410
  .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
1350
1411
  .sheet-stage__foot {
1351
1412
  position: absolute;
@@ -1375,14 +1436,16 @@ button { font: inherit; cursor: pointer; }
1375
1436
  /* ============================================================
1376
1437
  cardds/step.css — .step: one screen, one task.
1377
1438
  What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
1378
- step asks for it (a sheet, sheet.css), and the screen's one
1379
- move (.action-bar, actions.css) OUTSIDE the step, below it.
1439
+ step asks for it (a sheet, sheet.css). The move: a page of words has
1440
+ an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
1441
+ a step with a sheet has none: the sheet's own .card__foot finishes it
1442
+ (see "LAYERS" below).
1380
1443
 
1381
1444
  <section class="step">
1382
1445
  <section class="base-content">…the words…</section> ← first
1383
- <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1446
+ <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
1384
1447
  </section>
1385
- <div class="action-bar">…</div>
1448
+ <div class="action-bar">…</div> ← only when there is no sheet
1386
1449
 
1387
1450
  The step is the box the sheet slides in. It fills the screen
1388
1451
  column between the top bar's reserve (.screen already pads for
@@ -1432,6 +1495,10 @@ button { font: inherit; cursor: pointer; }
1432
1495
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
1433
1496
  frame — the bar is fixed to the viewport wherever it sits. */
1434
1497
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1498
+ /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1499
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1500
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
1501
+ beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
1435
1502
 
1436
1503
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
1437
1504
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -1441,14 +1508,23 @@ button { font: inherit; cursor: pointer; }
1441
1508
  gets the flex rule: it hugs its content, and when the column runs out (a
1442
1509
  short phone, large text) it shrinks and scrolls inside itself instead of
1443
1510
  pushing the bar or the sheet */
1444
- .step > .base-content {
1511
+ .step > .base-content, .step > .pages > .base-content {
1445
1512
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1446
1513
  --card-gap: var(--sp-4);
1447
1514
  gap: var(--card-gap);
1448
1515
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
1449
1516
  align-content: start;
1450
1517
  color: var(--on-surface);
1518
+ }
1519
+ /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1520
+ (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
1521
+ on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
1522
+ nothing without one. */
1523
+ .step > :is(.base-content, .pages) {
1524
+ padding-bottom: var(--sheet-room, 0rem);
1451
1525
  flex: 0 1 auto;
1526
+ }
1527
+ .step > :is(.base-content, .pages) {
1452
1528
  min-height: 0;
1453
1529
  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 */
1454
1530
  overflow-y: auto;
@@ -1820,6 +1896,14 @@ button { font: inherit; cursor: pointer; }
1820
1896
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1821
1897
  </div> — or the same two in an .action-bar__tier--pager under a control */
1822
1898
  .action-bar--pager { align-items: center; }
1899
+ /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
1900
+ them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
1901
+ (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
1902
+ foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
1903
+ past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
1904
+ is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
1905
+ the eye cannot see. */
1906
+ :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
1823
1907
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1824
1908
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1825
1909
  .pager__at > * {
@@ -5,10 +5,13 @@ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
5
5
  * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
6
6
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
7
7
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
8
- * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
8
+ * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
9
+ * (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of CALLERS —
10
+ * `Btn aria-controls={a Sheet's id}` — WAYS IN (sign in · new phone · join), each calling a `Sheet state="away"` that holds that way's
11
+ * whole form and its submit. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
9
12
  */
10
13
  export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
11
14
  export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
12
15
  }
13
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
16
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds CALLERS only (buttons that call a sheet, `aria-controls`) — the Step's move is its ActionBar. */
14
17
  export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -4,12 +4,15 @@ import { cx } from '../cx.js';
4
4
  * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
5
5
  * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
6
6
  * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
7
- * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
7
+ * In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
8
+ * (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of CALLERS —
9
+ * `Btn aria-controls={a Sheet's id}` — WAYS IN (sign in · new phone · join), each calling a `Sheet state="away"` that holds that way's
10
+ * whole form and its submit. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
8
11
  */
9
12
  export function BaseContent({ className, ...rest }) {
10
13
  return _jsx("section", { className: cx('base-content', className), ...rest });
11
14
  }
12
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
15
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds CALLERS only (buttons that call a sheet, `aria-controls`) — the Step's move is its ActionBar. */
13
16
  export function CtaPack({ className, ...rest }) {
14
17
  return _jsx("div", { className: cx('cta-pack', className), ...rest });
15
18
  }
@@ -14,9 +14,11 @@ export declare function Centre({ className, ...rest }: CentreProps): import("rea
14
14
  export interface PagesProps extends ComponentPropsWithoutRef<'div'> {
15
15
  }
16
16
  /**
17
- * Pages — the cards of a walk, all one height: every Card is rendered, stacked in one cell as tall as the
17
+ * Pages — the pages of a walk, all one height: every page is rendered, stacked in one cell as tall as the
18
18
  * tallest at any text size. Give the current one `aria-current="step"` and every other one `inert`
19
- * (hidden in place, out of the tab order). Goes inside a Centre, above the PagerAt; turn with slideTo().
19
+ * (hidden in place, out of the tab order). Turn with slideTo().
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `BaseContent`s, as a Step's
21
+ * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
20
22
  */
21
23
  export declare function Pages({ className, ...rest }: PagesProps): import("react").JSX.Element;
22
24
  /** the direction a Centre's card slides: `next` — out to the left, the new one in from the right; `back` — the reverse */
@@ -14,9 +14,11 @@ export function Centre({ className, ...rest }) {
14
14
  return _jsx("div", { className: cx('screen__centre', className), ...rest });
15
15
  }
16
16
  /**
17
- * Pages — the cards of a walk, all one height: every Card is rendered, stacked in one cell as tall as the
17
+ * Pages — the pages of a walk, all one height: every page is rendered, stacked in one cell as tall as the
18
18
  * tallest at any text size. Give the current one `aria-current="step"` and every other one `inert`
19
- * (hidden in place, out of the tab order). Goes inside a Centre, above the PagerAt; turn with slideTo().
19
+ * (hidden in place, out of the tab order). Turn with slideTo().
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `BaseContent`s, as a Step's
21
+ * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
20
22
  */
21
23
  export function Pages({ className, ...rest }) {
22
24
  return _jsx("div", { className: cx('pages', className), ...rest });
@@ -1,7 +1,7 @@
1
1
  import type { ComponentPropsWithoutRef } from 'react';
2
- export type SheetState = 'peek' | 'half' | '3q' | 'full';
2
+ export type SheetState = 'peek' | 'half' | '3q' | 'full' | 'away';
3
3
  export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
4
- /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
4
+ /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. `away` = a CALLED sheet not on the screen yet (slid out, inert): a `Btn aria-controls={its id}` in the Step's BaseContent calls it up to full and sends any other away. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
5
5
  state?: SheetState;
6
6
  /** floats above a footnote strip on a dim stage (bottom corners and border back) */
7
7
  raised?: boolean;
@@ -15,7 +15,10 @@ export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
15
15
  * content cards). Always the full height of its box — a Step or a SheetStage — a state is only how far it slid.
16
16
  * Shape: handle · head (a CardHead, a SheetHead — or ONE label Row carrying a Check/Toggle, when the sheet's
17
17
  * whole ask is that switch and peek must show it) · SheetBody (the only part that scrolls). Written top-down.
18
- * Inside a Step it never carries the step's action (the ActionBar is the one move); its own buttons only on a
19
- * screen with no bar (the map sheet). This is the single sheet; the menu of stacked sheets is `SheetStack`.
18
+ * It FINISHES on itself: a `CardFoot` as its last child holds the one button that completes it — standing at the bottom at
19
+ * half / 3q / full, gone at peek. Never an ActionBar in front of a sheet (a bar is the base's). Nothing in the body commits
20
+ * on its own. Older note — its own buttons only on a
21
+ * screen with no bar (the map sheet). Called sheets: several `Sheet state="away" states="peek full"` in one Step, each with an `id` a
22
+ * button names — one on the screen at a time; never beside a pager. This is the single sheet; the menu of stacked sheets is `SheetStack`.
20
23
  */
21
24
  export declare function Sheet({ state, raised, handle, states, className, children, ...rest }: SheetProps): import("react").JSX.Element;
@@ -5,9 +5,12 @@ import { cx } from '../cx.js';
5
5
  * content cards). Always the full height of its box — a Step or a SheetStage — a state is only how far it slid.
6
6
  * Shape: handle · head (a CardHead, a SheetHead — or ONE label Row carrying a Check/Toggle, when the sheet's
7
7
  * whole ask is that switch and peek must show it) · SheetBody (the only part that scrolls). Written top-down.
8
- * Inside a Step it never carries the step's action (the ActionBar is the one move); its own buttons only on a
9
- * screen with no bar (the map sheet). This is the single sheet; the menu of stacked sheets is `SheetStack`.
8
+ * It FINISHES on itself: a `CardFoot` as its last child holds the one button that completes it — standing at the bottom at
9
+ * half / 3q / full, gone at peek. Never an ActionBar in front of a sheet (a bar is the base's). Nothing in the body commits
10
+ * on its own. Older note — its own buttons only on a
11
+ * screen with no bar (the map sheet). Called sheets: several `Sheet state="away" states="peek full"` in one Step, each with an `id` a
12
+ * button names — one on the screen at a time; never beside a pager. This is the single sheet; the menu of stacked sheets is `SheetStack`.
10
13
  */
11
14
  export function Sheet({ state = 'half', raised, handle = true, states, className, children, ...rest }) {
12
- return (_jsxs("article", { className: cx('card sheet', `sheet--${state}`, raised && 'sheet--raised', className), "data-sheet-states": states, ...rest, children: [handle !== false && _jsx("button", { className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : 'Resize' }), children] }));
15
+ return (_jsxs("article", { className: cx('card sheet', `sheet--${state}`, raised && 'sheet--raised', className), "data-sheet-states": states, inert: state === 'away' || undefined, ...rest, children: [handle !== false && _jsx("button", { className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : 'Resize' }), children] }));
13
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
@@ -13,7 +13,6 @@
13
13
  "build": "node scripts/build-icons.mjs && tsc -p tsconfig.json && node scripts/flatten-css.mjs",
14
14
  "test": "playwright test",
15
15
  "validate:inbox": "node scripts/validate-inbox.mjs",
16
- "mcp:verify": "cd mcp-server && npm run verify && npm run verify:http",
17
16
  "prepack": "npm run build"
18
17
  },
19
18
  "devDependencies": {