@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 +48 -33
- package/cardds.js +62 -4
- package/css/actions.css +8 -0
- package/css/card.css +9 -3
- package/css/sheet.css +38 -0
- package/css/step.css +20 -5
- package/dist/cardds.css +75 -8
- package/dist/scaffold/BaseContent.d.ts +5 -2
- package/dist/scaffold/BaseContent.js +5 -2
- package/dist/scaffold/Centre.d.ts +4 -2
- package/dist/scaffold/Centre.js +4 -2
- package/dist/sheets/Sheet.d.ts +7 -4
- package/dist/sheets/Sheet.js +6 -3
- package/package.json +1 -2
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
|
|
9
|
-
every component's stories, live, searchable
|
|
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
|
|
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';
|
|
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),
|
|
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
|
|
45
|
-
so the gallery and the package share one truth with no build between them.
|
|
46
|
-
`/`
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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
|
|
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 >
|
|
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 >
|
|
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)
|
|
5
|
-
|
|
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
|
|
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 >
|
|
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 >
|
|
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)
|
|
1379
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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).
|
|
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 */
|
package/dist/scaffold/Centre.js
CHANGED
|
@@ -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
|
|
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).
|
|
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 });
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
19
|
-
*
|
|
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;
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
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": {
|