@usableapp/cardds 0.2.0 → 0.2.1

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,28 @@ 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), text size, and a slider
53
+ per size base. Nothing is hand-listed: add a preview file or a story and it shows.
51
54
 
52
55
  **CSS only** (no React, no build):
53
56
 
@@ -62,12 +65,7 @@ popover) but won't update its label, a stack still displays but won't open, a
62
65
  sheet renders every state but only moves when a class changes, a pin is six
63
66
  plain inputs.
64
67
 
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.
68
+ `tests/fixtures/*.html` are the pages the geometry tests measure — not a demo. Nothing here is verified by eye.
71
69
 
72
70
  ### In another project
73
71
 
@@ -83,7 +81,6 @@ import { Card, CardHead, Btn } from '@usableapp/cardds';
83
81
  import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the package
84
82
  ```
85
83
 
86
- Browse what exists at `npm run dev` → `/gallery` in this repo (every component, every story, searchable).
87
84
 
88
85
  **CSS only:** there's no build and no dependencies, so installing is cloning once, then
89
86
  copying the static files into your project — never edit the clone, never
@@ -113,13 +110,9 @@ on GitHub) — set that up as an SSH host alias if you already use a different
113
110
  account for `git@github.com` day to day, the way this machine's `~/.ssh/config`
114
111
  does it for other `everysundays` repos.
115
112
 
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.
113
+ **Record what you copied.** A vendored copy with no record of the commit it came
114
+ from can't be told apart from a fork: write the commit hash (and a sha256 per
115
+ file) beside the copy, so a hand edit or a stale copy can be caught.
123
116
 
124
117
  Working with Claude in another project? The `/cardds` skill
125
118
  (`~/.claude/skills/cardds/`) carries the install and the contract, and points
@@ -204,9 +197,10 @@ whose markup has rules, because the sheet's box is the step itself:
204
197
  …cardds controls (.field / .pin / .composer / .rows + .check / .segment /
205
198
  .chip-grid), or small content cards (.card--sm)…
206
199
  </div>
200
+ <div class="card__foot"><button class="btn btn--primary btn--block">…the one button that finishes the sheet…</button></div>
207
201
  </article>
208
202
  </section>
209
- <div class="action-bar">…one wide button…</div>
203
+ <!-- no .action-bar: a bar is the base's, never in front of a sheet -->
210
204
  ```
211
205
 
212
206
  - **`.step` needs a bounded column**: a direct child of `.screen--fill`, or
@@ -239,9 +233,13 @@ whose markup has rules, because the sheet's box is the step itself:
239
233
  - **The bar is outside the step**, after it. `.step` reads the bar's room
240
234
  from `--bar-reserve`; any parent that hosts the `.action-bar` counts
