@usableapp/cardds 0.6.5 → 0.6.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.6.6 — 2026-09-23
6
+
7
+ ### BOTTOM is a layer (Lh 2026-09-23, replaces 0.6.5's "the bar is a row")
8
+ - In a `Screen fill`, top + content are one flex column and **the content runs to the screen's bottom edge**. The bottom
9
+ (`ActionBar`) is a **layer over the content's foot** — it covers what is under it, on purpose. Nothing is added to the
10
+ content to make room.
11
+ - **The walk's `PagerAt` is the bottom layer's first row**: a direct child of the `ActionBar`, before its tiers, centred.
12
+ It is no longer a child of the `Step`. (The 2026-09-16 rule "the dots never in the bar" is retired.)
13
+ - `ActionBar` in a fill screen has no top padding.
14
+
15
+ ### `BaseContent` → `Page`
16
+ - The content is a box. What is in it is always components one after another. A page of words (picture · heading · words ·
17
+ a CtaPack) is **`Page`** (class `.page`, was `.base-content`), and a walk is `Pages` of `Page`s.
18
+ - `BaseContent` still works for this version as an alias of `Page`; it warns once in the console. Rename it.
19
+ - The knobs keep their names for now (`--base-content-media-h`, `--base-content-gap`, …).
20
+
21
+ ### Fixes and small additions
22
+ - A `Step` under an `AppBar` touches it again, so a full sheet stops on the bar's bottom edge (0.6.5 left an 8px strip — timebank #137).
23
+ - `Link sm`: a minor link at a new size `--fs-2xs` (15px) on a 28px line; its tap target stays 44px. A `ctaLink` row is as tall as that line.
24
+
5
25
  ## 0.6.5 — 2026-09-23
6
26
 
7
27
  ### A screen is top · content · bottom, one flex column (Lh 2026-09-23)
package/README.md CHANGED
@@ -37,7 +37,7 @@ npm test # Playwright: the tokens and the step/sheet/centre geometry
37
37
  ```
38
38
 
39
39
  ```tsx
