@usableapp/cardds 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Card-first mobile design system, **React-first**: `src/` is the component
4
4
  library — one thin component per pattern, emitting exactly the markup the CSS
5
- documents — and `app/` is the Commons Time Bank PWA built from it (the twelve
6
- screens of the Claude Design handoff, as real routes). The CSS (`css/*.css`)
5
+ documents. The CSS (`css/*.css`)
7
6
  stays the only truth: a component never styles anything, it only picks classes
8
7
  from props. The CSS also works alone, with no build, for a host that wants
9
- class names rather than components; `demo/` keeps the CSS spec pages the
10
- acceptance tests drive.
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).
11
11
 
12
12
  Repo: **https://github.com/everysundays/cardds** (private — this is the
13
13
  source of truth; every consuming project vendors a copy from here, never
@@ -23,10 +23,9 @@ licences (`fonts/LICENSES.md`).
23
23
 
24
24
  ```bash
25
25
  npm install
26
- npm run dev # the app (app/) on http://localhost:5174 — the twelve screens, /screens lists them
26
+ npm run dev # the gallery on http://localhost:5174 — every story, searchable
27
27
  npm run build # the library: icons → src/type/icons.ts · tsc → dist/ (ESM + .d.ts) · dist/cardds.css (flattened)
28
- npm run build:app # the PWA → app/dist (manifest, service worker, icons)
29
- npm test # Playwright: the step/sheet/centre geometry on demo/, the app's screens and the four handoff upgrades
28
+ npm test # Playwright: the step/sheet/centre geometry on tests/fixtures/, the app's screens and the four handoff upgrades
30
29
  ```
31
30
 
32
31
  ```tsx
@@ -42,12 +41,13 @@ the code), a card heading that needs more than two lines squeezing its type
42
41
  (see *Anatomy*), and the `.dropdown` writing its picked value back. `Icon`
43
42
  inlines the 56 symbols of `icons.svg` at build time, so no sprite file ships.
44
43
 
45
- **The app** (`app/`): Vite + React Router, `cardds` aliased to `src/` so the
46
- app and the package share one truth with no build between them. Twelve
47
- screens from `docs/handoff/commons-time-bank-SPEC.md` — welcome, install (the
48
- real `beforeinstallprompt`), verify phone, home, browse, detail, matches,
49
- empty, the two menus (sheet stacks), post, profile. `app/README.md` records
50
- what the port changed against the mockups and why.
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.)
51
51
 
52
52
  **CSS only** (no React, no build):
53
53
 
@@ -62,14 +62,12 @@ popover) but won't update its label, a stack still displays but won't open, a
62
62
  sheet renders every state but only moves when a class changes, a pin is six
63
63
  plain inputs.
64
64
 
65
- `demo/index.html` is the CSS spec — every component in one page, palette
66
- switcher top-right. `demo/sheet.html` — the single sheet at phone height: a step
67
- that reads, a step that acts, the overlay stage; `demo/stack.html` — the sheet
68
- stack: the menu and the display variant; `demo/elements.html` — in-card
69
- elements and layovers grown from the timebanking board. Serve the repo root
70
- (`python3 -m http.server 4174`) and open `/demo/`. These pages are the fixtures
71
- of `tests/step.spec.js` and `tests/centre.spec.js`; `tests/app.spec.js` drives
72
- the app. Nothing here is verified by eye.
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.
73
71
 
74
72
  ### In another project
75
73
 
@@ -187,19 +185,19 @@ A step is one screen, one task (see *Anatomy*). It is the other component
187
185
  whose markup has rules, because the sheet's box is the step itself:
188
186
 
189
187
  ```html
190
- <!-- reads: card only -->
188
+ <!-- reads: the words only -->
191
189
  <section class="step">
192
- <article class="card step__card">
193
- <div class="card__head"><div><span class="t-overline t-muted">ขั้นที่ 2 จาก 4</span><h2 class="t-h2">…</h2></div></div>
190
+ <article class="step__card">
191
+ <div class="card__head"><span class="t-overline t-muted">ขั้นที่ 2 จาก 4</span><h2 class="t-h2">…</h2></div>
194
192
  <p class="t-body">…</p>
195
193
  </article>
196
194
  </section>
197
195
  <div class="action-bar action-bar--pager">…</div>
198
196
 
199
- <!-- acts: card + sheet -->
197
+ <!-- acts: the words + a sheet -->
200
198
  <section class="step">
201
- <article class="card step__card">…the words…</article>
202
- <article class="card sheet sheet--half" data-sheet-states="peek half full">
199
+ <article class="step__card">…the words…</article>
200
+ <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
203
201
  <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
204
202
  <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
205
203
  <div class="sheet__body">
@@ -221,8 +219,10 @@ whose markup has rules, because the sheet's box is the step itself:
221
219
  card takes the inset back as its side margins — so the card and the bar's
222
220
  buttons stand on the inset line, and the sheet runs edge to edge like the
223
221
  bar and like a sheet in a `.sheet-stage`.
224
- - **`.step__card` first, `.sheet` last.** The card carries the step's words
225
- and is a normal card: head · content · optional foot. The sheet is
222
+ - **`.step__card` first, `.sheet` last.** The block carries the step's words
223
+ ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
224
+ Lh: a step reads like a page, not a card in a page). It keeps a card's
225
+ rhythm: a `.card__head`, `--card-gap` between things. The sheet is
226
226
  **handle · head · body**: the handle, then a `.card__head` title row (that
227
227
  row is what `peek` shows — heading left, the contextual thing right), then
228
228
  `.sheet__body` with what the member acts on. The handle and the head are