241
235
  (`<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.
236
+ - **Layers: the base, the sheet over it, the top bar** (Lh 2026-09-20). The `.action-bar` belongs to the
237
+ BASE — the move of a page of words (a pager, or one wide button). It is never in front of a sheet. A
238
+ screen with a sheet finishes ON the sheet: a `.card__foot`, the sheet's last child — ONE row, the page's
239
+ conclusion. The foot stands on the screen's bottom edge at half, 3q and full alike (a layer as tall as
240
+ the sheet, slid up by the share the sheet slid down — `sheet.css`), and is gone at peek. Nothing inside
241
+ the sheet's body commits on its own (a `Composer` there takes no `send`). A bar written beside a sheet anyway
242
+ stands down — no pointer (`actions.css`), `inert` (`cardds.js`): a net for a mistake, not a layout.
245
243
  - **Never write a sheet's height, and never a `.sheet-stage` inside a step.**
246
244
  State is a class (`sheet--peek` / `--half` / `--full`); the sheet is always
247
245
  the step's full height and only slides. `carddsSheetSet(sheet, state)` is
@@ -290,6 +288,23 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
290
288
  - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
291
289
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
292
290
  step that commits still goes back to one wide button (rule 3).
291
+ - **BaseContent that moves** (Lh 2026-09-20) — two ways, never both on one screen:
292
+ - *Pages of words:* `.step > .pages > .base-content` (one `aria-current="step"`, the rest `inert`) — every page
293
+ one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
294
+ - *Called sheets:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`), each naming a
295
+ sheet: `<button aria-controls="signin">` … `<article id="signin" class="card sheet sheet--away" inert
296
+ data-sheet-states="peek full">`. Each sheet holds that way's WHOLE form and its own submit on its foot, leading
297
+ to the next page; the base carries no control and the screen no bar. `.sheet--away` = not on the screen (slid
298
+ out, hidden once gone). Nothing waits at peek. A tap raises its sheet to full and sends any other away; the
299
+ handle takes it down to peek and never further — the words and the other buttons are back in reach (the step
300
+ publishes `--sheet-room`, the peek strip, so they scroll clear of it); the same button raises it from peek to full.
301
+ **Never a form taken apart** — one sheet per field, a "send" left on the base: a form lives whole in ONE sheet.
302
+ Fixture `tests/fixtures/calls.html`, tests `tests/calls.spec.js`, stories `Step · Walks` / `Step · Calls`.
303
+ - **A pager turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
304
+ sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
305
+ own foot finishes it. Should a pager meet a sheet anyway it stands
306
+ down while the sheet is on the screen, a peeking one included: dimmed, no pointer
307
+ (`actions.css`), `inert` (`cardds.js`).
293
308
 
294
309
  ## Files
295
310
 
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,56 @@ 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
+ /* ---- called sheets (sheet.css): a button in a step's words names a sheet with aria-controls ----
424
+ (The base offers ways in — sign in · new phone · join — each sheet holds that way's whole form and
425
+ 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
426
+ to (full) — from away, and from peek alike. The step then publishes --sheet-room, the strip the
427
+ sheet keeps at peek, so the words can scroll their buttons clear of it (step.css); the buttons say
428
+ aria-expanded. */
429
+ function carddsSheetCall(sheet) {
430
+ const step = sheet.parentElement;
431
+ step.querySelectorAll(':scope > .sheet').forEach(s => { if (s !== sheet && carddsSheetState(s) !== 'away') carddsSheetSet(s, 'away'); });
432
+ const states = carddsSheetStates(sheet);
433
+ const top = states.includes('full') ? 'full' : states[states.length - 1];
434
+ const was = carddsSheetState(sheet);
435
+ if (was !== top) carddsSheetSet(sheet, top);
436
+ if (was === 'away') sheet.querySelector(':scope > .sheet__handle')?.focus({ preventScroll: true });
437
+ }
438
+ function carddsStepSync(step) {
439
+ const callers = [...step.querySelectorAll('[aria-controls]')].filter(b => document.getElementById(b.getAttribute('aria-controls'))?.matches('.step > .sheet'));
440
+ if (!callers.length) return; // a plain step: one sheet, nothing calls it, nothing to publish
441
+ const on = [...step.querySelectorAll(':scope > .sheet')].find(s => carddsSheetState(s) !== 'away');
442
+ if (on) step.style.setProperty('--sheet-room', getComputedStyle(on).getPropertyValue('--sheet-peek')); // registered: reads back in px
443
+ else step.style.removeProperty('--sheet-room');
444
+ callers.forEach(b => b.setAttribute('aria-expanded', String(carddsSheetState(document.getElementById(b.getAttribute('aria-controls'))) !== 'away')));
445
+ }
446
+ document.addEventListener('click', e => {
447
+ const caller = e.target.closest('[aria-controls]');
448
+ const sheet = caller && document.getElementById(caller.getAttribute('aria-controls'));
449
+ if (sheet && sheet.matches('.step > .sheet')) carddsSheetCall(sheet);
450
+ });
451
+ document.addEventListener('cardds:sheet', e => { const step = e.target.closest?.('.step'); if (step) carddsStepSync(step); });
452
+
453
+ /* ---- bar guard: a bar is the base's, never in front of a sheet (actions.css, Lh 2026-09-20) ----
454
+ A sheet on the screen covers the bar and CSS takes the pointer away; `inert` takes the keyboard and
455
+ the accessibility tree too. Re-checked when a sheet changes state and when the page changes. */
456
+ function carddsPagerGuard() {
457
+ document.querySelectorAll('.action-bar').forEach((bar) => {
458
+ const host = bar.parentElement;
459
+ bar.inert = !!(host && host.querySelector('.step > .sheet:not(.sheet--away)'));
460
+ });
461
+ }
462
+ if (typeof document !== 'undefined') {
463
+ document.addEventListener('cardds:sheet', carddsPagerGuard);
464
+ const start = () => {
465
+ carddsPagerGuard();
466
+ let queued = false;
467
+ new MutationObserver(() => {
468
+ if (queued) return;
469
+ queued = true;
470
+ requestAnimationFrame(() => { queued = false; carddsPagerGuard(); });
471
+ }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
472
+ };
473
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
474
+ }
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
@@ -113,6 +126,31 @@
113
126
  cut is a share of the SHEET's height, not of the body's. */
114
127
  scroll-padding-bottom: var(--_cover);
115
128
  }
