@usableapp/cardds 0.1.5 → 0.1.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/README.md CHANGED
@@ -29,7 +29,7 @@ npm test # Playwright: the step/sheet/centre geometry on tests/fixtu
29
29
  ```
30
30
 
31
31
  ```tsx
32
- import { Screen, TopBar, Step, StepCard, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from 'cardds';
32
+ import { Screen, TopBar, Step, BaseContent, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from 'cardds';
33
33
  import 'cardds/dist/cardds.css'; // or cardds/cardds.css with the css/ folder beside it
34
34
  ```
35
35
 
@@ -187,16 +187,16 @@ whose markup has rules, because the sheet's box is the step itself:
187
187
  ```html
188
188
  <!-- reads: the words only -->
189
189
  <section class="step">
190
- <article class="step__card">
190
+ <section class="base-content">
191
191
  <div class="card__head"><span class="t-overline t-muted">ขั้นที่ 2 จาก 4</span><h2 class="t-h2">…</h2></div>
192
192
  <p class="t-body">…</p>
193
- </article>
193
+ </section>
194
194
  </section>
195
195
  <div class="action-bar action-bar--pager">…</div>
196
196
 
197
197
  <!-- acts: the words + a sheet -->
198
198
  <section class="step">
199
- <article class="step__card">…the words…</article>
199
+ <section class="base-content">…the words…</section>
200
200
  <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
201
201
  <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
202
202
  <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
@@ -219,7 +219,7 @@ whose markup has rules, because the sheet's box is the step itself:
219
219
  card takes the inset back as its side margins — so the card and the bar's
220
220
  buttons stand on the inset line, and the sheet runs edge to edge like the
221
221
  bar and like a sheet in a `.sheet-stage`.
222
- - **`.step__card` first, `.sheet` last.** The block carries the step's words
222
+ - **`.base-content` first, `.sheet` last.** The block carries the step's words
223
223
  ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
224
224
  Lh: a step reads like a page, not a card in a page). It keeps a card's
225
225
  rhythm: a `.card__head`, `--card-gap` between things. The sheet is
@@ -298,14 +298,14 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
298
298
  | `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
299
  | `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 |
300
300
  | `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
301
- | `css/base.css` | reset, surface, typography classes, `.section` | rarely |
301
+ | `css/base.css` | reset, surface, typography classes, `.base-content` | rarely |
302
302
  | `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 |
303
303
  | `css/stack.css` | **sheet stack** — `.sheet-stack`: the menu, sheets stacked (heads peek, up to 5; `--tap` tap-to-open, the back button dissolves in; `--closed` nothing open, heads at the bottom, the base above (`.sheet-stack__base`); `--display` the title is the peek, `--fanned` wider peeks); always edge to edge; a stacked sheet is a `.card` | rarely |
304
304
  | `css/sheet.css` | **sheet single** — `.sheet`: a card as bottom sheet, full height always, a state is how far it slid: `--peek` / `--half` / `--full` (translate only); handle · head · `.sheet__body` (the head pinned, the body scrolls); `.sheet-stage` (overlay stage: map sheet, a `__hero` — one thing centred in the band a half sheet leaves open, `--dim`, `--raised`, ask drawer) | when adding states |
305
- | `css/step.css` | `.step` — one screen, one task: `.step__card` (reads) + optional `.sheet` (acts), the bar's room, the keyboard's room | rarely |
305
+ | `css/step.css` | `.step` — one screen, one task: `.base-content` (reads) + optional `.sheet` (acts), the bar's room, the keyboard's room | rarely |
306
306
  | `css/forms.css` | `.field` (outlined input, label = placeholder), `.add-row` | when adding controls |
307
307
  | `css/journey.css` | `.route`, `.tile-badge`, `.note-row` — trip/status primitives | when adding variants |
308
- | `css/actions.css` | topbar, chips, buttons (`.btn--xl`), `.dropdown` (the standard select: pill + `.menu` popover), `.action-bar` (+ `--pager` for previous/next, as labels or as two round buttons; `--tiers` for a control row above the buttons), `.fab` | when adding actions |
308
+ | `css/actions.css` | topbar, chips, buttons (`.btn--xl`), `.dropdown` (the standard select: pill + `.menu` popover), `.action-bar` (+ `--pager` for previous/next, two round icon buttons, never labels; `--tiers` for a control row above the buttons), `.fab` | when adding actions |
309
309
  | `css/numbers.css` | `.ring` gauge, `.track` steps, `.card--band` + `.band-stack`, `.dotgrid`, `.picker`, `.badge`, `.bars` | big numbers |
310
310
  | `css/people.css` | `.avatar` (sm/lg/xl, outline, add, on, halo), `.avatar-stack`, `.avatar-pick` | people |
311
311
  | `css/choice.css` | `.check`, `.toggle`, `.chip--toggle`, `.chip-grid` + `.chip--pick`, `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
@@ -366,7 +366,7 @@ Every screen is the same four things, and every card the same three:
366
366
  by `translate` only. (The stack was first named "card stack": wrong, and
367
367
  retired.) Demos: `sheet.html`, `stack.html`.
368
368
  - **A step is one screen, one task: `.step`.** What the member reads is
369
- words on the base (`.step__card` — the name stayed, the frame went); what the member does, if the step asks for it, is a
369
+ 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
370
370
  `.sheet` in front of it; the screen's one move is `.action-bar`, outside
371
371
  the step, below it. A step where the member only READS is the card alone.
372
372
  A step where the member ACTS (types, picks, shoots a photo, records) adds
@@ -393,7 +393,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
393
393
  1. **One card = one boundary.** A card frames one thing: a category, a stat
394
394
  group, a list row. Tone slots: `.card` (card-1), `.card--2`, `.card--3`.
395
395
  2. **Sections are not cards.** Page sections = frameless on-surface content
396
- (`.section`) + a lone/packed CTA (`.cta-pack`).
396
+ (`.base-content`) + a lone/packed CTA (`.cta-pack`).
397
397
  3. **Surrounding UI is separate.** `.topbar`, `.filter-row`, `.chip-row`,
398
398
  `.action-bar` live outside cards, above/below. The bottom bar has two
399
399
  arrangements and they mean different things: the default pack (one wide
package/css/actions.css CHANGED
@@ -218,35 +218,27 @@
218
218
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
219
219
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
220
220
 
221
- /* ---- pager: previous / next at the two edges of the bar ----
221
+ /* ---- pager: previous / next as two round icon buttons at the two edges ----
222
222
  For a sequence you can walk BOTH ways — a three-card feature intro, a step
223
223
  flow. Use it instead of the default pack whenever going back is as ordinary
224
224
  as going on: a wide CTA beside a small round icon reads as one action plus
225
225
  an afterthought, which is wrong when the two are peers.
226
226
 
227
- Both buttons are the same shape and the same height, so neither wins by
228
- being physically bigger; the fill alone leans forward. The bar carries the
229
- buttons only: where you are is said by the .pager__at dots UNDER the card
227
+ The two are the same round shape and size (.icon-btn), one at each edge in
228
+ thumb reach from either side; the fill (--invert) alone leans forward. They
229
+ NEVER carry words (Lh 2026-09-16, for good): an arrow is the whole meaning,
230
+ and a word that isn't a direction ("Get started", "Done") is not a pager
231
+ button — that step COMMITS and goes back to the plain .action-bar with one
232
+ wide button. Where you are is said by the .pager__at dots UNDER the card
230
233
  that changes (a .screen__centre group, card.css), never in the bar — the
231
234
  dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
232
- "skip" link or nothing. Keep the last step of a flow on the plain
233
- .action-bar: a step that COMMITS deserves one wide button, not a pair.
235
+ "skip" link or nothing.
234
236
 
235
237
  <div class="action-bar action-bar--pager">
236
- <button class="btn" disabled><svg class="icon icon--xs"><use href="icons.svg#arrow-left"/></svg>Back</button>
237
- <button class="btn btn--primary">Next<svg class="icon icon--xs"><use href="icons.svg#arrow-right"/></svg></button>
238
- </div> */
238
+ <button class="icon-btn" aria-label="ย้อนกลับ" disabled>…</button>
239
+ <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
240
+ </div> — or the same two in an .action-bar__tier--pager under a control */
239
241
  .action-bar--pager { align-items: center; }
240
- /* equal share of the bar, and tighter than a lone CTA so two labels plus the
241
- indicator still fit a 320px screen */
242
- .action-bar--pager > .btn {
243
- flex: 1 1 0;
244
- min-width: 0;
245
- padding-inline: var(--sp-3);
246
- gap: var(--sp-1);
247
- white-space: nowrap;
248
- overflow: hidden;
249
- }
250
242
  /* the page dots: under the card in a .screen__centre group; tight gaps */
251
243
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
252
244
  .pager__at > * {
@@ -274,14 +266,7 @@
274
266
  .action-bar__tier > .btn { flex: 1; }
275
267
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
276
268
 
277
- /* ---- round pager: previous / next as two round buttons at the two edges ----
278
- The pager with nothing to say between its buttons — no dots, no label — as
279
- two equal round icon buttons, one at each edge, in thumb reach from either
280
- side. Rule 3 still holds: the step that commits goes back to one wide button.
281
- <div class="action-bar action-bar--pager">
282
- <button class="icon-btn" aria-label="ย้อนกลับ">…</button>
283
- <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
284
- </div> — or the same two in an .action-bar__tier--pager under a control */
269
+ /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
285
270
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
286
271
 
287
272
  /* .screen's default bottom padding is a constant; derive the reserve from the
package/css/base.css CHANGED
@@ -3,6 +3,17 @@
3
3
  ============================================================ */
4
4
 
5
5
  *, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
6
+ /* a control is a thing you press, not a thing you read: a long press never
7
+ raises the text-selection bar on it (Lh 2026-09-16). CONTROLS ONLY — words
8
+ in cards, rows and bodies stay selectable (an address, a code, a number
9
+ the member wants to copy), and inputs are untouched. Native buttons and
10
+ links are matched by element; the rest by the class that makes them one */
11
+ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--toggle, .chip--pick,
12
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .dropdown__trigger, .track__step, .pager__at {
13
+ user-select: none;
14
+ -webkit-user-select: none;
15
+ -webkit-touch-callout: none;
16
+ }
6
17
 
7
18
  /* the scale: everything is rem, so this one number resizes the whole system.
8
19
  100% = the user's text size (16px by default); narrow phones step to 15/16 so
@@ -49,6 +60,7 @@ button { font: inherit; cursor: pointer; }
49
60
  padding-inline: var(--screen-pad);
50
61
  display: grid;
51
62
  gap: var(--screen-gap);
63
+ align-content: start; /* a screen taller than its content (a fixed phone) never spreads its rows — the top bar stays at the top, a row keeps its height */
52
64
  }
53
65
  .screen > * { min-width: 0; } /* content never widens the column past the viewport */
54
66
  /* a screen that carries its own palette (data-palette on the element, not on <html>)
@@ -95,15 +107,20 @@ button { font: inherit; cursor: pointer; }
95
107
  .t-caps { text-transform: uppercase; }
96
108
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
97
109
 
98
- /* ---- section: frameless on-surface content + CTA (never a card) ---- */
99
- .section {
110
+ /* ---- base content: words straight on the base, no card around them — an
111
+ overline, a heading, a paragraph, maybe a .cta-pack. Was .section until
112
+ 2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
113
+ In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
114
+ .base-content {
100
115
  display: grid;
101
116
  gap: var(--sp-3);
117
+ align-content: start; /* a tall column never stretches the rows — the words hug the top */
102
118
  padding-block: var(--sp-4);
103
119
  }
104
- .section > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
105
- .section .cta-pack {
120
+ .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
121
+ .base-content .cta-pack {
106
122
  display: flex;
123
+ align-items: start; /* buttons keep their own height, never the row's */
107
124
  gap: var(--sp-2);
108
125
  margin-top: var(--sp-2);
109
126
  flex-wrap: wrap;
@@ -116,12 +133,9 @@ button { font: inherit; cursor: pointer; }
116
133
  text-align: center;
117
134
  gap: var(--sp-2);
118
135
  padding: var(--sp-12) var(--sp-6);
119
- color: var(--on-surface-muted);
136
+ color: var(--on-surface); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
120
137
  }
121
- .empty-state > :is(h1, h2, h3) { color: var(--on-surface); } /* the heading in full ink, the words muted */
122
138
  .empty-state__action { margin-top: var(--sp-2); }
123
- /* inside a card (a section's "nothing yet"): the card's ink, the way .t-muted and .row__meta follow it */
124
- .card .empty-state { color: var(--card-muted); padding-block: var(--sp-6); }
125
- .card .empty-state > :is(h1, h2, h3) { color: var(--card-ink); }
139
+ /* inside a card (a section's "nothing yet"): the card's ink */
140
+ .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
126
141
  .card--3 .empty-state { color: inherit; }
127
- .card--3 .empty-state > :not(:is(h1, h2, h3)) { opacity: 0.7; }
package/css/step.css CHANGED
@@ -5,7 +5,7 @@
5
5
  move (.action-bar, actions.css) OUTSIDE the step, below it.
6
6
 
7
7
  <section class="step">
8
- <article class="step__card">…the words…</article> ← first
8
+ <section class="base-content">…the words…</section> ← first
9
9
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
10
10
  </section>
11
11
  <div class="action-bar">…</div>
@@ -59,17 +59,19 @@
59
59
  frame — the bar is fixed to the viewport wherever it sits. */
60
60
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
61
61
 
62
- /* the reading block: the words on the base — no frame, no area colour (it was
63
- a .card until 2026-09-16; Lh: a step reads like a page, not a card in a
64
- page). It keeps a card's inner rhythm (a head, --card-gap between things,
65
- the tokens .card publishes for the head and .bleed) and the flex rule: it
66
- hugs its content, and when the column runs out (a short phone, large text)
67
- it shrinks and scrolls inside itself instead of pushing the bar or the sheet */
68
- .step__card {
62
+ /* the reading block: a .base-content (base.css) — the same words-on-the-base
63
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
64
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
65
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
66
+ between things, the tokens .card publishes for the head and .bleed) and
67
+ gets the flex rule: it hugs its content, and when the column runs out (a
68
+ short phone, large text) it shrinks and scrolls inside itself instead of
69
+ pushing the bar or the sheet */
70
+ .step > .base-content {
69
71
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
70
72
  --card-gap: var(--sp-4);
71
- display: grid;
72
73
  gap: var(--card-gap);
74
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
73
75
  align-content: start;
74
76
  color: var(--on-surface);
75
77
  flex: 0 1 auto;
package/dist/cardds.css CHANGED
@@ -339,6 +339,17 @@
339
339
  ============================================================ */
340
340
 
341
341
  *, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
342
+ /* a control is a thing you press, not a thing you read: a long press never
343
+ raises the text-selection bar on it (Lh 2026-09-16). CONTROLS ONLY — words
344
+ in cards, rows and bodies stay selectable (an address, a code, a number
345
+ the member wants to copy), and inputs are untouched. Native buttons and
346
+ links are matched by element; the rest by the class that makes them one */
347
+ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--toggle, .chip--pick,
348
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .dropdown__trigger, .track__step, .pager__at {
349
+ user-select: none;
350
+ -webkit-user-select: none;
351
+ -webkit-touch-callout: none;
352
+ }
342
353
 
343
354
  /* the scale: everything is rem, so this one number resizes the whole system.
344
355
  100% = the user's text size (16px by default); narrow phones step to 15/16 so
@@ -385,6 +396,7 @@ button { font: inherit; cursor: pointer; }
385
396
  padding-inline: var(--screen-pad);
386
397
  display: grid;
387
398
  gap: var(--screen-gap);
399
+ align-content: start; /* a screen taller than its content (a fixed phone) never spreads its rows — the top bar stays at the top, a row keeps its height */
388
400
  }
389
401
  .screen > * { min-width: 0; } /* content never widens the column past the viewport */
390
402
  /* a screen that carries its own palette (data-palette on the element, not on <html>)
@@ -431,15 +443,20 @@ button { font: inherit; cursor: pointer; }
431
443
  .t-caps { text-transform: uppercase; }
432
444
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
433
445
 
434
- /* ---- section: frameless on-surface content + CTA (never a card) ---- */
435
- .section {
446
+ /* ---- base content: words straight on the base, no card around them — an
447
+ overline, a heading, a paragraph, maybe a .cta-pack. Was .section until
448
+ 2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
449
+ In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
450
+ .base-content {
436
451
  display: grid;
437
452
  gap: var(--sp-3);
453
+ align-content: start; /* a tall column never stretches the rows — the words hug the top */
438
454
  padding-block: var(--sp-4);
439
455
  }
440
- .section > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
441
- .section .cta-pack {
456
+ .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
457
+ .base-content .cta-pack {
442
458
  display: flex;
459
+ align-items: start; /* buttons keep their own height, never the row's */
443
460
  gap: var(--sp-2);
444
461
  margin-top: var(--sp-2);
445
462
  flex-wrap: wrap;
@@ -452,15 +469,12 @@ button { font: inherit; cursor: pointer; }
452
469
  text-align: center;
453
470
  gap: var(--sp-2);
454
471
  padding: var(--sp-12) var(--sp-6);
455
- color: var(--on-surface-muted);
472
+ color: var(--on-surface); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
456
473
  }
457
- .empty-state > :is(h1, h2, h3) { color: var(--on-surface); } /* the heading in full ink, the words muted */
458
474
  .empty-state__action { margin-top: var(--sp-2); }
459
- /* inside a card (a section's "nothing yet"): the card's ink, the way .t-muted and .row__meta follow it */
460
- .card .empty-state { color: var(--card-muted); padding-block: var(--sp-6); }
461
- .card .empty-state > :is(h1, h2, h3) { color: var(--card-ink); }
475
+ /* inside a card (a section's "nothing yet"): the card's ink */
476
+ .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
462
477
  .card--3 .empty-state { color: inherit; }
463
- .card--3 .empty-state > :not(:is(h1, h2, h3)) { opacity: 0.7; }
464
478
 
465
479
  /* ---- css/card.css — card boundary + list variants ---- */
466
480
  /* ============================================================
@@ -1329,7 +1343,7 @@ button { font: inherit; cursor: pointer; }
1329
1343
  move (.action-bar, actions.css) OUTSIDE the step, below it.
1330
1344
 
1331
1345
  <section class="step">
1332
- <article class="step__card">…the words…</article> ← first
1346
+ <section class="base-content">…the words…</section> ← first
1333
1347
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1334
1348
  </section>
1335
1349
  <div class="action-bar">…</div>
@@ -1383,17 +1397,19 @@ button { font: inherit; cursor: pointer; }
1383
1397
  frame — the bar is fixed to the viewport wherever it sits. */
1384
1398
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1385
1399
 
1386
- /* the reading block: the words on the base — no frame, no area colour (it was
1387
- a .card until 2026-09-16; Lh: a step reads like a page, not a card in a
1388
- page). It keeps a card's inner rhythm (a head, --card-gap between things,
1389
- the tokens .card publishes for the head and .bleed) and the flex rule: it
1390
- hugs its content, and when the column runs out (a short phone, large text)
1391
- it shrinks and scrolls inside itself instead of pushing the bar or the sheet */
1392
- .step__card {
1400
+ /* the reading block: a .base-content (base.css) — the same words-on-the-base
1401
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
1402
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
1403
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
1404
+ between things, the tokens .card publishes for the head and .bleed) and
1405
+ gets the flex rule: it hugs its content, and when the column runs out (a
1406
+ short phone, large text) it shrinks and scrolls inside itself instead of
1407
+ pushing the bar or the sheet */
1408
+ .step > .base-content {
1393
1409
  --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1394
1410
  --card-gap: var(--sp-4);
1395
- display: grid;
1396
1411
  gap: var(--card-gap);
1412
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
1397
1413
  align-content: start;
1398
1414
  color: var(--on-surface);
1399
1415
  flex: 0 1 auto;
@@ -1747,35 +1763,27 @@ button { font: inherit; cursor: pointer; }
1747
1763
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
1748
1764
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
1749
1765
 
1750
- /* ---- pager: previous / next at the two edges of the bar ----
1766
+ /* ---- pager: previous / next as two round icon buttons at the two edges ----
1751
1767
  For a sequence you can walk BOTH ways — a three-card feature intro, a step
1752
1768
  flow. Use it instead of the default pack whenever going back is as ordinary
1753
1769
  as going on: a wide CTA beside a small round icon reads as one action plus
1754
1770
  an afterthought, which is wrong when the two are peers.
1755
1771
 
1756
- Both buttons are the same shape and the same height, so neither wins by
1757
- being physically bigger; the fill alone leans forward. The bar carries the
1758
- buttons only: where you are is said by the .pager__at dots UNDER the card
1772
+ The two are the same round shape and size (.icon-btn), one at each edge in
1773
+ thumb reach from either side; the fill (--invert) alone leans forward. They
1774
+ NEVER carry words (Lh 2026-09-16, for good): an arrow is the whole meaning,
1775
+ and a word that isn't a direction ("Get started", "Done") is not a pager
1776
+ button — that step COMMITS and goes back to the plain .action-bar with one
1777
+ wide button. Where you are is said by the .pager__at dots UNDER the card
1759
1778
  that changes (a .screen__centre group, card.css), never in the bar — the
1760
1779
  dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
1761
- "skip" link or nothing. Keep the last step of a flow on the plain
1762
- .action-bar: a step that COMMITS deserves one wide button, not a pair.
1780
+ "skip" link or nothing.
1763
1781
 
1764
1782
  <div class="action-bar action-bar--pager">
1765
- <button class="btn" disabled><svg class="icon icon--xs"><use href="icons.svg#arrow-left"/></svg>Back</button>
1766
- <button class="btn btn--primary">Next<svg class="icon icon--xs"><use href="icons.svg#arrow-right"/></svg></button>
1767
- </div> */
1783
+ <button class="icon-btn" aria-label="ย้อนกลับ" disabled>…</button>
1784
+ <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1785
+ </div> — or the same two in an .action-bar__tier--pager under a control */
1768
1786
  .action-bar--pager { align-items: center; }
1769
- /* equal share of the bar, and tighter than a lone CTA so two labels plus the
1770
- indicator still fit a 320px screen */
1771
- .action-bar--pager > .btn {
1772
- flex: 1 1 0;
1773
- min-width: 0;
1774
- padding-inline: var(--sp-3);
1775
- gap: var(--sp-1);
1776
- white-space: nowrap;
1777
- overflow: hidden;
1778
- }
1779
1787
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1780
1788
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1781
1789
  .pager__at > * {
@@ -1803,14 +1811,7 @@ button { font: inherit; cursor: pointer; }
1803
1811
  .action-bar__tier > .btn { flex: 1; }
1804
1812
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
1805
1813
 
1806
- /* ---- round pager: previous / next as two round buttons at the two edges ----
1807
- The pager with nothing to say between its buttons — no dots, no label — as
1808
- two equal round icon buttons, one at each edge, in thumb reach from either
1809
- side. Rule 3 still holds: the step that commits goes back to one wide button.
1810
- <div class="action-bar action-bar--pager">
1811
- <button class="icon-btn" aria-label="ย้อนกลับ">…</button>
1812
- <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1813
- </div> — or the same two in an .action-bar__tier--pager under a control */
1814
+ /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
1814
1815
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
1815
1816
 
1816
1817
  /* .screen's default bottom padding is a constant; derive the reserve from the
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/Section.js';
13
+ export * from './scaffold/BaseContent.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/Section.js';
16
+ export * from './scaffold/BaseContent.js';
17
17
  export * from './scaffold/EmptyState.js';
18
18
  export * from './scaffold/Centre.js';
19
19
  export * from './cards/Card.js';
@@ -0,0 +1,14 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
3
+ }
4
+ /**
5
+ * BaseContent — words straight on the base, no card around them: an overline, 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 buttons go to the ActionBar, not a CtaPack).
9
+ */
10
+ export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
11
+ export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
12
+ }
13
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
14
+ export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
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: an overline, 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 buttons go to the ActionBar, not a CtaPack).
8
+ */
9
+ export function BaseContent({ className, ...rest }) {
10
+ return _jsx("section", { className: cx('base-content', className), ...rest });
11
+ }
12
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
13
+ export function CtaPack({ className, ...rest }) {
14
+ return _jsx("div", { className: cx('cta-pack', className), ...rest });
15
+ }
@@ -9,6 +9,6 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<'div'>, '
9
9
  /** the heading's level: h2 on a surface (default) · h3 inside a card's section, under its h3 heading */
10
10
  level?: 'h2' | 'h3';
11
11
  }
12
- /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (in a `Centre`)
13
- * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in. */
12
+ /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (always in a `Centre`)
13
+ * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
14
14
  export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (in a `Centre`)
4
- * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in. */
3
+ /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (always in a `Centre`)
4
+ * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
5
5
  export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
6
6
  const H = level;
7
7
  return (_jsxs("div", { className: cx('empty-state', className), ...rest, children: [icon, title != null && _jsx(H, { className: "t-h2", children: title }), typeof children === 'string' ? _jsx("p", { className: "t-body", children: children }) : children, action != null && _jsx("div", { className: "empty-state__action", children: action })] }));
@@ -2,13 +2,9 @@ import type { ComponentPropsWithoutRef } from 'react';
2
2
  export interface StepProps extends ComponentPropsWithoutRef<'section'> {
3
3
  }
4
4
  /**
5
- * Step — one screen, one task. Children: a StepCard first (what the member READS — words on the base, not a card), an optional Sheet last
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
6
6
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
7
7
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
8
- * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
8
+ * Reads → BaseContent only. Acts → BaseContent + Sheet. Never hand-lay a step, never write a sheet height.
9
9
  */
10
10
  export declare function Step({ className, ...rest }: StepProps): import("react").JSX.Element;
11
- export interface StepCardProps extends ComponentPropsWithoutRef<'article'> {
12
- }
13
- /** StepCard — the reading block of a Step: the words on the base, no frame, no area colour (a CardHead · content, with a card's rhythm); it hugs its words and scrolls inside itself when the column runs out. */
14
- export declare function StepCard({ className, ...rest }: StepCardProps): import("react").JSX.Element;
@@ -1,15 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * Step — one screen, one task. Children: a StepCard first (what the member READS — words on the base, not a card), an optional Sheet last
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
5
5
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
6
6
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
7
- * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
7
+ * Reads → BaseContent only. Acts → BaseContent + 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 });
11
11
  }
12
- /** StepCard — the reading block of a Step: the words on the base, no frame, no area colour (a CardHead · content, with a card's rhythm); it hugs its words and scrolls inside itself when the column runs out. */
13
- export function StepCard({ className, ...rest }) {
14
- return _jsx("article", { className: cx('step__card', className), ...rest });
15
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.1.5",
3
+ "version": "0.1.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,12 +0,0 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface SectionProps extends ComponentPropsWithoutRef<'section'> {
3
- }
4
- /**
5
- * Section — frameless on-surface content + a CTA: an overline, a display heading, a paragraph, a CtaPack.
6
- * NEVER a card: sections are what the base says; cards are content the system offers.
7
- */
8
- export declare function Section({ className, ...rest }: SectionProps): import("react").JSX.Element;
9
- export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
10
- }
11
- /** CtaPack — the buttons under a Section's words, wrapping on one line. */
12
- export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /**
4
- * Section — frameless on-surface content + a CTA: an overline, a display heading, a paragraph, a CtaPack.
5
- * NEVER a card: sections are what the base says; cards are content the system offers.
6
- */
7
- export function Section({ className, ...rest }) {
8
- return _jsx("section", { className: cx('section', className), ...rest });
9
- }
10
- /** CtaPack — the buttons under a Section's words, wrapping on one line. */
11
- export function CtaPack({ className, ...rest }) {
12
- return _jsx("div", { className: cx('cta-pack', className), ...rest });
13
- }