@usableapp/cardds 0.1.7 → 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 +114 -40
- package/cardds.js +62 -4
- package/css/actions.css +8 -0
- package/css/card.css +9 -3
- package/css/layover.css +0 -16
- package/css/lists.css +50 -0
- package/css/palette.css +5 -0
- package/css/sheet.css +38 -0
- package/css/step.css +20 -5
- package/css/theme.template.css +49 -0
- package/css/tokens.css +100 -69
- package/dist/actions/Chip.d.ts +1 -3
- package/dist/actions/Chip.js +2 -2
- package/dist/cardds.css +230 -93
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Bubble.d.ts +18 -0
- package/dist/lists/Bubble.js +13 -0
- 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
|
|
@@ -105,21 +102,17 @@ cp -R ~/Sites/cardds/css ~/Sites/cardds/fonts ~/Sites/cardds/cardds.css \
|
|
|
105
102
|
folder next to it, and `fonts/` beside that (`css/fonts.css` reaches the faces
|
|
106
103
|
as `../fonts/`). `icons.svg` is referenced by path from your markup
|
|
107
104
|
(`<use href="icons.svg#bell">`), so put it where those references resolve.
|
|
108
|
-
Then follow the rules below — the short version is: tune `
|
|
109
|
-
nothing else, and never write a raw hex or px in a component.
|
|
105
|
+
Then follow the rules below — the short version is: tune your own `theme.css`
|
|
106
|
+
(a copy of `css/theme.template.css`, see "A project's theme") and nothing else, and never write a raw hex or px in a component.
|
|
110
107
|
|
|
111
108
|
The repo is private, so cloning needs an account with access (`everysundays`
|
|
112
109
|
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
|
|
|
@@ -297,6 +312,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
297
312
|
|---|---|---|
|
|
298
313
|
| `css/fonts.css` + `fonts/` | the two `@font-face`s: PK Nonthaburi (body, `--font-ui`), FC Pride (display, `--font-display`); the font files and their licences | when swapping a face |
|
|
299
314
|
| `css/tokens.css` | spacing, radius, type levels, motion (`--motion-sheet`), the human centre (`--screen-centre`), grayscale role defaults; registers the two sheet tokens (`--sheet-peek`, `--sheet-half`) so script can read them resolved | rarely |
|
|
315
|
+
| `css/theme.template.css` | **a project's theme** — the four size bases + the colour roles, at their defaults; NOT in the bundle: copied into the project as its `theme.css`, loaded after `cardds.css` | copy it, don't edit it |
|
|
300
316
|
| `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
|
|
301
317
|
| `css/base.css` | reset, surface, typography classes, `.base-content` | rarely |
|
|
302
318
|
| `css/card.css` | `.card` + its skeleton (`.card__head` / content / `.card__foot`), `.card-list`, `.card--row`, `.icon-row`, stats, `.card--centre` (one card at the human centre of a fill screen) and `.screen__centre` (a small group there) | when adding variants |
|
|
@@ -309,9 +325,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
309
325
|
| `css/numbers.css` | `.ring` gauge, `.track` steps, `.card--band` + `.band-stack`, `.dotgrid`, `.picker`, `.badge`, `.bars` | big numbers |
|
|
310
326
|
| `css/people.css` | `.avatar` (sm/lg/xl, outline, add, on, halo), `.avatar-stack`, `.avatar-pick` | people |
|
|
311
327
|
| `css/choice.css` | `.check`, `.toggle`, `.chip-grid` + `.chip--pick` (+ `--sign`), `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
|
|
312
|
-
| `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.timeline`, `.legend`, `.link` | lists inside cards |
|
|
328
|
+
| `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.thread` + `.bubble` (chat: theirs left, mine right, no tail), `.timeline`, `.legend`, `.link` | lists inside cards |
|
|
313
329
|
| `css/media.css` | `.card--cover`, `.quote`, `.tile-grid` + `.tile`, `.card--fold`, `.mosaic`, `.wave` | media & display |
|
|
314
|
-
| `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.
|
|
330
|
+
| `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.deck` | cards over content |
|
|
315
331
|
| `icons.svg` | Lucide sprite (ISC), 56 minimal stroke icons | add symbols as needed |
|
|
316
332
|
| `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
333
|
| `gallery/` | the gallery site: `npm run dev` → `/` every story, `/:Name` one component (`gallery/src/Gallery.tsx` globs `.design-sync/previews/`) | the demo |
|
|
@@ -502,6 +518,63 @@ Demo: the *skeleton* block at the top of `elements.html`.
|
|
|
502
518
|
`--on-accent` to its own ink and bg, so every accent consumer (buttons,
|
|
503
519
|
checks, toggles, rings, selected days) inverts without extra rules.
|
|
504
520
|
|
|
521
|
+
## A project's theme
|
|
522
|
+
|
|
523
|
+
A project retunes cardds from ONE file of its own, and nothing else. Copy
|
|
524
|
+
`css/theme.template.css` into the project as `theme.css`, load it **after**
|
|
525
|
+
`cardds.css`, change values:
|
|
526
|
+
|
|
527
|
+
```css
|
|
528
|
+
:root {
|
|
529
|
+
--sp-base: 0.3rem; /* roomier */
|
|
530
|
+
--r-base: 0.5rem; /* tighter corners */
|
|
531
|
+
--color-surface: #faf6ee; --color-ink: #1f1b16; --color-paper: #fff;
|
|
532
|
+
--color-brand: #c2410c; --color-on-brand: #fff;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
```html
|
|
537
|
+
<html data-palette="own"> <!-- colours of your own = coloured mode; any value no stock palette uses -->
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
That file is the whole public surface:
|
|
541
|
+
|
|
542
|
+
| what | tokens | moves |
|
|
543
|
+
|---|---|---|
|
|
544
|
+
| room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
|
|
545
|
+
| type | `--fs-base` (1rem = 16) | every `--fs-*`, `--badge`, `--icon-xs` |
|
|
546
|
+
| corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
|
|
547
|
+
| the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
|
|
548
|
+
| faces | `--font-ui`, `--font-display` | |
|
|
549
|
+
| colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
|
|
550
|
+
| colour roles (the escape hatch) | `--surface`, `--on-surface(-muted)`, `--card-{1,2,3}-{bg,ink}`, `--card-border`, `--card-muted`, `--card-2-muted`, `--accent`, `--on-accent`, `--action-{bg,ink,border}`, `--pos`, `--neg`, `--dot-{a,b}` | a role set directly wins over its derivation; the tints derive from the roles |
|
|
551
|
+
|
|
552
|
+
Rules:
|
|
553
|
+
|
|
554
|
+
- **A base, never a multiplier.** Every size token is `calc(base × multiplier)`
|
|
555
|
+
in `tokens.css`; the multipliers are the system's proportions. A project
|
|
556
|
+
never sets a derived token (`--sp-4`, `--fs-h1`, `--r-card`, `--tap`…), a
|
|
557
|
+
private one (`--_*`), or a published one (`--card-pad`, `--bar-reserve`, `--sheet-*`).
|
|
558
|
+
- **On `:root` only.** The derived tokens are computed where they are declared
|
|
559
|
+
(`:root`), so a base set on a subtree does not reach them.
|
|
560
|
+
- **After `cardds.css`.** A stock palette's block and a project's `:root` weigh
|
|
561
|
+
the same; the later file wins.
|
|
562
|
+
- **Coloured mode is the attribute.** `data-palette`, whatever its value, is what
|
|
563
|
+
trades the wireframe's borders for the card's shadow and the controls' soft
|
|
564
|
+
edge (`palette.css`, `forms.css`). Colours of your own → `<html data-palette="own">`.
|
|
565
|
+
Without it the colours still apply, on wireframe borders.
|
|
566
|
+
- **cardds does not judge the colours.** No contrast check, no warning — a
|
|
567
|
+
theme's values are the project's call. The derivations are only starting points;
|
|
568
|
+
any role can be set outright.
|
|
569
|
+
- `--text-scale` (below) is the member's setting, not the theme's: it scales
|
|
570
|
+
everything, the bases included.
|
|
571
|
+
|
|
572
|
+
Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
|
|
573
|
+
palette `own` shows a colour well per colour base.
|
|
574
|
+
`tests/tokens.spec.js` holds the defaults to the px they always were and the
|
|
575
|
+
template to the defaults; the step / centre geometry runs at two extreme
|
|
576
|
+
themes (`tight`, `loose`).
|
|
577
|
+
|
|
505
578
|
## New palette
|
|
506
579
|
|
|
507
580
|
Copy any block in `palette.css`, rename, retune:
|
|
@@ -517,9 +590,10 @@ Copy any block in `palette.css`, rename, retune:
|
|
|
517
590
|
|
|
518
591
|
## Scale: everything is rem
|
|
519
592
|
|
|
520
|
-
Every length token
|
|
521
|
-
`--
|
|
522
|
-
|
|
593
|
+
Every length token resolves to rem — a multiplier × one of four rem bases
|
|
594
|
+
(`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
|
|
595
|
+
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.75; see "A project's theme") — so the
|
|
596
|
+
whole system still resizes from one number: the root font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
|
|
523
597
|
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
|
|
524
598
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
525
599
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
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/layover.css
CHANGED
|
@@ -271,22 +271,6 @@
|
|
|
271
271
|
}
|
|
272
272
|
.callout__title { font: var(--type-title); }
|
|
273
273
|
.callout__sub { font: var(--type-caption); opacity: 0.7; }
|
|
274
|
-
/* speech chip: a chip with the same tail (mascot / greeting) */
|
|
275
|
-
.chip--bubble { position: relative; }
|
|
276
|
-
.chip--bubble::after {
|
|
277
|
-
content: "";
|
|
278
|
-
position: absolute;
|
|
279
|
-
left: var(--sp-4);
|
|
280
|
-
bottom: calc(-1 * var(--sp-2) + var(--border-w));
|
|
281
|
-
width: var(--sp-3);
|
|
282
|
-
height: var(--sp-3);
|
|
283
|
-
rotate: 45deg;
|
|
284
|
-
background: inherit;
|
|
285
|
-
border: inherit;
|
|
286
|
-
border-top: 0;
|
|
287
|
-
border-left: 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
274
|
/* ---- deck: swipe cards; the last child is on top ----
|
|
291
275
|
<div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
|
|
292
276
|
<div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
|
package/css/lists.css
CHANGED
|
@@ -87,6 +87,56 @@
|
|
|
87
87
|
.kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
|
|
88
88
|
.card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
|
|
89
89
|
|
|
90
|
+
/* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
|
|
91
|
+
No tail: a rounded block whose corner nearest the sender is square-ish
|
|
92
|
+
(bottom-left for theirs, bottom-right for mine), the way messaging apps
|
|
93
|
+
draw it. Under it a caption line (time · a tick), beside it small round
|
|
94
|
+
actions (a heart, more). A .thread stacks them.
|
|
95
|
+
<div class="thread">
|
|
96
|
+
<div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
|
|
97
|
+
<div class="bubble bubble--right">…</div>
|
|
98
|
+
</div> */
|
|
99
|
+
.thread { display: grid; gap: var(--sp-4); }
|
|
100
|
+
/* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
|
|
101
|
+
and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
|
|
102
|
+
.bubble {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: minmax(0, max-content) auto;
|
|
105
|
+
justify-content: start;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: var(--sp-1) var(--sp-3);
|
|
108
|
+
max-width: 88%;
|
|
109
|
+
margin-right: auto; /* theirs: hugs the left */
|
|
110
|
+
}
|
|
111
|
+
.bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
|
|
112
|
+
.bubble__body, .bubble__meta { grid-column: 1; }
|
|
113
|
+
.bubble__actions { grid-column: 2; grid-row: 1; }
|
|
114
|
+
.bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
|
|
115
|
+
.bubble--right > .bubble__actions { grid-column: 1; }
|
|
116
|
+
.bubble__body {
|
|
117
|
+
margin: 0;
|
|
118
|
+
min-width: 0;
|
|
119
|
+
padding: var(--sp-3) var(--sp-4);
|
|
120
|
+
border-radius: var(--r-card);
|
|
121
|
+
border-bottom-left-radius: var(--r-control);
|
|
122
|
+
background: var(--card-2-bg);
|
|
123
|
+
color: var(--card-2-ink);
|
|
124
|
+
font: var(--type-body);
|
|
125
|
+
overflow-wrap: anywhere;
|
|
126
|
+
}
|
|
127
|
+
.bubble--right > .bubble__body {
|
|
128
|
+
border-bottom-left-radius: var(--r-card);
|
|
129
|
+
border-bottom-right-radius: var(--r-control);
|
|
130
|
+
background: var(--accent);
|
|
131
|
+
color: var(--on-accent);
|
|
132
|
+
text-align: right;
|
|
133
|
+
}
|
|
134
|
+
.bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
|
|
135
|
+
.bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
|
|
136
|
+
.bubble--right > .bubble__meta { justify-self: end; }
|
|
137
|
+
.bubble__meta > .icon { color: var(--accent); }
|
|
138
|
+
.card--3 .bubble__meta { color: inherit; opacity: 0.7; }
|
|
139
|
+
|
|
90
140
|
/* ---- timeline: a hairline with date chips, entries to the right ----
|
|
91
141
|
<ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
|
|
92
142
|
.timeline {
|
package/css/palette.css
CHANGED
|
@@ -108,5 +108,10 @@
|
|
|
108
108
|
|
|
109
109
|
/* Colored palettes trade wireframe outlines for soft elevation. */
|
|
110
110
|
[data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
|
|
111
|
+
/* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
|
|
112
|
+
--color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
|
|
113
|
+
"own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
|
|
114
|
+
edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
|
|
115
|
+
[data-palette] { --card-border: transparent; --action-border: transparent; }
|
|
111
116
|
/* …but controls keep a faint edge so they still read as controls */
|
|
112
117
|
[data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
|
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;
|