40
- import { Screen, TopBar, Step, BaseContent, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
40
+ import { Screen, TopBar, Step, Page, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
41
41
  import '@usableapp/cardds/dist/cardds.css';
42
42
  import './theme.css'; // the project's own look, after the system's — see "A project's theme"
43
43
  ```
@@ -165,7 +165,7 @@ of `Screen`:
165
165
  | Part | Prop | What fills it |
166
166
  |---|---|---|
167
167
  | **top** | `top` | `TopBar` · `AppBar` · `"lead"` (no bar: the content starts `--screen-lead`, 15% of the screen, down) |
168
- | **content** | children | a `Step` (`Pages` · `BaseContent` · `BaseContent` + `Sheet`) · `Centre` · `SheetStack` … |
168
+ | **content** | children | a `Step` (`Pages` · `Page` · `Page` + `Sheet`) · `Centre` · `SheetStack` … |
169
169
  | **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
170
170
 
171
171
  ```tsx
@@ -174,12 +174,11 @@ of `Screen`:
174
174
  </Screen>
175
175
  ```
176
176
 
177
- - **The browser measures, nobody computes.** In `Screen fill` the content takes all the height the top and the bottom
178
- leave (`flex: 1`); the bottom is as tall as its rows. A one-row bar gives its room straight back to the content.
179
- No `--bar-reserve` arithmetic for a bar in the screen.
180
- - **The bar is INSIDE the screen, its last part** — `bottom`, never a sibling after `</Screen>`, never pinned with a
181
- hand-set `position`. The keyboard lifts the column's end (`--kb`). A `FloatBar` in `bottom` floats over the
182
- content at the screen's foot (it lies over a map or a stack — not a row).
177
+ - **Top and content are one flex column; the bottom is a LAYER** (Lh 2026-09-23). In `Screen fill` the content runs to the
178
+ screen's bottom edge; the bottom (`ActionBar`, with the walk's `PagerAt` as its first row) lies over it at the foot and
179
+ covers what is under it — on purpose, nothing is added to the content to make room. The keyboard lifts the layer (`--kb`).
180
+ - **The bar is INSIDE the screen** — `bottom`, never a sibling after `</Screen>`, never pinned with a hand-set `position`.
181
+ A `FloatBar` in `bottom` floats the same way.
183
182
  - **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `CardFoot`; `Screen` warns once when both
184
183
  are there.
185
184
  - **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
@@ -196,7 +195,7 @@ whose markup has rules, because the sheet's box is the step itself:
196
195
  ```html
197
196
  <!-- reads: the words only -->
198
197
  <section class="step">
199
- <section class="base-content">
198
+ <section class="page">
200
199
  <div class="card__head"><h2 class="t-h2">…</h2></div>
201
200
  <p class="t-body">…</p>
202
201
  </section>
@@ -205,7 +204,7 @@ whose markup has rules, because the sheet's box is the step itself:
205
204
 
206
205
  <!-- acts: the words + a sheet -->
207
206
  <section class="step">
208
- <section class="base-content">…the words…</section>
207
+ <section class="page">…the words…</section>
209
208
  <article class="card sheet sheet--half">
210
209
  <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
211
210
  <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
@@ -229,7 +228,7 @@ whose markup has rules, because the sheet's box is the step itself:
229
228
  card takes the inset back as its side margins — so the card and the bar's
230
229
  buttons stand on the inset line, and the sheet runs edge to edge like the
231
230
  bar and like a sheet in a `.sheet-stage`.
232
- - **`.base-content` first, `.sheet` last.** The block carries the step's words
231
+ - **`.page` first, `.sheet` last.** The block carries the step's words
233
232
  ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
234
233
  Lh: a step reads like a page, not a card in a page). It keeps a card's
235
234
  rhythm: a `.card__head`, `--card-gap` between things. The sheet is
@@ -311,8 +310,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
311
310
  - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
312
311
  holding two `.icon-btn`s and nothing else: they stand at the two edges. The
313
312
  step that commits still goes back to one wide button (rule 3).
314
- - **BaseContent that moves** (Lh 2026-09-20) — two ways, never both on one screen:
315
- - *Pages of words:* `.step > .pages > .base-content` (one `aria-current="step"`, the rest `inert`) — every page
313
+ - **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
314
+ - *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
316
315
  one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
317
316
  - *Sheets the app calls:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`); each
318
317
  way has a sheet that holds its WHOLE form and its own submit on its foot, starting `.sheet--away` (not on the
@@ -367,7 +366,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
367
366
  by `translate` only. (The stack was first named "card stack": wrong, and
368
367
  retired.) Demos: `sheet.html`, `stack.html`.
369
368
  - **A step is one screen, one task: `.step`.** What the member reads is
370
- words on the base (`.base-content` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
369
+ words on the base (`.page` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
371
370
  `.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
372
371
  bottom, after the step. A step where the member only READS is the card alone.
373
372
  A step where the member ACTS (types, picks, shoots a photo, records) adds
@@ -394,7 +393,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
394
393
  1. **One card = one boundary.** A card frames one thing: a category, a stat
395
394
  group, a list row. Tone slots: `.card` (card-1), `.card--2`, `.card--3`.
396
395
  2. **Sections are not cards.** Page sections = frameless on-surface content
397
- (`.base-content`) + a lone/packed CTA (`.cta-pack`).
396
+ (`.page`) + a lone/packed CTA (`.cta-pack`).
398
397
  3. **Surrounding UI is separate.** `.topbar`, `.filter-row`, `.chip-row`,
399
398
  `.action-bar` live outside cards, above/below. The bottom bar has two
400
399
  arrangements and they mean different things: the default pack (one wide
package/css/actions.css CHANGED
@@ -289,7 +289,7 @@
289
289
  shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
290
290
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
291
291
  .action-bar__tier > .btn { flex: 1; }
292
- .action-bar__tier--link { justify-content: center; } /* a Link under the buttons, e.g. "details" that opens a Modal */
292
+ .action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
293
293
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
294
294
  /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
295
295
  adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
package/css/base.css CHANGED
@@ -113,21 +113,21 @@ button { font: inherit; cursor: pointer; }
113
113
  a heading, a paragraph, maybe a .cta-pack. Was .section until
114
114
  2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
115
115
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
116
- .base-content {
116
+ .page {
117
117
  display: grid;
118
118
  gap: var(--base-content-gap, var(--sp-3));
119
119
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
120
120
  padding-block: var(--base-content-pad-y, var(--sp-4));
121
121
  }
122
- .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
122
+ .page > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
123
123
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
124
124
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
125
125
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
126
126
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
127
- .base-content > .card__head > .t-h1 { font-size: var(--fs-h2); }
128
- .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
129
- .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
130
- .base-content .cta-pack {
127
+ .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
128
+ .page > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
129
+ .page > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
130
+ .page .cta-pack {
131
131
  display: flex;
132
132
  align-items: start; /* buttons keep their own height, never the row's */
133
133
  gap: var(--sp-2);
package/css/card.css CHANGED
@@ -227,13 +227,13 @@
227
227
  user-select: none; /* a walk is dragged, not read-and-selected — without this, a finger crossing the words highlights them instead of turning the page (timebank #59) */
228
228
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
229
229
  }
230
- .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
230
+ .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
231
231
  .pages > [inert] { visibility: hidden; }
232
- /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
232
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
233
233
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
234
234
  <section class="step"><div class="pages">
235
- <section class="base-content" aria-current="step">…</section>
236
- <section class="base-content" inert>…</section>
235
+ <section class="page" aria-current="step">…</section>
236
+ <section class="page" inert>…</section>
237
237
  </div></section> <div class="action-bar action-bar--pager">← →</div> */
238
238
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
239
239
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
package/css/lists.css CHANGED
@@ -205,5 +205,9 @@
205
205
  padding: 0;
206
206
  cursor: pointer;
207
207
  }
208
+ /* a SMALL link (Lh 2026-09-23): a thing tapped rarely — "details", a ctaLink — at the minor-link size (15) and 28px of height;
209
+ its 44px tap target stays, an invisible ::after around it */
210
+ .link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
211
+ .link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
208
212
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
209
213
  .card--3 .link--muted { color: var(--card-3-muted); }
package/css/sheet.css CHANGED
@@ -94,7 +94,7 @@
94
94
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
95
95
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
96
96
 
97
- /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
97
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .page has buttons that each CALL a sheet ----
98
98
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
99
99
  changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
100
100
  with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
package/css/stack.css CHANGED
@@ -232,7 +232,7 @@
232
232
  }
233
233
  /* a base that is ONE BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
234
234
  loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
235
- .sheet-stack__base:has(> .base-content:only-child) { align-content: start; }
235
+ .sheet-stack__base:has(> .page:only-child) { align-content: start; }
236
236
 
237
237
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
238
238
  into it — an opacity fade in place, never a slide in from the left (the
package/css/step.css CHANGED
@@ -7,7 +7,7 @@
7
7
  (see "LAYERS" below).
8
8
 
9
9
  <section class="step">
10
- <section class="base-content">…the words…</section> ← first
10
+ <section class="page">…the words…</section> ← first
11
11
  <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
12
12
  </section>
13
13
  <div class="action-bar">…</div> ← only when there is no sheet
@@ -66,14 +66,14 @@
66
66
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
67
67
  /* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
68
68
  is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
69
- :has(> .action-bar) .step:has(> .pager__at) { margin-bottom: calc(var(--bar-reserve) - var(--sp-6) + var(--kb, 0rem)); }
69
+
70
70
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
71
71
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
72
72
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
73
73
  own screens are composed (the guide); the framework does not police it — a component never changes
74
74
  because another one is there. */
75
75
 
76
- /* the reading block: a .base-content (base.css) — the same words-on-the-base
76
+ /* the reading block: a .page (base.css) — the same words-on-the-base
77
77
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
78
78
  Lh: a step reads like a page, not a card in a page — so it IS the page's
79
79
  block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
@@ -81,7 +81,7 @@
81
81
  gets the flex rule: it hugs its content, and when the column runs out (a
82
82
  short phone, large text) it shrinks and scrolls inside itself instead of
83
83
  pushing the bar or the sheet */
84
- .step > .base-content, .step > .pages > .base-content {
84
+ .step > .page, .step > .pages > .page {
85
85
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
86
86
  --card-gap: var(--sp-4);
87
87
  gap: var(--card-gap);
@@ -89,26 +89,14 @@
89
89
  align-content: start;
90
90
  color: var(--on-surface);
91
91
  }
92
- /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
92
+ /* the reading block is the .page — or the .pages that holds several of them, a walk of words
93
93
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
94
94
  (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
95
- .step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
96
- .step > :is(.base-content, .pages) {
95
+ .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
96
+ .step > :is(.page, .pages) {
97
97
  flex: 0 1 auto;
98
98
  }
99
- /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
100
- room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
101
- .step > .pager__at {
102
- /* it FLOATS over the reading block's foot (Lh 2026-09-23: the strip it took under the words was wasted room) — the pages
103
- run down to the bar, the dots sit on their bottom edge, centred, on the base's ground so a line passing under stays clear of them */
104
- position: absolute; bottom: 0; left: 50%; translate: -50% 0; z-index: 2;
105
- padding: var(--sp-3) var(--sp-2);
106
- border-radius: var(--r-chip);
107
- background: var(--surface);
108
- }
109
- /* the reading block fills the step, and keeps the dots' height free at the end of its scroll so the last line clears them */
110
- .step:has(> .pager__at) > :is(.base-content, .pages) { flex: 1 1 auto; padding-bottom: calc(var(--dot) + 2 * var(--sp-3)); }
111
- .step > :is(.base-content, .pages) {
99
+ .step > :is(.page, .pages) {
112
100
  min-height: 0;
113
101
  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 */
114
102
  overflow-y: auto;
@@ -118,8 +106,8 @@
118
106
  /* a picture that opens the words stays put, only the words scroll under it (Lh 2026-09-23, timebank's walk at large text:
119
107
  the picture scrolled away with the words). Sticky at the top of the reading block's scroller; its height is
120
108
  --base-content-media-h as everywhere, never shrunk (Lh 2026-09-23: a fixed height, no scaling). */
121
- .step > .base-content > :is(img, .ph):first-child,
122
- .step > .pages > .base-content > :is(img, .ph):first-child {
109
+ .step > .page > :is(img, .ph):first-child,
110
+ .step > .pages > .page > :is(img, .ph):first-child {
123
111
  position: sticky;
124
112
  top: 0;
125
113
  z-index: 1;
@@ -127,23 +115,20 @@
127
115
  }
128
116
 
129
117
  /* ============================================================
130
- ONE FLEX COLUMN (Lh 2026-09-23: every part of a screen is laid by flex). In a .screen--fill the bar is IN the
131
- column, after the step: the step takes what is left (flex: 1), the bar takes what its rows need — measured by the
132
- browser, no --bar-reserve arithmetic, so a one-row bar gives its room straight back to the pages. The keyboard lifts
133
- the column's end: the screen pads its bottom by --kb, the bar rides on it.
118
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
119
+ in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
120
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
134
121
  (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
135
122
  ============================================================ */
136
123
  .screen--fill > .action-bar {
137
- position: relative; inset: auto; bottom: auto;
138
- flex: none;
139
- margin-top: auto; /* the column's end, even on a screen with nothing that grows (a centred card is absolute) */
124
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
140
125
  max-width: none;
141
- margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge, as the step; the bar's own padding keeps the inset */
126
+ margin: 0;
127
+ padding-top: 0; /* no ground of its own, never a solid strip */
142
128
  }
143
- .screen--fill:has(> .action-bar) { padding-bottom: var(--kb, 0rem); }
144
129
  .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
145
- /* the step and its bar touch: the screen's gap between two stacked things is not for these two (the bar's own padding is its room) */
146
- .screen--fill:has(> .step) > .action-bar { margin-top: calc(-1 * var(--screen-gap)); }
130
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
131
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
147
132
  /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
148
133
  reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
149
134
  .screen--fill > .appbar {
@@ -154,4 +139,7 @@
154
139
  margin-bottom: calc(var(--sp-2) - var(--screen-gap));
155
140
  }
156
141
  .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
142
+ /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
143
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
144
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
157
145
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
package/css/tokens.css CHANGED
@@ -20,7 +20,7 @@
20
20
  and never a multiplier, so the proportions of the system hold at any size.
21
21
  The px in the comments are at these defaults (1rem = 16px). */
22
22
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
23
- --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
23
+ --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.68 (15, minor links — was 0.775/17 until 2026-09-23). A new size is Lh's call only.
24
24
  Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
25
25
  --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
26
26
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
@@ -103,6 +103,7 @@
103
103
 
104
104
  /* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
105
105
  Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
106
+ --fs-2xs: calc(var(--fs-base) * 0.68); /* 15 minor links — a ctaLink, a "details" (Lh 2026-09-23: added by Lh; ONLY Lh adds a size) */
106
107
  --fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
107
108
  --fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
108
109
  --fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
@@ -5,11 +5,13 @@ export interface LinkProps extends ComponentProps<'a'> {
5
5
  muted?: boolean;
6
6
  /** a trailing xs icon (arrow-up-right for "See all") */
7
7
  icon?: IconName;
8
+ /** SMALL: the minor-link size (15) and a 28px line, for what is tapped rarely (details, a ctaLink); the tap target stays 44px */
9
+ sm?: boolean;
8
10
  /** without an href it renders as a button */
9
11
  href?: string;
10
12
  }
11
13
  /** Link — an inline text action big enough to tap (40px): "See all ↗" in a card head, "Skip" between pager buttons. */
12
- export declare function Link({ muted, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
14
+ export declare function Link({ muted, sm, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
13
15
  ref?: import("react").Ref<HTMLAnchorElement> | undefined;
14
16
  key?: import("react").Key | null | undefined;
15
17
  download?: any;
@@ -3,6 +3,6 @@ import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Icon } from '../type/Icon.js';
5
5
  /** Link — an inline text action big enough to tap (40px): "See all ↗" in a card head, "Skip" between pager buttons. */
6
- export function Link({ muted, icon, href, className, children, ...rest }) {
7
- return createElement(href ? 'a' : 'button', { href, type: href ? undefined : 'button', className: cx('link', muted && 'link--muted', className), ...rest }, children, icon && _jsx(Icon, { name: icon, size: "xs" }));
6
+ export function Link({ muted, sm, icon, href, className, children, ...rest }) {
7
+ return createElement(href ? 'a' : 'button', { href, type: href ? undefined : 'button', className: cx('link', muted && 'link--muted', sm && 'link--sm', className), ...rest }, children, icon && _jsx(Icon, { name: icon, size: "xs" }));
8
8
  }
package/dist/cardds.css CHANGED
@@ -61,7 +61,7 @@
61
61
  and never a multiplier, so the proportions of the system hold at any size.
62
62
  The px in the comments are at these defaults (1rem = 16px). */
63
63
  --sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
64
- --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
64
+ --fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.68 (15, minor links — was 0.775/17 until 2026-09-23). A new size is Lh's call only.
65
65
  Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
66
66
  --r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
67
67
  --tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
@@ -144,6 +144,7 @@
144
144
 
145
145
  /* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
146
146
  Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
147
+ --fs-2xs: calc(var(--fs-base) * 0.68); /* 15 minor links — a ctaLink, a "details" (Lh 2026-09-23: added by Lh; ONLY Lh adds a size) */
147
148
  --fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
148
149
  --fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
149
150
  --fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
@@ -492,21 +493,21 @@ button { font: inherit; cursor: pointer; }
492
493
  a heading, a paragraph, maybe a .cta-pack. Was .section until
493
494
  2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
494
495
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
495
- .base-content {
496
+ .page {
496
497
  display: grid;
497
498
  gap: var(--base-content-gap, var(--sp-3));
498
499
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
499
500
  padding-block: var(--base-content-pad-y, var(--sp-4));
500
501
  }
501
- .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
502
+ .page > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
502
503
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
503
504
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
504
505
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
505
506
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
506
- .base-content > .card__head > .t-h1 { font-size: var(--fs-h2); }
507
- .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
508
- .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
509
- .base-content .cta-pack {
507
+ .page > .card__head > .t-h1 { font-size: var(--fs-h2); }
508
+ .page > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
509
+ .page > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
510
+ .page .cta-pack {
510
511
  display: flex;
511
512
  align-items: start; /* buttons keep their own height, never the row's */
512
513
  gap: var(--sp-2);
@@ -758,13 +759,13 @@ button { font: inherit; cursor: pointer; }
758
759
  user-select: none; /* a walk is dragged, not read-and-selected — without this, a finger crossing the words highlights them instead of turning the page (timebank #59) */
759
760
  touch-action: pan-y; /* leaves vertical scroll to the browser; horizontal is the app's own drag, never a native pan */
760
761
  }
761
- .pages > :is(.card, .base-content) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
762
+ .pages > :is(.card, .page) { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
762
763
  .pages > [inert] { visibility: hidden; }
763
- /* pages of WORDS (Lh, 2026-09-20): the same walk with .base-content pages, on the base — in a .step the
764
+ /* pages of WORDS (Lh, 2026-09-20): the same walk with .page pages, on the base — in a .step the
764
765
  .pages is the reading block (step.css), the round pager in the bar turns it, the same slideTo().
765
766
  <section class="step"><div class="pages">
766
- <section class="base-content" aria-current="step">…</section>
767
- <section class="base-content" inert>…</section>
767
+ <section class="page" aria-current="step">…</section>
768
+ <section class="page" inert>…</section>
768
769
  </div></section> <div class="action-bar action-bar--pager">← →</div> */
769
770
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
770
771
  ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
@@ -1070,7 +1071,7 @@ button { font: inherit; cursor: pointer; }
1070
1071
  }
1071
1072
  /* a base that is ONE BaseContent is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
1072
1073
  loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
1073
- .sheet-stack__base:has(> .base-content:only-child) { align-content: start; }
1074
+ .sheet-stack__base:has(> .page:only-child) { align-content: start; }
1074
1075
 
1075
1076
  /* back button: lives in a slot the header reserves once open, and DISSOLVES
1076
1077
  into it — an opacity fade in place, never a slide in from the left (the
@@ -1247,7 +1248,7 @@ button { font: inherit; cursor: pointer; }
1247
1248
  setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
1248
1249
  :root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
1249
1250
 
1250
- /* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
1251
+ /* ---- called sheets (Lh, 2026-09-20): a .step whose .page has buttons that each CALL a sheet ----
1251
1252
  .sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
1252
1253
  changes), and hidden once it has left so its shadow does not lie on the step's bottom edge; write it
1253
1254
  with `inert` (the React Sheet does). WHO brings it onto the screen is the app's business — a button, a route:
@@ -1532,7 +1533,7 @@ button { font: inherit; cursor: pointer; }
1532
1533
  (see "LAYERS" below).
1533
1534
 
1534
1535
  <section class="step">
1535
- <section class="base-content">…the words…</section> ← first
1536
+ <section class="page">…the words…</section> ← first
1536
1537
  <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
1537
1538
  </section>
1538
1539
  <div class="action-bar">…</div> ← only when there is no sheet
@@ -1591,14 +1592,14 @@ button { font: inherit; cursor: pointer; }
1591
1592
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1592
1593
  /* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
1593
1594
  is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
1594
- :has(> .action-bar) .step:has(> .pager__at) { margin-bottom: calc(var(--bar-reserve) - var(--sp-6) + var(--kb, 0rem)); }
1595
+
1595
1596
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
1596
1597
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
1597
1598
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
1598
1599
  own screens are composed (the guide); the framework does not police it — a component never changes
1599
1600
  because another one is there. */
1600
1601
 
1601
- /* the reading block: a .base-content (base.css) — the same words-on-the-base
1602
+ /* the reading block: a .page (base.css) — the same words-on-the-base
1602
1603
  block as anywhere else (it was .step__card, and a .card before 2026-09-16;
1603
1604
  Lh: a step reads like a page, not a card in a page — so it IS the page's
1604
1605
  block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
@@ -1606,7 +1607,7 @@ button { font: inherit; cursor: pointer; }
1606
1607
  gets the flex rule: it hugs its content, and when the column runs out (a
1607
1608
  short phone, large text) it shrinks and scrolls inside itself instead of
1608
1609
  pushing the bar or the sheet */
1609
- .step > .base-content, .step > .pages > .base-content {
1610
+ .step > .page, .step > .pages > .page {
1610
1611
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1611
1612
  --card-gap: var(--sp-4);
1612
1613
  gap: var(--card-gap);
@@ -1614,26 +1615,14 @@ button { font: inherit; cursor: pointer; }
1614
1615
  align-content: start;
1615
1616
  color: var(--on-surface);
1616
1617
  }
1617
- /* the reading block is the .base-content — or the .pages that holds several of them, a walk of words
1618
+ /* the reading block is the .page — or the .pages that holds several of them, a walk of words
1618
1619
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
1619
1620
  (the buttons that call the other sheets) scrolls clear of it — CSS alone: the box declares --sheet-peek. */
1620
- .step:has(> .sheet--peek) > :is(.base-content, .pages) { padding-bottom: var(--sheet-peek); }
1621
- .step > :is(.base-content, .pages) {
1621
+ .step:has(> .sheet--peek) > :is(.page, .pages) { padding-bottom: var(--sheet-peek); }
1622
+ .step > :is(.page, .pages) {
1622
1623
  flex: 0 1 auto;
1623
1624
  }
1624
- /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
1625
- room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
1626
- .step > .pager__at {
1627
- /* it FLOATS over the reading block's foot (Lh 2026-09-23: the strip it took under the words was wasted room) — the pages
1628
- run down to the bar, the dots sit on their bottom edge, centred, on the base's ground so a line passing under stays clear of them */
1629
- position: absolute; bottom: 0; left: 50%; translate: -50% 0; z-index: 2;
1630
- padding: var(--sp-3) var(--sp-2);
1631
- border-radius: var(--r-chip);
1632
- background: var(--surface);
1633
- }
1634
- /* the reading block fills the step, and keeps the dots' height free at the end of its scroll so the last line clears them */
1635
- .step:has(> .pager__at) > :is(.base-content, .pages) { flex: 1 1 auto; padding-bottom: calc(var(--dot) + 2 * var(--sp-3)); }
1636
- .step > :is(.base-content, .pages) {
1625
+ .step > :is(.page, .pages) {
1637
1626
  min-height: 0;
1638
1627
  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 */
1639
1628
  overflow-y: auto;
@@ -1643,8 +1632,8 @@ button { font: inherit; cursor: pointer; }
1643
1632
  /* a picture that opens the words stays put, only the words scroll under it (Lh 2026-09-23, timebank's walk at large text:
1644
1633
  the picture scrolled away with the words). Sticky at the top of the reading block's scroller; its height is
1645
1634
  --base-content-media-h as everywhere, never shrunk (Lh 2026-09-23: a fixed height, no scaling). */
1646
- .step > .base-content > :is(img, .ph):first-child,
1647
- .step > .pages > .base-content > :is(img, .ph):first-child {
1635
+ .step > .page > :is(img, .ph):first-child,
1636
+ .step > .pages > .page > :is(img, .ph):first-child {
1648
1637
  position: sticky;
1649
1638
  top: 0;
1650
1639
  z-index: 1;
@@ -1652,23 +1641,20 @@ button { font: inherit; cursor: pointer; }
1652
1641
  }
1653
1642
 
1654
1643
  /* ============================================================
1655
- ONE FLEX COLUMN (Lh 2026-09-23: every part of a screen is laid by flex). In a .screen--fill the bar is IN the
1656
- column, after the step: the step takes what is left (flex: 1), the bar takes what its rows need — measured by the
1657
- browser, no --bar-reserve arithmetic, so a one-row bar gives its room straight back to the pages. The keyboard lifts
1658
- the column's end: the screen pads its bottom by --kb, the bar rides on it.
1644
+ TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
1645
+ in a .screen--fill the content runs to the screen's bottom edge, and the bar lies over it at the foot — it covers what is
1646
+ under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
1659
1647
  (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
1660
1648
  ============================================================ */
1661
1649
  .screen--fill > .action-bar {
1662
- position: relative; inset: auto; bottom: auto;
1663
- flex: none;
1664
- margin-top: auto; /* the column's end, even on a screen with nothing that grows (a centred card is absolute) */
1650
+ position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
1665
1651
  max-width: none;
1666
- margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge, as the step; the bar's own padding keeps the inset */
1652
+ margin: 0;
1653
+ padding-top: 0; /* no ground of its own, never a solid strip */
1667
1654
  }
1668
- .screen--fill:has(> .action-bar) { padding-bottom: var(--kb, 0rem); }
1669
1655
  .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
1670
- /* the step and its bar touch: the screen's gap between two stacked things is not for these two (the bar's own padding is its room) */
1671
- .screen--fill:has(> .step) > .action-bar { margin-top: calc(-1 * var(--screen-gap)); }
1656
+ /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
1657
+ .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
1672
1658
  /* the TOP is in the column too (Lh 2026-09-23): an .appbar in a fill screen is its first row, not fixed over it — the screen
1673
1659
  reserves nothing for it; the breathing under it stays the one it had (--sp-2), not the screen's gap */
1674
1660
  .screen--fill > .appbar {
@@ -1679,6 +1665,9 @@ button { font: inherit; cursor: pointer; }
1679
1665
  margin-bottom: calc(var(--sp-2) - var(--screen-gap));
1680
1666
  }
1681
1667
  .screen.screen--fill:has(> .appbar) { padding-top: 0; --header-gap: 0rem; }
1668
+ /* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
1669
+ them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
1670
+ .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
1682
1671
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
1683
1672
 
1684
1673
  /* ---- css/forms.css — outlined fields, add-row ---- */
@@ -2099,7 +2088,7 @@ button { font: inherit; cursor: pointer; }
2099
2088
  shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
2100
2089
  .action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
2101
2090
  .action-bar__tier > .btn { flex: 1; }
2102
- .action-bar__tier--link { justify-content: center; } /* a Link under the buttons, e.g. "details" that opens a Modal */
2091
+ .action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
2103
2092
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2104
2093
  /* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
2105
2094
  adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
@@ -3004,6 +2993,10 @@ button { font: inherit; cursor: pointer; }
3004
2993
  padding: 0;
3005
2994
  cursor: pointer;
3006
2995
  }
2996
+ /* a SMALL link (Lh 2026-09-23): a thing tapped rarely — "details", a ctaLink — at the minor-link size (15) and 28px of height;
2997
+ its 44px tap target stays, an invisible ::after around it */
2998
+ .link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
2999
+ .link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
3007
3000
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
3008
3001
  .card--3 .link--muted { color: var(--card-3-muted); }
3009
3002
 
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export * from './scaffold/ActionBar.js';
10
10
  export * from './scaffold/PagerAt.js';
11
11
  export * from './scaffold/FloatBar.js';
12
12
  export * from './scaffold/FilterRow.js';
13
- export * from './scaffold/BaseContent.js';
13
+ export * from './scaffold/Page.js';
14
14
  export * from './scaffold/EmptyState.js';
15
15
  export * from './scaffold/Centre.js';
16
16
  export * from './cards/Card.js';
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export * from './scaffold/ActionBar.js';
13
13
  export * from './scaffold/PagerAt.js';
14
14
  export * from './scaffold/FloatBar.js';
15
15
  export * from './scaffold/FilterRow.js';
16
- export * from './scaffold/BaseContent.js';
16
+ export * from './scaffold/Page.js';
17
17
  export * from './scaffold/EmptyState.js';
18
18
  export * from './scaffold/Centre.js';
19
19
  export * from './cards/Card.js';
@@ -17,7 +17,7 @@ export interface PagesProps extends ComponentProps<'div'> {
17
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
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
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `Page`s, as a Step's
21
21
  * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
22
22
  */
23
23
  export declare function Pages({ className, ...rest }: PagesProps): import("react").JSX.Element;
@@ -17,7 +17,7 @@ export function Centre({ className, ...rest }) {
17
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
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
20
+ * Pages of cards: `Card`s, inside a Centre, above the PagerAt. Pages of words: `Page`s, as a Step's
21
21
  * reading block (no Sheet on that screen — a pager never works beside one), the round pager in the ActionBar.
22
22
  */
23
23
  export function Pages({ className, ...rest }) {
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps } from 'react';
2
+ export interface PageProps extends ComponentProps<'section'> {
3
+ }
4
+ /**
5
+ * Page — ONE page of words in the content: a picture (optional, first — it stays put while the words scroll), a heading, the words,
6
+ * maybe a CtaPack. A component INSIDE the content box, never the box itself (Lh 2026-09-23): several Pages in a `Pages` = a walk the
7
+ * pager turns (slideTo); a single one sits in the content (a Step) directly. Was BaseContent until 2026-09-23 — "words on the base" meant
8
+ * a still, one-scaffold screen, which is gone.
9
+ */
10
+ export declare function Page({ className, ...rest }: PageProps): import("react").JSX.Element;
11
+ /** @deprecated renamed `Page` (2026-09-23) — same props, same markup; kept one version for the move */
12
+ export declare function BaseContent(props: PageProps): import("react").JSX.Element;
13
+ export type BaseContentProps = PageProps;
14
+ export interface CtaPackProps extends ComponentProps<'div'> {
15
+ }
16
+ /** CtaPack — the buttons under a Page's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
17
+ export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /**
4
+ * Page — ONE page of words in the content: a picture (optional, first — it stays put while the words scroll), a heading, the words,
5
+ * maybe a CtaPack. A component INSIDE the content box, never the box itself (Lh 2026-09-23): several Pages in a `Pages` = a walk the
6
+ * pager turns (slideTo); a single one sits in the content (a Step) directly. Was BaseContent until 2026-09-23 — "words on the base" meant
7
+ * a still, one-scaffold screen, which is gone.
8
+ */
9
+ export function Page({ className, ...rest }) {
10
+ return _jsx("section", { className: cx('page', className), ...rest });
11
+ }
12
+ let warned = false;
13
+ /** @deprecated renamed `Page` (2026-09-23) — same props, same markup; kept one version for the move */
14
+ export function BaseContent(props) {
15
+ if (!warned) {
16
+ warned = true;
17
+ console.warn('cardds: BaseContent is now Page — rename it (same props).');
18
+ }
19
+ return _jsx(Page, { ...props });
20
+ }
21
+ /** CtaPack — the buttons under a Page's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
22
+ export function CtaPack({ className, ...rest }) {
23
+ return _jsx("div", { className: cx('cta-pack', className), ...rest });
24
+ }
@@ -8,15 +8,15 @@ export interface ScreenProps extends ComponentProps<'div'> {
8
8
  as?: 'div' | 'main' | 'section';
9
9
  /** TOP: a `TopBar` / `AppBar`, or `"lead"` — no bar, the content starts `--screen-lead` (15% of the screen) down */
10
10
  top?: ReactNode | 'lead';
11
- /** BOTTOM: an `ActionBar` (or a `FloatBar`) — laid at the end of the screen's flex column, as tall as its rows need.
11
+ /** BOTTOM: an `ActionBar` (or a `FloatBar`) — a LAYER over the content's foot, as tall as its rows need.
12
12
  * Never with a `Sheet` in the content: a sheet finishes on its own foot (warns once). */
13
13
  bottom?: ReactNode;
14
14
  }
15
15
  /**
16
- * Screen — the mobile screen, THREE parts in one flex column: **top** · **content** (the children) · **bottom**.
17
- * Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start 15% down). Content = a `Step` (`Pages` / `BaseContent`
18
- * [+ `Sheet`]), a `Centre`, a `SheetStack`… Bottom = an `ActionBar` (`FloatBar`), measured by the browser: the content takes
19
- * all the rest (`fill`). Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
16
+ * Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
17
+ * screen's foot; **bottom** a LAYER over it there (Lh 2026-09-23). Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start
18
+ * 15% down). Content = a box of components one after another — a `Step` (`Pages` of `Page`s / a `Page` [+ `Sheet`]), a `Centre`, a
19
+ * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
20
20
  * The slots set the order; children laid by hand in the same order still work.
21
21
  */
22
22
  export declare function Screen({ fill, palette, as, top, bottom, className, children, ref, ...rest }: ScreenProps): import("react").DetailedReactHTMLElement<{
@@ -2,10 +2,10 @@ import { createElement, useEffect, useRef } from 'react';
2
2
  import { cx } from '../cx.js';
3
3
  let warnedSheetBar = false;
4
4
  /**
5
- * Screen — the mobile screen, THREE parts in one flex column: **top** · **content** (the children) · **bottom**.
6
- * Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start 15% down). Content = a `Step` (`Pages` / `BaseContent`
7
- * [+ `Sheet`]), a `Centre`, a `SheetStack`… Bottom = an `ActionBar` (`FloatBar`), measured by the browser: the content takes
8
- * all the rest (`fill`). Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
5
+ * Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
6
+ * screen's foot; **bottom** a LAYER over it there (Lh 2026-09-23). Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start
7
+ * 15% down). Content = a box of components one after another — a `Step` (`Pages` of `Page`s / a `Page` [+ `Sheet`]), a `Centre`, a
8
+ * `SheetStack`… Bottom = an `ActionBar` (the walk's `PagerAt` its first row) or a `FloatBar`, covering the content's foot on purpose. Max 25rem wide, the standard inset, no vertical padding. A plain Screen (no `fill`) scrolls.
9
9
  * The slots set the order; children laid by hand in the same order still work.
10
10
  */
11
11
  export function Screen({ fill, palette, as = 'div', top, bottom, className, children, ref, ...rest }) {
@@ -2,9 +2,9 @@ import type { ComponentProps } from 'react';
2
2
  export interface StepProps extends ComponentProps<'section'> {
3
3
  }
4
4
  /**
5
- * Step — one screen, one task. Children: a BaseContent first (what the member READS — words on the base, not a card), an optional Sheet last
5
+ * Step — one screen, one task. Children: a Page first (what the member READS — words on the base, not a card), an optional Sheet last
6
6
  * (what the member DOES); the ActionBar is the Screen's `bottom`, after the Step. It is a `Screen fill`'s content: it takes
7
7
  * all the height the top and the bottom leave (one flex column), bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
8
- * Reads → BaseContent only. Acts → BaseContent + Sheet. Never hand-lay a step, never write a sheet height.
8
+ * Reads → Page only. Acts → Page + Sheet. Never hand-lay a step, never write a sheet height.
9
9
  */
10
10
  export declare function Step({ className, ...rest }: StepProps): import("react").JSX.Element;
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * Step — one screen, one task. Children: a BaseContent first (what the member READS — words on the base, not a card), an optional Sheet last
4
+ * Step — one screen, one task. Children: a Page first (what the member READS — words on the base, not a card), an optional Sheet last
5
5
  * (what the member DOES); the ActionBar is the Screen's `bottom`, after the Step. It is a `Screen fill`'s content: it takes
6
6
  * all the height the top and the bottom leave (one flex column), bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
7
- * Reads → BaseContent only. Acts → BaseContent + Sheet. Never hand-lay a step, never write a sheet height.
7
+ * Reads → Page only. Acts → Page + Sheet. Never hand-lay a step, never write a sheet height.
8
8
  */
9
9
  export function Step({ className, ...rest }) {
10
10
  return _jsx("section", { className: cx('step', className), ...rest });
@@ -28,7 +28,7 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
28
28
  open?: number | null;
29
29
  defaultOpen?: number | null;
30
30
  onOpenChange?: (open: number | null) => void;
31
- /** CONTENT above the sheet heads — one `BaseContent`: a welcome, the news, a picture. It reads
31
+ /** CONTENT above the sheet heads — one `Page`: a welcome, the news, a picture. It reads
32
32
  * from the top; a sheet coming up covers it */
33
33
  base?: ReactNode;
34
34
  /** keep a sheet's body mounted after it closes (it opens again at once, scrolled where it was); default: dropped */
@@ -25,7 +25,7 @@ export interface WalkthroughProps extends Omit<ComponentProps<typeof Screen>, 'c
25
25
  cta?: ReactNode | ((c: WalkthroughCta) => ReactNode);
26
26
  /** rows of controls ABOVE the CTA row (a text-size Slider…), one row each, top to bottom — keep them few */
27
27
  controls?: ReactNode[];
28
- /** a line UNDER the row: a `Link` (e.g. "developer details" that opens a Modal) — centred, one line */
28
+ /** a line UNDER the row: a `Link sm` (e.g. "developer details" that opens a Modal) — centred, one short line */
29
29
  ctaLink?: ReactNode;
30
30
  /** layers over the screen: the app's `SheetStage dim` + `Modal` that a link or button opens (the app wires it) */
31
31
  children?: ReactNode;
@@ -34,7 +34,7 @@ export interface WalkthroughProps extends Omit<ComponentProps<typeof Screen>, 'c
34
34
  nextLabel?: string;
35
35
  }
36
36
  /**
37
- * Walkthrough — TEMPLATE, a `Screen` with its three parts filled — top: `lead` · content: `Pages` · bottom: `ActionBar`.
37
+ * Walkthrough — TEMPLATE, a `Screen` with its three parts filled — top: `lead` · content: `Pages` (down to the screen's foot) · bottom: a LAYER over it — the dots, then the `ActionBar` rows.
38
38
  * A walk of pages that tell (onboarding, an introduction). Each page = a picture, a heading,
39
39
  * the words; the dots at the foot; one row of calls to action at the bottom. No top bar. Fixed: the order, the picture
40
40
  * first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useRef, useState } from 'react';
3
3
  import { Screen } from '../scaffold/Screen.js';
4
4
  import { Step } from '../sheets/Step.js';
5
- import { BaseContent } from '../scaffold/BaseContent.js';
5
+ import { Page } from '../scaffold/Page.js';
6
6
  import { Pages, slideTo } from '../scaffold/Centre.js';
7
7
  import { PagerAt } from '../scaffold/PagerAt.js';
8
8
  import { ActionBar, ActionBarTier } from '../scaffold/ActionBar.js';
@@ -10,7 +10,7 @@ import { CardHead } from '../cards/CardHead.js';
10
10
  import { Text } from '../type/Text.js';
11
11
  import { IconBtn } from '../actions/IconBtn.js';
12
12
  /**
13
- * Walkthrough — TEMPLATE, a `Screen` with its three parts filled — top: `lead` · content: `Pages` · bottom: `ActionBar`.
13
+ * Walkthrough — TEMPLATE, a `Screen` with its three parts filled — top: `lead` · content: `Pages` (down to the screen's foot) · bottom: a LAYER over it — the dots, then the `ActionBar` rows.
14
14
  * A walk of pages that tell (onboarding, an introduction). Each page = a picture, a heading,
15
15
  * the words; the dots at the foot; one row of calls to action at the bottom. No top bar. Fixed: the order, the picture
16
16
  * first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
@@ -29,6 +29,7 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
29
29
  const row = typeof cta === 'function' ? cta({ at, count, go }) : cta;
30
30
  const custom = row !== undefined && row !== null;
31
31
  const pager = (_jsxs(_Fragment, { children: [_jsx(IconBtn, { icon: "arrow-left", label: backLabel, disabled: at === 0, onClick: () => go(at - 1) }), _jsx(IconBtn, { invert: true, icon: "arrow-right", label: nextLabel, disabled: at === count - 1, onClick: () => go(at + 1) })] }));
32
- const bottom = controls.length || ctaLink != null ? (_jsxs(ActionBar, { variant: "tiers", children: [controls.map((c, i) => _jsx(ActionBarTier, { children: c }, i)), _jsx(ActionBarTier, { pager: true, children: custom ? row : pager }), ctaLink != null && _jsx(ActionBarTier, { className: "action-bar__tier--link", children: ctaLink })] })) : (_jsx(ActionBar, { variant: "pager", children: custom ? row : pager }));
33
- return (_jsxs(Screen, { fill: true, top: "lead", bottom: bottom, ...rest, children: [_jsxs(Step, { ref: step, children: [_jsx(Pages, { children: pages.map((p, i) => (_jsxs(BaseContent, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }), count > 1 && _jsx(PagerAt, { at: at + 1, of: count })] }), children] }));
32
+ const dots = count > 1 ? _jsx(PagerAt, { at: at + 1, of: count }) : null;
33
+ const bottom = dots || controls.length || ctaLink != null ? (_jsxs(ActionBar, { variant: "tiers", children: [dots, controls.map((c, i) => _jsx(ActionBarTier, { children: c }, i)), _jsx(ActionBarTier, { pager: true, children: custom ? row : pager }), ctaLink != null && _jsx(ActionBarTier, { className: "action-bar__tier--link", children: ctaLink })] })) : (_jsx(ActionBar, { variant: "pager", children: custom ? row : pager }));
34
+ return (_jsxs(Screen, { fill: true, top: "lead", bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
34
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
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.",
@@ -1,17 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- export interface BaseContentProps extends ComponentProps<'section'> {
3
- }
4
- /**
5
- * BaseContent — words straight on the base, no card around them: a heading, a paragraph,
6
- * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
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 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 WAYS IN
10
- * (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
11
- * cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
12
- */
13
- export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
14
- export interface CtaPackProps extends ComponentProps<'div'> {
15
- }
16
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
17
- export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -1,18 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /**
4
- * BaseContent — words straight on the base, no card around them: a heading, a paragraph,
5
- * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
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 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 WAYS IN
9
- * (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
10
- * cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
11
- */
12
- export function BaseContent({ className, ...rest }) {
13
- return _jsx("section", { className: cx('base-content', className), ...rest });
14
- }
15
- /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
16
- export function CtaPack({ className, ...rest }) {
17
- return _jsx("div", { className: cx('cta-pack', className), ...rest });
18
- }