129
+ /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
130
+ A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
131
+ page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
132
+ full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
133
+ foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
134
+ slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
135
+ 3q and full alike while the sheet slides behind them. A percentage of the same height on both — not a
136
+ length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
137
+ takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
138
+ stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
139
+ One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
140
+ .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
141
+ .sheet > .card__foot {
142
+ 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 */
143
+ align-items: end;
144
+ padding: 0 var(--card-pad) var(--card-pad);
145
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
146
+ pointer-events: none;
147
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
148
+ transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s;
149
+ }
150
+ .sheet > .card__foot > * { pointer-events: auto; }
151
+ .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) */
152
+ .sheet.is-dragging > .card__foot { transition: none; }
153
+ :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); }
116
154
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
117
155
  .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
118
156
 
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
@@ -1184,6 +1203,31 @@ button { font: inherit; cursor: pointer; }
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
  }
1206
+ /* ---- the sheet's foot: the button that FINISHES the sheet, on the sheet (Lh, 2026-09-20) ----
1207
+ A screen with a sheet has no bar in front of it: the button that completes what the sheet asks — the
1208
+ page's conclusion — is a .card__foot, the sheet's last child, on the sheet's own material. The sheet is
1209
+ full height at every state, so its real bottom edge is under the cut unless it stands at full. So the
1210
+ foot is a layer as tall as the sheet, its buttons at its bottom, slid UP by the very share the sheet
1211
+ slid down (--_y, + the drag): the two cancel, and the buttons stand on the box's bottom edge at half,
1212
+ 3q and full alike while the sheet slides behind them. A percentage of the same height on both — not a
1213
+ length — so a box that resizes (the keyboard) moves them as one, with nothing to catch up. The layer
1214
+ takes no taps, its buttons do; the strip behind them is the sheet's ground, --_foot tall, and the body
1215
+ stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
1216
+ One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
1217
+ .sheet { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); }
1218
+ .sheet > .card__foot {
1219
+ 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 */
1220
+ align-items: end;
1221
+ padding: 0 var(--card-pad) var(--card-pad);
1222
+ background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
1223
+ pointer-events: none;
1224
+ translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
1225
+ transition: translate var(--motion-sheet) ease, opacity var(--motion-sheet) ease, visibility 0s;
1226
+ }
1227
+ .sheet > .card__foot > * { pointer-events: auto; }
1228
+ .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) */
1229
+ .sheet.is-dragging > .card__foot { transition: none; }
1230
+ :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); }
1187
1231
  .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
