@usableapp/cardds 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
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.7.1 — 2026-09-24
6
+
7
+ ### The bar fixed from outside is gone
8
+ - Before: an `ActionBar` written after `</Screen>` (in `<body>` or an app shell) was fixed to the viewport, and the screen
9
+ / step kept `--bar-reserve` of room for it. After: `.action-bar` is in flow; a bar is always the Screen's `bottom` (its
10
+ last child) — a layer over the content's foot in a fill screen, a sticky last row on a scrolling one (0.7.0). A bar left
11
+ outside a Screen now sits wherever it was written. `--bar-reserve` is still published (the centred card's cap reads it);
12
+ nothing reserves room with it any more. `ActionBar inline` stays, for a bare bar in a preview.
13
+
14
+ ## 0.7.0 — 2026-09-24
15
+
16
+ Breaking — the clean-up promised since 0.6.5. Four renames/removals, one new way.
17
+
18
+ ### A bar on a scrolling screen goes INSIDE the Screen
19
+ - Before: on a screen that is not `fill` (it scrolls), the `ActionBar` was written after `</Screen>`, fixed to the
20
+ viewport, and the screen reserved `--bar-reserve` at its bottom. After: write it as `<Screen bottom={<ActionBar>…}>`
21
+ like on a fill screen. The screen becomes a column at least one phone tall; the bar is its last row, `position: sticky`
22
+ at the viewport's foot (above the keyboard) — in reach while the words scroll, under them when they end.
23
+ `validateScreen` already reports a bar after `</Screen>`. Story `ActionBar · OnAScrollingScreen`.
24
+
25
+ ### `BaseContent` removed
26
+ - Before: `BaseContent` (an alias that warned). After: `Page` — same props, same markup.
27
+
28
+ ### `--base-content-*` knobs renamed `--page-*` (no fallback)
29
+ - `--base-content-media-h` → `--page-media-h`, and likewise `-gap`, `-pad-y`, `-body-ink`. The old names do nothing now:
30
+ rename them in your theme.css.
31
+
32
+ ### `Bubble actions` removed
33
+ - Before: small buttons beside a chat message (`actions`, `.bubble__actions`, knob `--bubble-actions-gap`). After: gone —
34
+ a Bubble is the message and its `meta`. The bubble is one column now.
35
+
5
36
  ## 0.6.16 — 2026-09-24
6
37
 
7
38
  ### iOS 16.4 supported (was 18.3)
package/README.md CHANGED
@@ -246,9 +246,10 @@ whose markup has rules, because the sheet's box is the step itself:
246
246
  step that follows anything else keeps the screen's gap, and the sheet
247
247
  stops under that thing.
248
248
  - **The bar is after the step, inside the screen** (the screen's bottom — *Screen: top · content · bottom*). In a
249
- `.screen--fill` the step and the bar share one flex column and touch; the step takes the rest. (A bar outside a
250
- fill screen — an app shell — is still fixed to the viewport and the step reads `--bar-reserve`: the old way,
251
- kept until 0.7.0.)
249
+ `.screen--fill` the bar is a layer over the content's foot. On a SCROLLING screen (not fill, 0.7.0) the screen is a
250
+ column at least one phone tall and the bar its last row, `position: sticky` at the viewport's foot (above `--kb`):
251
+ it stays in reach while the words scroll and sits under them when they end. Either way the bar is written INSIDE
252
+ the Screen (`bottom`) — a bar after `</Screen>` is an error (`validateScreen`).
252
253
  - **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
253
254
  foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
254
255
  its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
@@ -409,17 +410,9 @@ Demo: the *skeleton* block at the top of `elements.html`.
409
410
  5. **`.screen` carries the horizontal inset and nothing vertical.** The inset
410
411
  is `--screen-pad`; cards and on-surface UI sit flush inside it, with no
411
412
  per-component side padding. Top and bottom are zero: vertical room only
412
- exists as a reserve for something fixed parked over the screen, so each bar
413
- adds its own — `.appbar` the top (`.screen:has(> .appbar)`), `.action-bar`
414
- and `.float-bar` the bottom, each from its own height token. Whatever
415
- parent hosts the bar counts (`:has(> .action-bar) .screen`): `<body>` or an
416
- app shell alike. A page with no bars needs no override, and `.screen--fill`
417
- is excluded from the bottom reserves — there a `.step` carries the bar's
418
- room itself, as a bottom margin, reading the same `--bar-reserve`. The
419
- reserves use the nominal control height, so a bar whose button wraps to two
420
- lines can still overlap the last line of content.
421
- **In a `.screen--fill` none of this applies to a bar in the screen** (2026-09-23): it is the column's last
422
- part and the browser measures it — the reserves are the old way for a bar outside a fill screen.
413
+ is taken by what is in the column. The bottom bar is always the screen's last child (0.7.1 — the bar fixed to
414
+ the viewport from outside and its reserves are gone): in a `.screen--fill` a layer over the content's foot, on a
415
+ scrolling screen a `sticky` last row. `--bar-reserve` stays published only for a `.card--centre`'s cap.
423
416
  **Padding is published, not copied.** `.screen` sets `--screen-pad` and
424
417
  `--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
425
418
  `--card-pad` and `--card-gap`; anything that has to cancel a container's
package/css/actions.css CHANGED
@@ -216,21 +216,15 @@
216
216
  opacity: 1;
217
217
  }
218
218
 
219
- /* ---- bottom CTA pack: fixed action zone under the content ----
220
- --bar-reserve is the room the bar needs — its own height plus a breathing
221
- gap above it — published here so anything else that must stop clear of it
222
- reads this instead of recomputing it. .screen's own reserve below reads
223
- it; so does .step (step.css), the box a sheet slides in — one number, so
224
- the two can't drift apart.
225
- --kb is the on-screen keyboard's height, published on :root by cardds.js
226
- from the visual viewport (0 without one, and on a platform that shrinks
227
- the layout viewport itself): the bar rides above the keyboard on iOS the
228
- way it already does on Android, and .step grows its room by the same. */
219
+ /* ---- bottom CTA pack: the screen's bottom (Screen `bottom`) ----
220
+ A bar is always INSIDE its .screen, its last child (0.7.1: the bar fixed to the viewport from outside, and the
221
+ reserves the screen and the step kept for it, are gone). In a .screen--fill it is a layer over the content's foot
222
+ (step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
223
+ height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
224
+ --kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
229
225
  :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
230
226
  .action-bar {
231
- position: fixed;
232
- inset-inline: 0;
233
- bottom: var(--kb, 0);
227
+ position: relative;
234
228
  z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
235
229
  max-width: var(--screen-w);
236
230
  margin-inline: auto;
@@ -238,6 +232,15 @@
238
232
  display: flex;
239
233
  gap: var(--sp-2);
240
234
  }
235
+ /* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
236
+ last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
237
+ it takes the room it needs in the column, so nothing is added to the content for it */
238
+ .screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
239
+ .screen:not(.screen--fill) > .action-bar:last-child {
240
+ position: sticky; bottom: var(--kb, 0rem);
241
+ max-width: none;
242
+ margin: auto calc(-1 * var(--screen-pad)) 0;
243
+ }
241
244
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
242
245
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
243
246
 
@@ -304,13 +307,6 @@
304
307
  :is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
305
308
  :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
306
309
 
307
- /* .screen's default bottom padding is a constant; derive the reserve from the
308
- bar instead, so a taller button (a longer label, a script with more leading)
309
- can never end up underneath it. Any parent that hosts the bar counts —
310
- <body>, or an app shell (#app > .screen + .action-bar) — since the bar is
311
- fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
312
- its .step carries the bar's room itself, step.css.) */
313
- :has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
314
310
 
315
311
  /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
316
312
  rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
package/css/base.css CHANGED
@@ -115,18 +115,18 @@ button { font: inherit; cursor: pointer; }
115
115
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
116
116
  .page {
117
117
  display: grid;
118
- gap: var(--base-content-gap, var(--sp-3));
118
+ gap: var(--page-gap, var(--sp-3));
119
119
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
120
- padding-block: var(--base-content-pad-y, var(--sp-4));
120
+ padding-block: var(--page-pad-y, var(--sp-4));
121
121
  }
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) */
122
+ .page > .t-body { color: var(--page-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
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); }
128
+ .page > :is(img, .ph) { max-height: var(--page-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(--page-media-h, 14rem); }
130
130
  .page .cta-pack {
131
131
  display: flex;
132
132
  align-items: start; /* buttons keep their own height, never the row's */
package/css/lists.css CHANGED
@@ -98,29 +98,23 @@
98
98
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
99
99
  No tail: a rounded block whose corner nearest the sender is square-ish
100
100
  (bottom-left for theirs, bottom-right for mine), the way messaging apps
101
- draw it. Under it a caption line (time · a tick), beside it small round
102
- actions (a heart, more). A .thread stacks them.
101
+ draw it. Under it a caption line (time · a tick). A .thread stacks them.
103
102
  <div class="thread">
104
- <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
103
+ <div class="bubble"><p class="bubble__body">…</p><span class="bubble__meta">Dec 4 · 8:15</span></div>
105
104
  <div class="bubble bubble--right">…</div>
106
105
  </div> */
107
106
  .thread { display: grid; gap: var(--thread-gap, var(--sp-4)); }
108
- /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
109
- and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
107
+ /* one column: the message (max-content, but it gives way — minmax(0) — so a long one shrinks and wraps); the meta under it */
110
108
  .bubble {
111
109
  display: grid;
112
- grid-template-columns: minmax(0, max-content) auto;
110
+ grid-template-columns: minmax(0, max-content);
113
111
  justify-content: start;
114
112
  align-items: center;
115
113
  gap: var(--sp-1) var(--sp-3);
116
114
  max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
117
115
  margin-right: auto; /* theirs: hugs the left */
118
116
  }
119
- .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
120
- .bubble__body, .bubble__meta { grid-column: 1; }
121
- .bubble__actions { grid-column: 2; grid-row: 1; }
122
- .bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
123
- .bubble--right > .bubble__actions { grid-column: 1; }
117
+ .bubble--right { justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right */
124
118
  .bubble__body {
125
119
  margin: 0;
126
120
  min-width: 0;
@@ -139,7 +133,6 @@
139
133
  color: var(--on-accent);
140
134
  text-align: right;
141
135
  }
142
- .bubble__actions { display: flex; gap: var(--bubble-actions-gap, var(--sp-1)); flex: none; opacity: 0.7; }
143
136
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
144
137
  .bubble--right > .bubble__meta { justify-self: end; }
145
138
  .bubble__meta > .icon { color: var(--accent); }
package/css/stack.css CHANGED
@@ -230,7 +230,7 @@
230
230
  gap: var(--card-gap, var(--sp-5));
231
231
  overflow: clip;
232
232
  }
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 —
233
+ /* a base that is ONE Page 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
235
  .sheet-stack__base:has(> .page:only-child) { align-content: start; }
236
236
 
package/css/step.css CHANGED
@@ -10,7 +10,7 @@
10
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
- <div class="action-bar">…</div> ← only when there is no sheet
13
+ <div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
14
14
 
15
15
  The step is the box the sheet slides in. It fills the screen
16
16
  column between the top bar's reserve (.screen already pads for
@@ -59,11 +59,6 @@
59
59
  more air and a short one less; a screen with a bar keeps the bar's own room */
60
60
  .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
61
61
 
62
- /* the bar's room — read from actions.css, never recomputed. Margin, not
63
- padding, so the clip cuts at the box edge and nothing shows behind the
64
- bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
65
- frame — the bar is fixed to the viewport wherever it sits. */
66
- :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
67
62
  /* 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
63
  is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
69
64
 
@@ -119,7 +114,7 @@
119
114
  TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
120
115
  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
121
116
  under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
122
- (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
117
+ (On a scrolling screen the bar is a sticky last row instead, actions.css.)
123
118
  ============================================================ */
124
119
  .screen--fill > .action-bar {
125
120
  position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
@@ -127,7 +122,6 @@
127
122
  margin: 0;
128
123
  padding-top: 0; /* no ground of its own, never a solid strip */
129
124
  }
130
- .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
131
125
  /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
132
126
  .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
133
127
  /* 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
@@ -145,13 +139,13 @@
145
139
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
146
140
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
147
141
  /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
148
- the lead + the picture's own height (--base-content-media-h), square — so the words start where they always did */
142
+ the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
149
143
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
150
144
  .step > .pages > .page > :is(img, .ph):first-child {
151
145
  width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
152
146
  max-width: none;
153
147
  margin-inline: calc(-1 * var(--screen-pad));
154
- height: calc(var(--_lead, 0rem) + var(--base-content-media-h, 14rem));
148
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
155
149
  max-height: none;
156
150
  border-radius: 0;
157
151
  border-inline: 0;
package/dist/cardds.css CHANGED
@@ -495,18 +495,18 @@ button { font: inherit; cursor: pointer; }
495
495
  In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
496
496
  .page {
497
497
  display: grid;
498
- gap: var(--base-content-gap, var(--sp-3));
498
+ gap: var(--page-gap, var(--sp-3));
499
499
  align-content: start; /* a tall column never stretches the rows — the words hug the top */
500
- padding-block: var(--base-content-pad-y, var(--sp-4));
500
+ padding-block: var(--page-pad-y, var(--sp-4));
501
501
  }
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
+ .page > .t-body { color: var(--page-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) */
503
503
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
504
504
  (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
505
505
  /* the page's ONE heading on the base (a screen with no TopBar: CardHead level="h1") reads at the card heading's size —
506
506
  h1 is the semantic level, not a bigger face; --fs-h1 is the bar title's size (Lh 2026-09-22) */
507
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); }
508
+ .page > :is(img, .ph) { max-height: var(--page-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(--page-media-h, 14rem); }
510
510
  .page .cta-pack {
511
511
  display: flex;
512
512
  align-items: start; /* buttons keep their own height, never the row's */
@@ -1073,7 +1073,7 @@ button { font: inherit; cursor: pointer; }
1073
1073
  gap: var(--card-gap, var(--sp-5));
1074
1074
  overflow: clip;
1075
1075
  }
1076
- /* 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 —
1076
+ /* a base that is ONE Page is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
1077
1077
  loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
1078
1078
  .sheet-stack__base:has(> .page:only-child) { align-content: start; }
1079
1079
 
@@ -1543,7 +1543,7 @@ button { font: inherit; cursor: pointer; }
1543
1543
  <section class="page">…the words…</section> ← first
1544
1544
  <article class="card sheet sheet--half">…the controls… <div class="card__foot">…</div></article> ← last, optional
1545
1545
  </section>
1546
- <div class="action-bar">…</div> ← only when there is no sheet
1546
+ <div class="action-bar">…</div> ← the screen's last child, only when there is no sheet
1547
1547
 
1548
1548
  The step is the box the sheet slides in. It fills the screen
1549
1549
  column between the top bar's reserve (.screen already pads for
@@ -1592,11 +1592,6 @@ button { font: inherit; cursor: pointer; }
1592
1592
  more air and a short one less; a screen with a bar keeps the bar's own room */
1593
1593
  .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
1594
1594
 
1595
- /* the bar's room — read from actions.css, never recomputed. Margin, not
1596
- padding, so the clip cuts at the box edge and nothing shows behind the
1597
- bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
1598
- frame — the bar is fixed to the viewport wherever it sits. */
1599
- :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1600
1595
  /* 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)
1601
1596
  is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
1602
1597
 
@@ -1652,7 +1647,7 @@ button { font: inherit; cursor: pointer; }
1652
1647
  TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
1653
1648
  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
1654
1649
  under it, on purpose; nothing is added to the content to make room. The keyboard lifts the layer (--kb).
1655
- (A bar OUTSIDE a fill screen — an app shell, a scrolling screen — is still fixed to the viewport, actions.css.)
1650
+ (On a scrolling screen the bar is a sticky last row instead, actions.css.)
1656
1651
  ============================================================ */
1657
1652
  .screen--fill > .action-bar {
1658
1653
  position: absolute; inset-inline: 0; top: auto; bottom: var(--kb, 0rem);
@@ -1660,7 +1655,6 @@ button { font: inherit; cursor: pointer; }
1660
1655
  margin: 0;
1661
1656
  padding-top: 0; /* no ground of its own, never a solid strip */
1662
1657
  }
1663
- .screen--fill:has(> .action-bar) > .step { margin-bottom: 0; }
1664
1658
  /* the walk's dots are the BOTTOM layer's too (Lh 2026-09-23): a row of the bar, over its buttons, centred */
1665
1659
  .action-bar > .pager__at, .action-bar__tier > .pager__at { align-self: center; }
1666
1660
  /* 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
@@ -1678,13 +1672,13 @@ button { font: inherit; cursor: pointer; }
1678
1672
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
1679
1673
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
1680
1674
  /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
1681
- the lead + the picture's own height (--base-content-media-h), square — so the words start where they always did */
1675
+ the lead + the picture's own height (--page-media-h), square — so the words start where they always did */
1682
1676
  .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
1683
1677
  .step > .pages > .page > :is(img, .ph):first-child {
1684
1678
  width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
1685
1679
  max-width: none;
1686
1680
  margin-inline: calc(-1 * var(--screen-pad));
1687
- height: calc(var(--_lead, 0rem) + var(--base-content-media-h, 14rem));
1681
+ height: calc(var(--_lead, 0rem) + var(--page-media-h, 14rem));
1688
1682
  max-height: none;
1689
1683
  border-radius: 0;
1690
1684
  border-inline: 0;
@@ -2048,21 +2042,15 @@ button { font: inherit; cursor: pointer; }
2048
2042
  opacity: 1;
2049
2043
  }
2050
2044
 
2051
- /* ---- bottom CTA pack: fixed action zone under the content ----
2052
- --bar-reserve is the room the bar needs — its own height plus a breathing
2053
- gap above it — published here so anything else that must stop clear of it
2054
- reads this instead of recomputing it. .screen's own reserve below reads
2055
- it; so does .step (step.css), the box a sheet slides in — one number, so
2056
- the two can't drift apart.
2057
- --kb is the on-screen keyboard's height, published on :root by cardds.js
2058
- from the visual viewport (0 without one, and on a platform that shrinks
2059
- the layout viewport itself): the bar rides above the keyboard on iOS the
2060
- way it already does on Android, and .step grows its room by the same. */
2045
+ /* ---- bottom CTA pack: the screen's bottom (Screen `bottom`) ----
2046
+ A bar is always INSIDE its .screen, its last child (0.7.1: the bar fixed to the viewport from outside, and the
2047
+ reserves the screen and the step kept for it, are gone). In a .screen--fill it is a layer over the content's foot
2048
+ (step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
2049
+ height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
2050
+ --kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
2061
2051
  :root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
2062
2052
  .action-bar {
2063
- position: fixed;
2064
- inset-inline: 0;
2065
- bottom: var(--kb, 0);
2053
+ position: relative;
2066
2054
  z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
2067
2055
  max-width: var(--screen-w);
2068
2056
  margin-inline: auto;
@@ -2070,6 +2058,15 @@ button { font: inherit; cursor: pointer; }
2070
2058
  display: flex;
2071
2059
  gap: var(--sp-2);
2072
2060
  }
2061
+ /* a bar INSIDE a scrolling (not fill) screen (Lh 2026-09-24, 0.7.0): the screen is a column at least one phone tall, the bar its
2062
+ last row — at the foot when the words are short, and sticky to the viewport's foot (above the keyboard) while they scroll;
2063
+ it takes the room it needs in the column, so nothing is added to the content for it */
2064
+ .screen:not(.screen--fill):has(> .action-bar:last-child) { display: flex; flex-direction: column; min-height: 100dvh; }
2065
+ .screen:not(.screen--fill) > .action-bar:last-child {
2066
+ position: sticky; bottom: var(--kb, 0rem);
2067
+ max-width: none;
2068
+ margin: auto calc(-1 * var(--screen-pad)) 0;
2069
+ }
2073
2070
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
2074
2071
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
2075
2072
 
@@ -2136,13 +2133,6 @@ button { font: inherit; cursor: pointer; }
2136
2133
  :is(.action-bar--pager, .action-bar__tier--pager) > :not(.icon-btn):not(:first-child):not(:last-child) { margin-inline: auto; }
2137
2134
  :is(.action-bar--pager, .action-bar__tier--pager):has(> :nth-child(3)) > .icon-btn:first-child { margin-right: 0; }
2138
2135
 
2139
- /* .screen's default bottom padding is a constant; derive the reserve from the
2140
- bar instead, so a taller button (a longer label, a script with more leading)
2141
- can never end up underneath it. Any parent that hosts the bar counts —
2142
- <body>, or an app shell (#app > .screen + .action-bar) — since the bar is
2143
- fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
2144
- its .step carries the bar's room itself, step.css.) */
2145
- :has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
2146
2136
 
2147
2137
  /* ---- action note (Lh 2026-09-24): the main button's place, the main button's size, but ON the ground — no fill, no border, no
2148
2138
  rounding — one line of muted words, an inline icon allowed; nothing to press. In a CardFoot or an ActionBar row it takes the
@@ -2941,29 +2931,23 @@ button { font: inherit; cursor: pointer; }
2941
2931
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
2942
2932
  No tail: a rounded block whose corner nearest the sender is square-ish
2943
2933
  (bottom-left for theirs, bottom-right for mine), the way messaging apps
2944
- draw it. Under it a caption line (time · a tick), beside it small round
2945
- actions (a heart, more). A .thread stacks them.
2934
+ draw it. Under it a caption line (time · a tick). A .thread stacks them.
2946
2935
  <div class="thread">
2947
- <div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
2936
+ <div class="bubble"><p class="bubble__body">…</p><span class="bubble__meta">Dec 4 · 8:15</span></div>
2948
2937
  <div class="bubble bubble--right">…</div>
2949
2938
  </div> */
2950
2939
  .thread { display: grid; gap: var(--thread-gap, var(--sp-4)); }
2951
- /* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
2952
- and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
2940
+ /* one column: the message (max-content, but it gives way — minmax(0) — so a long one shrinks and wraps); the meta under it */
2953
2941
  .bubble {
2954
2942
  display: grid;
2955
- grid-template-columns: minmax(0, max-content) auto;
2943
+ grid-template-columns: minmax(0, max-content);
2956
2944
  justify-content: start;
2957
2945
  align-items: center;
2958
2946
  gap: var(--sp-1) var(--sp-3);
2959
2947
  max-width: var(--bubble-max-w, 75%); /* a message may run to three quarters of the thread (Lh 2026-09-23) */
2960
2948
  margin-right: auto; /* theirs: hugs the left */
2961
2949
  }
2962
- .bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
2963
- .bubble__body, .bubble__meta { grid-column: 1; }
2964
- .bubble__actions { grid-column: 2; grid-row: 1; }
2965
- .bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
2966
- .bubble--right > .bubble__actions { grid-column: 1; }
2950
+ .bubble--right { justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right */
2967
2951
  .bubble__body {
2968
2952
  margin: 0;
2969
2953
  min-width: 0;
@@ -2982,7 +2966,6 @@ button { font: inherit; cursor: pointer; }
2982
2966
  color: var(--on-accent);
2983
2967
  text-align: right;
2984
2968
  }
2985
- .bubble__actions { display: flex; gap: var(--bubble-actions-gap, var(--sp-1)); flex: none; opacity: 0.7; }
2986
2969
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
2987
2970
  .bubble--right > .bubble__meta { justify-self: end; }
2988
2971
  .bubble__meta > .icon { color: var(--accent); }
@@ -8,11 +8,9 @@ export interface BubbleProps extends ComponentProps<'div'> {
8
8
  side?: 'left' | 'right';
9
9
  /** the caption under the message — a time, a tick (an Icon) */
10
10
  meta?: ReactNode;
11
- /** small round buttons beside the message (a heart, more) */
12
- actions?: ReactNode;
13
11
  }
14
12
  /**
15
13
  * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
16
- * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
14
+ * for mine), no tail. `meta` sits under it on the sender's side. Stack them in a Thread.
17
15
  */
18
- export declare function Bubble({ side, meta, actions, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
16
+ export declare function Bubble({ side, meta, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
@@ -6,8 +6,8 @@ export function Thread({ className, ...rest }) {
6
6
  }
7
7
  /**
8
8
  * Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
9
- * for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
9
+ * for mine), no tail. `meta` sits under it on the sender's side. Stack them in a Thread.
10
10
  */
11
- export function Bubble({ side = 'left', meta, actions, className, children, ...rest }) {
12
- return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), actions != null && _jsx("span", { className: "bubble__actions", children: actions }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
11
+ export function Bubble({ side = 'left', meta, className, children, ...rest }) {
12
+ return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
13
13
  }
@@ -2,7 +2,7 @@ import type { ComponentProps } from 'react';
2
2
  export interface ActionBarProps extends ComponentProps<'div'> {
3
3
  /** `pager`: previous / next as two equal buttons at the edges (a Link may sit between them; the PagerAt dots never do — they go under the card) · `tiers`: rows stacked — a control tier above the buttons */
4
4
  variant?: 'pager' | 'tiers';
5
- /** render in flow instead of fixed to the viewport bottom (for a preview or a mock frame); the real bar is fixed */
5
+ /** a bare bar for a preview: no position, full width, no side padding (a real bar is a Screen's `bottom`) */
6
6
  inline?: boolean;
7
7
  }
8
8
  /**
@@ -11,7 +11,7 @@ export interface ActionBarProps extends ComponentProps<'div'> {
11
11
  * Plain: one wide primary Btn (the step that commits). `pager`: Back + Next as peers — buttons only; where you are is the
12
12
  * PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-12).
13
13
  * `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
14
- * In a `Screen fill` the content takes the rest; outside one (an app shell) the bar is still fixed and `--bar-reserve` keeps its room.
14
+ * In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
15
15
  */
16
16
  export declare function ActionBar({ variant, inline, className, style, ...rest }: ActionBarProps): import("react").JSX.Element;
17
17
  export interface ActionBarTierProps extends ComponentProps<'div'> {
@@ -6,7 +6,7 @@ import { cx } from '../cx.js';
6
6
  * Plain: one wide primary Btn (the step that commits). `pager`: Back + Next as peers — buttons only; where you are is the
7
7
  * PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-12).
8
8
  * `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
9
- * In a `Screen fill` the content takes the rest; outside one (an app shell) the bar is still fixed and `--bar-reserve` keeps its room.
9
+ * In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
10
10
  */
11
11
  export function ActionBar({ variant, inline, className, style, ...rest }) {
12
12
  return (_jsx("div", { className: cx('action-bar', variant && `action-bar--${variant}`, className), style: inline ? { position: 'static', width: '100%', maxWidth: 'none', paddingInline: 0, ...style } : style, ...rest }));
@@ -8,9 +8,6 @@ export interface PageProps extends ComponentProps<'section'> {
8
8
  * a still, one-scaffold screen, which is gone.
9
9
  */
10
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
11
  export interface CtaPackProps extends ComponentProps<'div'> {
15
12
  }
16
13
  /** 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. */
@@ -9,15 +9,6 @@ import { cx } from '../cx.js';
9
9
  export function Page({ className, ...rest }) {
10
10
  return _jsx("section", { className: cx('page', className), ...rest });
11
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
12
  /** 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
13
  export function CtaPack({ className, ...rest }) {
23
14
  return _jsx("div", { className: cx('cta-pack', className), ...rest });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.6.16",
3
+ "version": "0.7.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",