@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 +31 -0
- package/README.md +7 -14
- package/css/actions.css +16 -20
- package/css/base.css +5 -5
- package/css/lists.css +5 -12
- package/css/stack.css +1 -1
- package/css/step.css +4 -10
- package/dist/cardds.css +31 -48
- package/dist/lists/Bubble.d.ts +2 -4
- package/dist/lists/Bubble.js +3 -3
- package/dist/scaffold/ActionBar.d.ts +2 -2
- package/dist/scaffold/ActionBar.js +1 -1
- package/dist/scaffold/Page.d.ts +0 -3
- package/dist/scaffold/Page.js +0 -9
- package/package.json +1 -1
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
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
the
|
|
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:
|
|
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(--
|
|
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(--
|
|
120
|
+
padding-block: var(--page-pad-y, var(--sp-4));
|
|
121
121
|
}
|
|
122
|
-
.page > .t-body { color: var(--
|
|
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(--
|
|
129
|
-
.page > .ph { width: 100%; height: var(--
|
|
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)
|
|
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="
|
|
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
|
-
/*
|
|
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)
|
|
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 {
|
|
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
|
|
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
|
-
(
|
|
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 (--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
500
|
+
padding-block: var(--page-pad-y, var(--sp-4));
|
|
501
501
|
}
|
|
502
|
-
.page > .t-body { color: var(--
|
|
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(--
|
|
509
|
-
.page > .ph { width: 100%; height: var(--
|
|
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
|
|
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
|
-
(
|
|
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 (--
|
|
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(--
|
|
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:
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
the
|
|
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:
|
|
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)
|
|
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="
|
|
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
|
-
/*
|
|
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)
|
|
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 {
|
|
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); }
|
package/dist/lists/Bubble.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
16
|
+
export declare function Bubble({ side, meta, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
|
package/dist/lists/Bubble.js
CHANGED
|
@@ -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
|
|
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,
|
|
12
|
-
return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }),
|
|
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
|
-
/**
|
|
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`
|
|
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`
|
|
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 }));
|
package/dist/scaffold/Page.d.ts
CHANGED
|
@@ -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. */
|
package/dist/scaffold/Page.js
CHANGED
|
@@ -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.
|
|
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.",
|