1188
1232
  .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
1189
1233
 
@@ -1375,14 +1419,16 @@ button { font: inherit; cursor: pointer; }
1375
1419
  /* ============================================================
1376
1420
  cardds/step.css — .step: one screen, one task.
1377
1421
  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.
1422
+ step asks for it (a sheet, sheet.css). The move: a page of words has
1423
+ an .action-bar (actions.css) OUTSIDE the step, below it — the base's;
1424
+ a step with a sheet has none: the sheet's own .card__foot finishes it
1425
+ (see "LAYERS" below).
1380
1426
 
1381
1427
  <section class="step">
1382
1428
  <section class="base-content">…the words…</section> ← first
1383
- <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1429
+ <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
1384
1430
  </section>
1385
- <div class="action-bar">…</div>
1431
+ <div class="action-bar">…</div> ← only when there is no sheet
1386
1432
 
1387
1433
  The step is the box the sheet slides in. It fills the screen
1388
1434
  column between the top bar's reserve (.screen already pads for
@@ -1432,6 +1478,10 @@ button { font: inherit; cursor: pointer; }
1432
1478
  bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
1433
1479
  frame — the bar is fixed to the viewport wherever it sits. */
1434
1480
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1481
+ /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1482
+ move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1483
+ (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. Should a bar be written
1484
+ beside a sheet anyway it stands down (actions.css + cardds.js) — a net for a mistake, not a layout. */
1435
1485
 
1436
1486
  /* the reading block: a .base-content (base.css) — the same words-on-the-base
1437
1487
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
@@ -1441,14 +1491,23 @@ button { font: inherit; cursor: pointer; }
1441
1491
  gets the flex rule: it hugs its content, and when the column runs out (a
1442
1492
  short phone, large text) it shrinks and scrolls inside itself instead of
1443
1493
  pushing the bar or the sheet */
1444
- .step > .base-content {
1494
+ .step > .base-content, .step > .pages > .base-content {
1445
1495
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1446
1496
  --card-gap: var(--sp-4);
1447
1497
  gap: var(--card-gap);
1448
1498
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
1449
1499
  align-content: start;
1450
1500
  color: var(--on-surface);
1501
+ }
1502
+ /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1503
+ (card.css). --sheet-room: the strip a CALLED sheet keeps at peek (sheet.css, "called sheets"), published
1504
+ on the step by cardds.js while one is on the screen, so the buttons under it can scroll clear of it;
1505
+ nothing without one. */
1506
+ .step > :is(.base-content, .pages) {
1507
+ padding-bottom: var(--sheet-room, 0rem);
1451
1508
  flex: 0 1 auto;
1509
+ }
1510
+ .step > :is(.base-content, .pages) {
1452
1511
  min-height: 0;
1453
1512
  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
1513
  overflow-y: auto;
@@ -1820,6 +1879,14 @@ button { font: inherit; cursor: pointer; }
1820
1879
  <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1821
1880
  </div> — or the same two in an .action-bar__tier--pager under a control */
1822
1881
  .action-bar--pager { align-items: center; }
1882
+ /* The bar belongs to the BASE (Lh, 2026-09-20): it is the move of a page of words — a pager turning
1883
+ them, or one wide button. It is never in front of a sheet: a sheet that is on the screen covers it
1884
+ (step.css, "layers"), a peeking one included, and the button that finishes a sheet is the sheet's own
1885
+ foot (sheet.css). So a pager never works beside a sheet — that screen has work that cannot be walked
1886
+ past — and neither does any other bar: while a sheet is on (only .sheet--away, a sheet not called yet,
1887
+ is not), the bar takes no pointer here and cardds.js makes it inert, so the keyboard cannot reach what
1888
+ the eye cannot see. */
1889
+ :has(.step > .sheet:not(.sheet--away)) > .action-bar { pointer-events: none; }
1823
1890
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1824
1891
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1825
1892
  .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.1",
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": {