@@ -311,12 +311,12 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
311
311
  | `css/choice.css` | `.check`, `.toggle`, `.chip--toggle`, `.chip-grid` + `.chip--pick`, `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
312
312
  | `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.timeline`, `.legend`, `.link` | lists inside cards |
313
313
  | `css/media.css` | `.card--cover`, `.quote`, `.tile-grid` + `.tile`, `.card--fold`, `.mosaic`, `.wave` | media & display |
314
- | `css/layover.css` | `.drawer` (ask), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.chip--bubble`, `.deck` | cards over content |
314
+ | `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.chip--bubble`, `.deck` | cards over content |
315
315
  | `icons.svg` | Lucide sprite (ISC), 56 minimal stroke icons | add symbols as needed |
316
316
  | `src/` → `dist/` | the React library: `src/<group>/<Name>.tsx`, one thin component per pattern, JSDoc = the rule; built by `npm run build` (tsc) into `dist/` with `.d.ts`; `src/type/icons.ts` is generated from `icons.svg` | when adding a component |
317
- | `app/` | the Commons Time Bank PWA: `app/src/screens/*.tsx` (the twelve screens), `App.tsx` (the routes), `install.ts` (the install prompt), `public/` (manifest, `sw.js`, icons); `npm run dev` / `build:app` | the product |
318
- | `demo/` | the CSS spec pages (`index`, `sheet`, `stack`, `elements`) — CSS only, and the fixtures of the geometry tests | when the CSS contract changes |
319
- | `docs/handoff/` | the Claude Design handoff spec the app was ported from | reference |
317
+ | `gallery/` | the gallery site: `npm run dev` → `/` every story, `/:Name` one component (`gallery/src/Gallery.tsx` globs `.design-sync/previews/`) | the demo |
318
+ | `tests/fixtures/` | the HTML pages (`index`, `sheet`, `stack`, `elements`) the geometry tests measure — ex `demo/`, not a demo | when a geometry test needs a new case |
319
+ | `docs/handoff/` | the Claude Design handoff spec (the Commons Time Bank app built from it was dropped 2026-09-16) | reference |
320
320
  | `scripts/` | `build-icons.mjs` (icons.svg → `src/type/icons.ts`), `flatten-css.mjs` (`cardds.css` + imports → `dist/cardds.css`) | rarely |
321
321
  | `docs/guides/` | `building-with-cardds.md` — the anatomy, the step, the two sheet systems and the vocabulary in React terms; shipped to Claude Design as its guidelines | when the contract changes |
322
322
  | `cardds.js` | the behaviours (delegated, class/property toggles only — everything renders without it): dropdown value sync, `.sheet-stack--tap` open/close (tolerates one wrapper per sheet; close reads the tapped sheet), `.pin` auto-advance / Backspace / paste + `cardds:pin`, the sheet handle (tap / drag-snap / Enter / Space), the focus and keyboard raises (`--kb`), `cardds:sheet` | rarely |
@@ -356,7 +356,7 @@ Every screen is the same four things, and every card the same three:
356
356
  - **Two sheet systems, one material.** *Sheet single* (`.sheet`, `sheet.css`)
357
357
  is ONE sheet the member acts in, whatever it holds: a short entry in front
358
358
  of a step's card, a place on a map, a payment on a dim page — in a `.step`
359
- or a `.sheet-stage`, and its state is how far it slid (peek / half / full).
359
+ or a `.sheet-stage`, and its state is how far it slid (peek / half / 3q / full).
360
360
  *Sheet stack* (`.sheet-stack`, `stack.css`) is MANY sheets, each a main
361
361
  section of the app, stacked like paper at the middle of the home screen:
362
362
  the menu. Heads peek, a tap brings one sheet to the front, back returns it;
@@ -365,8 +365,8 @@ Every screen is the same four things, and every card the same three:
365
365
  sheet is `.card.sheet` — and both obey rule 8: rigid, full height, moved
366
366
  by `translate` only. (The stack was first named "card stack": wrong, and
367
367
  retired.) Demos: `sheet.html`, `stack.html`.
368
- - **A step is one screen, one task: `.step`.** What the member reads is a
369
- card (`.step__card`); what the member does, if the step asks for it, is a
368
+ - **A step is one screen, one task: `.step`.** What the member reads is
369
+ words on the base (`.step__card` — the name stayed, the frame went); what the member does, if the step asks for it, is a
370
370
  `.sheet` in front of it; the screen's one move is `.action-bar`, outside
371
371
  the step, below it. A step where the member only READS is the card alone.
372
372
  A step where the member ACTS (types, picks, shoots a photo, records) adds
@@ -469,9 +469,12 @@ Demo: the *skeleton* block at the top of `elements.html`.
469
469
  8. **A single sheet is rigid material, like a sheet in the stack.** It is always the full
470
470
  height of the box that holds it (`inset: 0`, set once, never written
471
471
  again) and a state is nothing but how far that full card slid:
472
- `sheet--full` slid nowhere, `sheet--half` slid down by `--sheet-half`
473
- (50% of itself), `sheet--peek` slid down until only `--sheet-peek` shows —
474
- the handle and the title row. `translate` is the only animated property
472
+ `sheet--full` slid nowhere, `sheet--3q` slid down by `--sheet-3q` (a
473
+ quarter of itself — three quarters show), `sheet--half` by `--sheet-half`
474
+ (50%), `sheet--peek` slid down until only `--sheet-peek` shows — the
475
+ handle and the title row. A tap on the handle toggles between the sheet's
476
+ own state and peek (never a step-by-step climb, Lh 2026-09-16); a drag
477
+ snaps to any listed state, and where it lands is what a tap returns to. `translate` is the only animated property
475
478
  (`--motion-sheet`), and nothing toggles height, display or z-index for a
476
479
  state — the stack's rule, inherited. The box clips whatever slid below its
477
480
  bottom edge, so a sheet at any height is still a card: rounded top, a
@@ -484,7 +487,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
484
487
  padding. Two boxes hold sheets and the states
485
488
  mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
486
489
  and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
487
- sheet `--dim` + `--raised`, the ask drawer `--ask` + `.lift` + `.drawer`).
490
+ sheet `--dim` + `--raised`, the ask `--ask` + `.modal` = `__lift` + `__drawer`).
488
491
  A stage keeps the top bar's line free (`--_top`), so `--full` stops under
489
492
  `--header-h` and keeps its corners. Both sheet tokens are published — a
490
493
  sheet with a taller head retunes `--sheet-peek` — and registered in
package/cardds.js CHANGED
@@ -177,8 +177,12 @@ function carddsCloseStack(stack, card) {
177
177
  without it; this adds the handle's click and drag, the focus raise, the
178
178
  keyboard room, and one event. Delegated: any sheet on the page, no ids.
179
179
 
180
- - click / Enter / Space on .sheet__handle cycles the states named in
181
- data-sheet-states (default "peek half full"), wrapping round
180
+ - click / Enter / Space on .sheet__handle TOGGLES the sheet between its
181
+ own state and peek (Lh 2026-09-16 — never a step-by-step climb). "Its
182
+ own state" is the last non-peek state it rested in (authored, dragged
183
+ to, or set), kept in data-sheet-home
184
+ - data-sheet-states (default "peek half 3q full") lists the states a
185
+ drag snaps to
182
186
  - drag on the handle writes the offset to --_drag (the CSS adds it to the
183
187
  state's translate) and on release snaps to the nearest state; a move
184
188
  shorter than --sp-2 is a tap. The snap is the one place the sheet's
@@ -194,7 +198,7 @@ function carddsCloseStack(stack, card) {
194
198
 
195
199
  carddsSheetSet(sheet, state) is the one entry point; an app that sets
196
200
  the state itself calls it rather than swapping classes, so listeners hear. */
197
- const CARDDS_SHEET_STATES = ['peek', 'half', 'full'];
201
+ const CARDDS_SHEET_STATES = ['peek', 'half', '3q', 'full'];
198
202
  const CARDDS_SHEET_TAP_REM = 0.5; /* = --sp-2: a move shorter than this is a tap, not a drag */
199
203
 
200
204
  function carddsSheetStates(sheet) {
@@ -210,6 +214,10 @@ function carddsSheetSet(sheet, state) {
210
214
  if (!CARDDS_SHEET_STATES.includes(state)) return;
211
215
  const was = carddsSheetState(sheet);
212
216
  CARDDS_SHEET_STATES.forEach(s => sheet.classList.toggle('sheet--' + s, s === state));
217
+ /* where a tap from peek returns to: the non-peek state it is going to, or the
218
+ one it is leaving (an authored state never passed through here before) */
219
+ const home = state !== 'peek' ? state : was !== 'peek' ? was : null;
220
+ if (home) sheet.dataset.sheetHome = home;
213
221
  if (was !== state) sheet.dispatchEvent(new CustomEvent('cardds:sheet', { bubbles: true, detail: { state } }));
214
222
  }
215
223
 
@@ -224,10 +232,19 @@ document.addEventListener('click', e => {
224
232
  if (handle === carddsSheetSkipClick) { carddsSheetSkipClick = null; return; }
225
233
  const sheet = handle.closest('.sheet');
226
234
  if (!sheet) return;
227
- const states = carddsSheetStates(sheet);
228
- carddsSheetSet(sheet, states[(states.indexOf(carddsSheetState(sheet)) + 1) % states.length]);
235
+ carddsSheetSet(sheet, carddsSheetState(sheet) === 'peek' ? carddsSheetHome(sheet) : 'peek');
229
236
  });
230
237
 
238
+ /* the state a tap from peek returns to: the last non-peek state it rested in,
239
+ else the authored one, else the first non-peek state it snaps to */
240
+ function carddsSheetHome(sheet) {
241
+ const states = carddsSheetStates(sheet);
242
+ const home = sheet.dataset.sheetHome;
243
+ if (home && states.includes(home)) return home;
244
+ const now = carddsSheetState(sheet);
245
+ return now !== 'peek' ? now : states.find(s => s !== 'peek') ?? 'half';
246
+ }
247
+
231
248
  /* how far the sheet has slid, in px: its rendered top against its resting top
232
249
  at full (the box's padding edge + its own offset). Read from geometry, not
233
250
  from the computed translate — that keeps percentages and calcs as text. */
@@ -243,6 +260,7 @@ function carddsSheetStops(sheet) {
243
260
  return {
244
261
  full: 0,
245
262
  half: height * (parseFloat(cs.getPropertyValue('--sheet-half')) || 0), // a share of the height
263
+ '3q': height * (parseFloat(cs.getPropertyValue('--sheet-3q')) || 0),
246
264
  peek: height - (parseFloat(cs.getPropertyValue('--sheet-peek')) || 0), // a length, resolved
247
265
  };
248
266
  }
@@ -299,18 +317,17 @@ document.addEventListener('pointerup', carddsSheetRelease);
299
317
  document.addEventListener('pointercancel', carddsSheetRelease);
300
318
 
301
319
  /* focus in a peeked sheet: a control under the cut cannot be used, so raise
302
- the sheet first — to the state after peek at least, further if that one
303
- has no room for the control under the pinned head (a small phone at a
304
- large text size). The browser's own reveal scrolled the body against the
320
+ the sheet first — to its own state (the one a tap returns to) at least,
321
+ further if that one has no room for the control under the pinned head (a
322
+ small phone at a large text size). The browser's own reveal scrolled the body against the
305
323
  peek strip before this fired; start over from the top and let the reveal,
306
324
  with the raised state's scroll-padding, do the least. */
307
325
  document.addEventListener('focusin', e => {
308
326
  const sheet = e.target.closest?.('.sheet');
309
327
  if (!sheet || !sheet.classList.contains('sheet--peek') || e.target.closest('.sheet__handle')) return;
310
- const states = carddsSheetStates(sheet);
311
328
  const body = sheet.querySelector(':scope > .sheet__body');
312
329
  if (body) body.scrollTop = 0;
313
- carddsSheetReveal(e.target, states[states.indexOf('peek') + 1] ?? states[states.length - 1]);
330
+ carddsSheetReveal(e.target, carddsSheetHome(sheet));
314
331
  });
315
332
 
316
333
  /* --kb: the height of the viewport an on-screen keyboard covers. iOS keeps
@@ -377,3 +394,23 @@ document.addEventListener('scroll', e => {
377
394
  clearTimeout(carddsScrollTimers.get(t));
378
395
  carddsScrollTimers.set(t, setTimeout(() => t.classList.remove('is-scrolling'), 700));
379
396
  }, true);
397
+
398
+ /* ---- Modal (layover.css, .sheet-stage--ask): a click on any button in the
399
+ drawer closes the modal by playing its open animations backwards —
400
+ Animation.reverse() on every animation in the card, the same keyframes, no
401
+ closing keyframes to keep in step — then fires "cardds:modal" on the stage
402
+ (bubbles, detail.answer = the button's data-answer) so the app removes the
403
+ stage. Reduced motion (no animations): the event fires at once. */
404
+ document.addEventListener('click', e => {
405
+ const btn = e.target.closest('.modal__drawer button');
406
+ if (!btn) return;
407
+ const stage = btn.closest('.sheet-stage--ask');
408
+ const modal = btn.closest('.modal');
409
+ if (!stage || !modal || stage.classList.contains('is-closing')) return;
410
+ stage.classList.add('is-closing');
411
+ const anims = modal.getAnimations({ subtree: true });
412
+ anims.forEach(a => a.reverse());
413
+ Promise.all(anims.map(a => a.finished)).then(() => {
414
+ stage.dispatchEvent(new CustomEvent('cardds:modal', { bubbles: true, detail: { answer: btn.dataset.answer ?? null } }));
415
+ });
416
+ });
package/css/layover.css CHANGED
@@ -1,48 +1,93 @@
1
1
  /* ============================================================
2
2
  cardds/layover.css — cards that lay OVER other content:
3
- ask drawer · popover menu · expanding banner · float bar ·
3
+ Modal (ask) · popover menu · expanding banner · float bar ·
4
4
  callout · swipe deck. The sheet (peek / half / full / page)
5
5
  lives in sheet.css; these compose with .sheet-stage.
6
6
  ============================================================ */
7
7
 
8
- /* ---- ask drawer: the content card lifts, a dark drawer asks ----
8
+ /* ---- ask: the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
9
9
  <div class="sheet-stage sheet-stage--ask">
10
- <article class="card lift">…page content…</article>
11
- <div class="drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
12
- <div class="btn-trio"><button class="icon-btn">×</button><span class="btn-trio__dots"></span><button class="icon-btn icon-btn--invert">?</button><span class="btn-trio__dots"></span><button class="icon-btn">✓</button></div>
13
- </div></div> */
10
+ <article class="card modal">
11
+ <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
12
+ <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
13
+ <div class="btn-trio">…</div>
14
+ </div>
15
+ </article>
16
+ </div>
17
+ .modal is the card — no border (the shadow is its edge on the scrim);
18
+ .modal__lift the light part that holds the thing being asked about (it
19
+ scrolls when tall); .modal__drawer the dark end that asks. The ground is
20
+ the page under a scrim (like --dim). Two beats (2 × --motion-sheet) when
21
+ the stage appears, and the same two backwards when it closes:
22
+ 1. the card drops in from above the stage to the screen's centre — its
23
+ drawer still inside it, height 0: nothing of the ask shows yet
24
+ 2. the drawer comes out of the card's bottom edge while the lift moves
25
+ up, both at once: the card grows from its centre, which never moves
26
+ (the stage centres it; a growing card keeps its middle where it is)
27
+ Closing: a click on any button in the drawer plays the same animations in
28
+ reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
29
+ fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
30
+ button's data-answer) for the app to remove the stage.
31
+ Reduced motion: the finished state, no beats; closing fires at once. */
14
32
  .sheet-stage--ask {
15
33
  --drawer-h: 14rem;
16
- background: var(--card-3-bg);
17
- color: var(--card-3-ink);
34
+ display: grid;
35
+ align-content: center; /* the card at the screen's centre — and kept there as it grows */
36
+ padding-inline: var(--screen-pad);
18
37
  }
19
- .lift {
20
- position: absolute;
21
- inset: 0 0 var(--drawer-h) 0;
22
- border: 0;
23
- border-radius: 0 0 var(--r-card) var(--r-card);
24
- overflow: hidden;
25
- overscroll-behavior: contain;
38
+ .sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
39
+ .sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
40
+ .modal {
41
+ position: relative;
42
+ z-index: 1; /* over the scrim */
43
+ max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) — a % here would be of the grid area, which is the card itself */
44
+ padding: 0; /* the parts pad themselves */
45
+ border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
46
+ display: grid;
47
+ grid-template-rows: minmax(0, 1fr) auto;
48
+ overflow: clip;
26
49
  box-shadow: var(--shadow-sheet);
50
+ }
51
+ .modal__lift {
52
+ padding: var(--card-pad);
53
+ display: grid;
54
+ gap: var(--card-gap);
27
55
  align-content: start;
56
+ min-height: 0;
57
+ overflow-y: auto; /* tall content scrolls inside; the drawer stays */
58
+ overscroll-behavior: contain;
28
59
  }
29
- .drawer {
30
- position: absolute;
31
- inset-inline: 0;
32
- bottom: 0;
60
+ .modal__drawer {
33
61
  height: var(--drawer-h);
62
+ box-sizing: border-box;
34
63
  padding: var(--sp-5);
64
+ overflow: clip;
35
65
  display: grid;
36
66
  align-content: center;
37
67
  justify-items: center;
38
68
  gap: var(--sp-1);
39
69
  text-align: center;
70
+ background: var(--card-3-bg);
71
+ color: var(--card-3-ink);
40
72
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
41
73
  --on-accent: var(--card-3-bg);
42
74
  }
43
- .drawer .t-caption { opacity: 0.7; }
44
- .drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
45
- .drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
75
+ .modal__drawer .t-caption { opacity: 0.7; }
76
+ .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
77
+ .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
78
+ @media (prefers-reduced-motion: no-preference) {
79
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
80
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
81
+ @keyframes modal-drop {
82
+ 0% { translate: 0 -100cqh; } /* above the stage */
83
+ 50%, 100% { translate: 0 0; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
84
+ }
85
+ @keyframes modal-open {
86
+ 0%, 50% { height: 0; padding-block: 0; } /* inside the card */
87
+ 100% { height: var(--drawer-h); padding-block: var(--sp-5); } /* 2. out below, the lift up, together */
88
+ }
89
+ }
90
+
46
91
  .btn-trio { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
47
92
  .btn-trio__dots {
48
93
  width: var(--sp-5);
package/css/sheet.css CHANGED
@@ -1,14 +1,14 @@
1
1
  /* ============================================================
2
2
  cardds/sheet.css — sheet single: one card as a bottom sheet.
3
- Three states: .sheet--peek · .sheet--half · .sheet--full
3
+ Four states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full
4
4
 
5
5
  A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
6
6
  (stack.css): it is ALWAYS the full height of the box that holds it —
7
7
  set once by inset: 0, never written again, never transitioned — and
8
8
  a state is nothing but how far that full card has slid up. Peek is
9
9
  a full sheet that slid down until only its handle and title show;
10
- half is one that slid down by half of itself; full is one that has
11
- not slid at all. The box clips whatever slid below its bottom edge,
10
+ half is one that slid down by half of itself; 3q (three quarters,
11
+ Lh 2026-09-16) by a quarter; full is one that has not slid at all. The box clips whatever slid below its bottom edge,
12
12
  so a sheet at any height is still a card: rounded top, a straight
13
13
  cut at the box's edge, its content scrolling inside.
14
14
 
@@ -28,7 +28,11 @@
28
28
  page. Many sheets stacked as the app's menu are the other system,
29
29
  .sheet-stack (stack.css): the same material, its own class and file.
30
30
 
31
- <article class="card sheet sheet--half" data-sheet-states="peek half full">
31
+ <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
32
+ A tap on the handle TOGGLES the sheet between its own state and peek
33
+ (Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
34
+ the listed states, and the state it lands on becomes the one a tap
35
+ returns to.
32
36
  <button class="sheet__handle" type="button" aria-label="…"></button>
33
37
  <div class="card__head"><h2 class="t-h2">…</h2>…</div>
34
38
  <div class="sheet__body">…controls, or small content cards…</div>
@@ -51,6 +55,7 @@
51
55
  retune it: .my-sheet { --sheet-peek: … } */
52
56
  --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
53
57
  --sheet-half: 0.5; /* a share of the sheet's own height */
58
+ --sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
54
59
  /* the sheet's own height, seen from inside it: the box it slides in is the
55
60
  size container (.step, .sheet-stage), so 100cqh is that box, less what
56
61
  the sheet gives up at its top (--_top) and bottom (--_lift). The sheet
@@ -81,6 +86,7 @@
81
86
  }
82
87
  .sheet--full { --_y: 0%; --_cover: 0rem; }
83
88
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
89
+ .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
84
90
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
85
91
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
86
92
 
package/css/step.css CHANGED
@@ -1,11 +1,11 @@
1
1
  /* ============================================================
2
2
  cardds/step.css — .step: one screen, one task.
3
- What the member reads (a card), what the member does when the
3
+ What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
4
4
  step asks for it (a sheet, sheet.css), and the screen's one
5
5
  move (.action-bar, actions.css) OUTSIDE the step, below it.
6
6
 
7
7
  <section class="step">
8
- <article class="card step__card">…the words…</article> ← first
8
+ <article class="step__card">…the words…</article> ← first
9
9
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
10
10
  </section>
11
11
  <div class="action-bar">…</div>
@@ -59,10 +59,19 @@
59
59
  frame — the bar is fixed to the viewport wherever it sits. */
60
60
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
61
61
 
62
- /* the reading card: a plain card with a flex rule — it hugs its content,
63
- and when the column runs out (a short phone, large text) it shrinks and
64
- scrolls inside itself instead of pushing the bar or the sheet */
62
+ /* the reading block: the words on the base — no frame, no area colour (it was
63
+ a .card until 2026-09-16; Lh: a step reads like a page, not a card in a
64
+ page). It keeps a card's inner rhythm (a head, --card-gap between things,
65
+ the tokens .card publishes for the head and .bleed) and the flex rule: it
66
+ hugs its content, and when the column runs out (a short phone, large text)
67
+ it shrinks and scrolls inside itself instead of pushing the bar or the sheet */
65
68
  .step__card {
69
+ --card-pad: 0rem; /* published for .bleed: nothing to cancel */
70
+ --card-gap: var(--sp-4);
71
+ display: grid;
72
+ gap: var(--card-gap);
73
+ align-content: start;
74
+ color: var(--on-surface);
66
75
  flex: 0 1 auto;
67
76
  min-height: 0;
68
77
  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 */
package/css/tokens.css CHANGED
@@ -176,3 +176,4 @@
176
176
  ============================================================ */
177
177
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
178
178
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
179
+ @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
package/dist/cardds.css CHANGED
@@ -217,6 +217,7 @@
217
217
  ============================================================ */
218
218
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
219
219
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
220
+ @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
220
221
 
221
222
  /* ---- css/palette.css — color settings — the file you tune ---- */
222
223
  /* ============================================================
@@ -1020,15 +1021,15 @@ button { font: inherit; cursor: pointer; }
1020
1021
  /* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
1021
1022
  /* ============================================================
1022
1023
  cardds/sheet.css — sheet single: one card as a bottom sheet.
1023
- Three states: .sheet--peek · .sheet--half · .sheet--full
1024
+ Four states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full
1024
1025
 
1025
1026
  A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
1026
1027
  (stack.css): it is ALWAYS the full height of the box that holds it —
1027
1028
  set once by inset: 0, never written again, never transitioned — and
1028
1029
  a state is nothing but how far that full card has slid up. Peek is
1029
1030
  a full sheet that slid down until only its handle and title show;
1030
- half is one that slid down by half of itself; full is one that has
1031
- not slid at all. The box clips whatever slid below its bottom edge,
1031
+ half is one that slid down by half of itself; 3q (three quarters,
1032
+ Lh 2026-09-16) by a quarter; full is one that has not slid at all. The box clips whatever slid below its bottom edge,
1032
1033
  so a sheet at any height is still a card: rounded top, a straight
1033
1034
  cut at the box's edge, its content scrolling inside.
1034
1035
 
@@ -1048,7 +1049,11 @@ button { font: inherit; cursor: pointer; }
1048
1049
  page. Many sheets stacked as the app's menu are the other system,
1049
1050
  .sheet-stack (stack.css): the same material, its own class and file.
1050
1051
 
1051
- <article class="card sheet sheet--half" data-sheet-states="peek half full">
1052
+ <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
1053
+ A tap on the handle TOGGLES the sheet between its own state and peek
1054
+ (Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
1055
+ the listed states, and the state it lands on becomes the one a tap
1056
+ returns to.
1052
1057
  <button class="sheet__handle" type="button" aria-label="…"></button>
1053
1058
  <div class="card__head"><h2 class="t-h2">…</h2>…</div>
1054
1059
  <div class="sheet__body">…controls, or small content cards…</div>
@@ -1071,6 +1076,7 @@ button { font: inherit; cursor: pointer; }
1071
1076
  retune it: .my-sheet { --sheet-peek: … } */
1072
1077
  --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
1073
1078
  --sheet-half: 0.5; /* a share of the sheet's own height */
1079
+ --sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
1074
1080
  /* the sheet's own height, seen from inside it: the box it slides in is the
1075
1081
  size container (.step, .sheet-stage), so 100cqh is that box, less what
1076
1082
  the sheet gives up at its top (--_top) and bottom (--_lift). The sheet
@@ -1101,6 +1107,7 @@ button { font: inherit; cursor: pointer; }
1101
1107
  }
1102
1108
  .sheet--full { --_y: 0%; --_cover: 0rem; }
1103
1109
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1110
+ .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1104
1111
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1105
1112
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1106
1113
 
@@ -1317,12 +1324,12 @@ button { font: inherit; cursor: pointer; }
1317
1324
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
1318
1325
  /* ============================================================
1319
1326
  cardds/step.css — .step: one screen, one task.
1320
- What the member reads (a card), what the member does when the
1327
+ What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
1321
1328
  step asks for it (a sheet, sheet.css), and the screen's one
1322
1329
  move (.action-bar, actions.css) OUTSIDE the step, below it.
1323
1330
 
1324
1331
  <section class="step">
1325
- <article class="card step__card">…the words…</article> ← first
1332
+ <article class="step__card">…the words…</article> ← first
1326
1333
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1327
1334
  </section>
1328
1335
  <div class="action-bar">…</div>
@@ -1376,10 +1383,19 @@ button { font: inherit; cursor: pointer; }
1376
1383
  frame — the bar is fixed to the viewport wherever it sits. */
1377
1384
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1378
1385
 
1379
- /* the reading card: a plain card with a flex rule — it hugs its content,
1380
- and when the column runs out (a short phone, large text) it shrinks and
1381
- scrolls inside itself instead of pushing the bar or the sheet */
1386
+ /* the reading block: the words on the base — no frame, no area colour (it was
1387
+ a .card until 2026-09-16; Lh: a step reads like a page, not a card in a
1388
+ page). It keeps a card's inner rhythm (a head, --card-gap between things,
1389
+ the tokens .card publishes for the head and .bleed) and the flex rule: it
1390
+ hugs its content, and when the column runs out (a short phone, large text)
1391
+ it shrinks and scrolls inside itself instead of pushing the bar or the sheet */
1382
1392
  .step__card {
1393
+ --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1394
+ --card-gap: var(--sp-4);
1395
+ display: grid;
1396
+ gap: var(--card-gap);
1397
+ align-content: start;
1398
+ color: var(--on-surface);
1383
1399
  flex: 0 1 auto;
1384
1400
  min-height: 0;
1385
1401
  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 */
@@ -2834,49 +2850,94 @@ button { font: inherit; cursor: pointer; }
2834
2850
  /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout, deck ---- */
2835
2851
  /* ============================================================
2836
2852
  cardds/layover.css — cards that lay OVER other content:
2837
- ask drawer · popover menu · expanding banner · float bar ·
2853
+ Modal (ask) · popover menu · expanding banner · float bar ·
2838
2854
  callout · swipe deck. The sheet (peek / half / full / page)
2839
2855
  lives in sheet.css; these compose with .sheet-stage.
2840
2856
  ============================================================ */
2841
2857
 
2842
- /* ---- ask drawer: the content card lifts, a dark drawer asks ----
2858
+ /* ---- ask: the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
2843
2859
  <div class="sheet-stage sheet-stage--ask">
2844
- <article class="card lift">…page content…</article>
2845
- <div class="drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
2846
- <div class="btn-trio"><button class="icon-btn">×</button><span class="btn-trio__dots"></span><button class="icon-btn icon-btn--invert">?</button><span class="btn-trio__dots"></span><button class="icon-btn">✓</button></div>
2847
- </div></div> */
2860
+ <article class="card modal">
2861
+ <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
2862
+ <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
2863
+ <div class="btn-trio">…</div>
2864
+ </div>
2865
+ </article>
2866
+ </div>
2867
+ .modal is the card — no border (the shadow is its edge on the scrim);
2868
+ .modal__lift the light part that holds the thing being asked about (it
2869
+ scrolls when tall); .modal__drawer the dark end that asks. The ground is
2870
+ the page under a scrim (like --dim). Two beats (2 × --motion-sheet) when
2871
+ the stage appears, and the same two backwards when it closes:
2872
+ 1. the card drops in from above the stage to the screen's centre — its
2873
+ drawer still inside it, height 0: nothing of the ask shows yet
2874
+ 2. the drawer comes out of the card's bottom edge while the lift moves
2875
+ up, both at once: the card grows from its centre, which never moves
2876
+ (the stage centres it; a growing card keeps its middle where it is)
2877
+ Closing: a click on any button in the drawer plays the same animations in
2878
+ reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
2879
+ fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
2880
+ button's data-answer) for the app to remove the stage.
2881
+ Reduced motion: the finished state, no beats; closing fires at once. */
2848
2882
  .sheet-stage--ask {
2849
2883
  --drawer-h: 14rem;
2850
- background: var(--card-3-bg);
2851
- color: var(--card-3-ink);
2884
+ display: grid;
2885
+ align-content: center; /* the card at the screen's centre — and kept there as it grows */
2886
+ padding-inline: var(--screen-pad);
2852
2887
  }
2853
- .lift {
2854
- position: absolute;
2855
- inset: 0 0 var(--drawer-h) 0;
2856
- border: 0;
2857
- border-radius: 0 0 var(--r-card) var(--r-card);
2858
- overflow: hidden;
2859
- overscroll-behavior: contain;
2888
+ .sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
2889
+ .sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
2890
+ .modal {
2891
+ position: relative;
2892
+ z-index: 1; /* over the scrim */
2893
+ max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) — a % here would be of the grid area, which is the card itself */
2894
+ padding: 0; /* the parts pad themselves */
2895
+ border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
2896
+ display: grid;
2897
+ grid-template-rows: minmax(0, 1fr) auto;
2898
+ overflow: clip;
2860
2899
  box-shadow: var(--shadow-sheet);
2900
+ }
2901
+ .modal__lift {
2902
+ padding: var(--card-pad);
2903
+ display: grid;
2904
+ gap: var(--card-gap);
2861
2905
  align-content: start;
2906
+ min-height: 0;
2907
+ overflow-y: auto; /* tall content scrolls inside; the drawer stays */
2908
+ overscroll-behavior: contain;
2862
2909
  }
2863
- .drawer {
2864
- position: absolute;
2865
- inset-inline: 0;
2866
- bottom: 0;
2910
+ .modal__drawer {
2867
2911
  height: var(--drawer-h);
2912
+ box-sizing: border-box;
2868
2913
  padding: var(--sp-5);
2914
+ overflow: clip;
2869
2915
  display: grid;
2870
2916
  align-content: center;
2871
2917
  justify-items: center;
2872
2918
  gap: var(--sp-1);
2873
2919
  text-align: center;
2920
+ background: var(--card-3-bg);
2921
+ color: var(--card-3-ink);
2874
2922
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
2875
2923
  --on-accent: var(--card-3-bg);
2876
2924
  }
2877
- .drawer .t-caption { opacity: 0.7; }
2878
- .drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
2879
- .drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
2925
+ .modal__drawer .t-caption { opacity: 0.7; }
2926
+ .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
2927
+ .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
2928
+ @media (prefers-reduced-motion: no-preference) {
2929
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
2930
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
2931
+ @keyframes modal-drop {
2932
+ 0% { translate: 0 -100cqh; } /* above the stage */
2933
+ 50%, 100% { translate: 0 0; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
2934
+ }
2935
+ @keyframes modal-open {
2936
+ 0%, 50% { height: 0; padding-block: 0; } /* inside the card */
2937
+ 100% { height: var(--drawer-h); padding-block: var(--sp-5); } /* 2. out below, the lift up, together */
2938
+ }
2939
+ }
2940
+
2880
2941
  .btn-trio { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
2881
2942
  .btn-trio__dots {
2882
2943
  width: var(--sp-5);
@@ -1,7 +1,11 @@
1
1
  import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
- export interface LiftProps extends ComponentPropsWithoutRef<'article'> {
2
+ export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
3
3
  }
4
- /** Lift — the page content card lifted above a Drawer on an `SheetStage ask`. */
4
+ /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
+ export declare function Modal({ className, ...rest }: ModalProps): import("react").JSX.Element;
6
+ export interface LiftProps extends ComponentPropsWithoutRef<'div'> {
7
+ }
8
+ /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
5
9
  export declare function Lift({ className, ...rest }: LiftProps): import("react").JSX.Element;
6
10
  export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
7
11
  /** the question */
@@ -9,7 +13,7 @@ export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'titl
9
13
  /** the fine print under it */
10
14
  caption?: ReactNode;
11
15
  }
12
- /** Drawer — the dark strip at the bottom of an ask stage: a question, a caption, a BtnTrio (no · ask · yes). */
16
+ /** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons); give each button a `data-answer` — any of them closes the modal. */
13
17
  export declare function Drawer({ title, caption, className, children, ...rest }: DrawerProps): import("react").JSX.Element;
14
18
  export interface BtnTrioProps extends ComponentPropsWithoutRef<'div'> {
15
19
  }
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, Fragment } from 'react';
3
3
  import { cx } from '../cx.js';
4
- /** Lift — the page content card lifted above a Drawer on an `SheetStage ask`. */
4
+ /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
+ export function Modal({ className, ...rest }) {
6
+ return _jsx("article", { className: cx('card modal', className), ...rest });
7
+ }
8
+ /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
5
9
  export function Lift({ className, ...rest }) {
6
- return _jsx("article", { className: cx('card lift', className), ...rest });
10
+ return _jsx("div", { className: cx('modal__lift', className), ...rest });
7
11
  }
8
- /** Drawer — the dark strip at the bottom of an ask stage: a question, a caption, a BtnTrio (no · ask · yes). */
12
+ /** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons); give each button a `data-answer` — any of them closes the modal. */
9
13
  export function Drawer({ title, caption, className, children, ...rest }) {
10
- return (_jsxs("div", { className: cx('drawer', className), ...rest, children: [_jsx("h2", { className: "t-h2", children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
14
+ return (_jsxs("div", { className: cx('modal__drawer', className), ...rest, children: [_jsx("h2", { className: "t-h2", children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
11
15
  }
12
16
  /** BtnTrio — three round buttons with dotted connectors between them (× · ? · ✓); the middle one inverted. */
13
17
  export function BtnTrio({ className, children, ...rest }) {
@@ -1,13 +1,13 @@
1
1
  import type { ComponentPropsWithoutRef } from 'react';
2
- export type SheetState = 'peek' | 'half' | 'full';
2
+ export type SheetState = 'peek' | 'half' | '3q' | 'full';
3
3
  export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
4
- /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `full` (its top edge at the top bar) — a class, never a height */
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` */
5
5
  state?: SheetState;
6
6
  /** floats above a footnote strip on a dim stage (bottom corners and border back) */
7
7
  raised?: boolean;
8
8
  /** the grab handle (tap / drag / keyboard via cardds.js); `false` for a sheet with no handle, a string sets its accessible name */
9
9
  handle?: boolean | string;
10
- /** which states the handle cycles through, e.g. "peek half full" (default all three) */
10
+ /** which states a drag snaps to, e.g. "peek half full" (default all four: peek half 3q full) */
11
11
  states?: string;
12
12
  }
13
13
  /**
@@ -6,7 +6,7 @@ export interface SheetStageProps extends ComponentPropsWithoutRef<'div'> {
6
6
  hero?: ReactNode;
7
7
  /** blur and darken the background (the dim page sheet) */
8
8
  dim?: boolean;
9
- /** the ask drawer stage: a Lift card over a dark Drawer */
9
+ /** the ask stage: the page under a scrim, one Modal (Lift + Drawer) at the centre */
10
10
  ask?: boolean;
11
11
  /** a footnote strip on the scrim under a raised sheet */
12
12
  foot?: ReactNode;
@@ -2,14 +2,13 @@ import type { ComponentPropsWithoutRef } from 'react';
2
2
  export interface StepProps extends ComponentPropsWithoutRef<'section'> {
3
3
  }
4
4
  /**
5
- * Step — one screen, one task. Children: a StepCard first (what the member READS), an optional Sheet last
5
+ * Step — one screen, one task. Children: a StepCard first (what the member READS — words on the base, not a card), an optional Sheet last
6
6
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
7
7
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
8
8
  * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
9
9
  */
10
10
  export declare function Step({ className, ...rest }: StepProps): import("react").JSX.Element;
11
11
  export interface StepCardProps extends ComponentPropsWithoutRef<'article'> {
12
- tone?: 1 | 2 | 3;
13
12
  }
14
- /** StepCard — the reading card of a Step: a normal Card (CardHead · content) that hugs its words and scrolls inside itself when the column runs out. */
15
- export declare function StepCard({ tone, className, ...rest }: StepCardProps): import("react").JSX.Element;
13
+ /** StepCard — the reading block of a Step: the words on the base, no frame, no area colour (a CardHead · content, with a card's rhythm); it hugs its words and scrolls inside itself when the column runs out. */
14
+ export declare function StepCard({ className, ...rest }: StepCardProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * Step — one screen, one task. Children: a StepCard first (what the member READS), an optional Sheet last
4
+ * Step — one screen, one task. Children: a StepCard first (what the member READS — words on the base, not a card), an optional Sheet last
5
5
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
6
6
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
7
7
  * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
@@ -9,7 +9,7 @@ import { cx } from '../cx.js';
9
9
  export function Step({ className, ...rest }) {
10
10
  return _jsx("section", { className: cx('step', className), ...rest });
11
11
  }
12
- /** StepCard — the reading card of a Step: a normal Card (CardHead · content) that hugs its words and scrolls inside itself when the column runs out. */
13
- export function StepCard({ tone, className, ...rest }) {
14
- return _jsx("article", { className: cx('card step__card', tone && tone !== 1 && `card--${tone}`, className), ...rest });
12
+ /** StepCard — the reading block of a Step: the words on the base, no frame, no area colour (a CardHead · content, with a card's rhythm); it hugs its words and scrolls inside itself when the column runs out. */
13
+ export function StepCard({ className, ...rest }) {
14
+ return _jsx("article", { className: cx('step__card', className), ...rest });
15
15
  }
package/package.json CHANGED
@@ -1,18 +1,16 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
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), app/ is the Commons Time Bank PWA built from them, demo/ the CSS spec pages the acceptance tests (tests/) drive.",
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.",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+ssh://git@github.com/everysundays/cardds.git"
10
10
  },
11
11
  "scripts": {
12
- "dev": "vite --config app/vite.config.ts",
12
+ "dev": "vite --config gallery/vite.config.ts",
13
13
  "build": "node scripts/build-icons.mjs && tsc -p tsconfig.json && node scripts/flatten-css.mjs",
14
- "build:app": "node scripts/build-icons.mjs && vite build --config app/vite.config.ts",
15
- "preview:app": "vite preview --config app/vite.config.ts",
16
14
  "test": "playwright test",
17
15
  "validate:inbox": "node scripts/validate-inbox.mjs",
18
16
  "mcp:verify": "cd mcp-server && npm run verify && npm run verify:http",