@usableapp/cardds 0.7.18 → 0.7.20
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 +22 -0
- package/README.md +13 -0
- package/css/actions.css +7 -7
- package/css/base.css +1 -0
- package/css/layover.css +1 -1
- package/css/lists.css +10 -0
- package/css/sheet.css +16 -10
- package/css/step.css +1 -1
- package/css/tokens.css +7 -0
- package/dist/cardds.css +43 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Bullets.d.ts +16 -0
- package/dist/lists/Bullets.js +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.20 — 2026-09-28
|
|
6
|
+
|
|
7
|
+
Lh, from timebank on a real Android phone (card #153). Nothing to change in a screen.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- A `Sheet`'s title is ONE line, cut with … when too long (was: wrapped to two lines). The thing at the head's end keeps its place; a caption still takes its own row. A wrapped title no longer makes a taller peek strip — there is none.
|
|
11
|
+
- `SheetSteps`: the open step's content (words, `Field`, `Pin` cells…) sits inset to the step title's words — the bar's own inset (`--sheet-step-bar-pad-x`), or its own knob `--sheet-step-inner-pad-x`.
|
|
12
|
+
- `SheetSteps`: room under the LAST step (`--sheet-steps-end`, default sp-6) — scrolled to the end it no longer touches the sheet's foot button.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- A folded `SheetStep`'s long `summary` ran past the sheet's right edge; it is cut with … inside its row now.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `Bullets` + `Bullet` (lists): each item led by a small square picture one line tall (`1lh`), the words wrap beside it. `pic` = an image URL or any node, on the list (every item) or on an item (its own); none = the placeholder square. Knobs `--bullets-pic-size` · `--bullets-pic-gap` · `--bullets-pic-radius` · `--bullets-gap` · `--bullets-font`. Gallery: `/Bullets`.
|
|
19
|
+
|
|
20
|
+
## 0.7.19 — 2026-09-28
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- SAFE AREA (Lh, timebank on Android Chrome: the bar's ← → buttons lay half under the gesture bar). With `viewport-fit=cover` the phone draws the page under its own bars; cardds kept nothing clear of them. Now it publishes `--safe-top` / `--safe-bottom` (`env(safe-area-inset-*)`, 0 without cover) and keeps clear of them: the `ActionBar`'s buttons (its ground still runs to the edge), a peeked `Sheet`'s head, a sheet's foot button and the end of its body, `FloatBar`, `Fab`, a stage's foot; a fill `Screen` starts under the status bar. A keyboard up covers the gesture bar, so the bottom room then goes to the keyboard alone. `--bar-reserve` includes it.
|
|
24
|
+
- README says the ONE viewport meta cardds expects: `width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content`.
|
|
25
|
+
- Nothing to change in a screen. A component of the app's own pinned to an edge reads `--safe-top` / `--safe-bottom`.
|
|
26
|
+
|
|
5
27
|
## 0.7.18 — 2026-09-27
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -42,6 +42,19 @@ import '@usableapp/cardds/dist/cardds.css';
|
|
|
42
42
|
import './theme.css'; // the project's own look, after the system's — see "A project's theme"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
**The viewport meta cardds expects** (one way for every app):
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`viewport-fit=cover` = the app runs edge to edge under the phone's own bars; cardds publishes their room as
|
|
52
|
+
`--safe-top` / `--safe-bottom` (from `env(safe-area-inset-*)`, 0 without cover) and keeps what the member reads or taps
|
|
53
|
+
clear of them — the bottom bar's buttons, a sheet's peeked head, its foot and the end of its body, the float bar, the FAB,
|
|
54
|
+
a stage's foot; a fill `Screen` starts under the status bar — while every ground still runs to the edge. A
|
|
55
|
+
component of the app's own pinned to an edge reads the same two tokens. `interactive-widget=resizes-content` =
|
|
56
|
+
Android shrinks the page for the keyboard (`--kb` stays 0 there; iOS publishes it).
|
|
57
|
+
|
|
45
58
|
Every behaviour is the component's own React code — the `Sheet`'s handle (tap, drag,
|
|
46
59
|
keyboard) with the focus and on-screen-keyboard raises, the `SheetStack` opening and
|
|
47
60
|
closing, the `Pin` auto-advance, the `Modal`'s leaving — each controlled like an input
|
package/css/actions.css
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
.icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
|
|
95
95
|
.icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
|
|
96
96
|
/* fab: the square floating action, bottom-right of a stage or screen */
|
|
97
|
-
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
|
|
97
|
+
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
|
|
98
98
|
|
|
99
99
|
/* ---- buttons ---- */
|
|
100
100
|
.btn {
|
|
@@ -220,13 +220,13 @@
|
|
|
220
220
|
(step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
|
|
221
221
|
height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
|
|
222
222
|
--kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
|
|
223
|
-
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
223
|
+
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
224
224
|
.action-bar {
|
|
225
225
|
position: relative;
|
|
226
226
|
z-index: var(--z-bar); /* above .sheet-stack sheets, which paint at --level 1-5 */
|
|
227
227
|
max-width: var(--screen-w);
|
|
228
228
|
margin-inline: auto;
|
|
229
|
-
padding: var(--sp-3) var(--sp-4) var(--sp-5);
|
|
229
|
+
padding: var(--sp-3) var(--sp-4) calc(var(--sp-5) + var(--_safe-b-open)); /* its ground runs under the phone's gesture bar, its buttons stand above it */
|
|
230
230
|
display: flex;
|
|
231
231
|
gap: var(--sp-2);
|
|
232
232
|
}
|
|
@@ -293,12 +293,12 @@
|
|
|
293
293
|
.action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
|
|
294
294
|
.action-bar__tier > .btn { flex: 1; }
|
|
295
295
|
.action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
|
|
296
|
-
:has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
296
|
+
:has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
297
297
|
/* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
|
|
298
298
|
adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
|
|
299
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
300
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
301
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
299
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
300
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
301
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
302
302
|
|
|
303
303
|
/* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
|
|
304
304
|
:is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
|
package/css/base.css
CHANGED
|
@@ -105,6 +105,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
105
105
|
display: flex;
|
|
106
106
|
flex-direction: column;
|
|
107
107
|
height: 100dvh;
|
|
108
|
+
padding-top: var(--safe-top); /* under the phone's status bar when the app runs edge to edge (Lh 2026-09-28) */
|
|
108
109
|
overflow: clip;
|
|
109
110
|
position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
|
|
110
111
|
container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
|
package/css/layover.css
CHANGED
package/css/lists.css
CHANGED
|
@@ -204,3 +204,13 @@
|
|
|
204
204
|
.link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a WaitLink counting down) */
|
|
205
205
|
.link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
|
|
206
206
|
.card--3 .link--muted { color: var(--card-3-muted); }
|
|
207
|
+
|
|
208
|
+
/* ---- bullets: a list, each item led by a small square picture as tall as a line of its words (Lh 2026-09-28, timebank #153) ----
|
|
209
|
+
<ul class="bullets"><li class="bullets__item"><img class="bullets__pic" alt=""><span class="bullets__text">…</span></li></ul>
|
|
210
|
+
The picture is the app's (an image or any node); none = the wireframe's crossed square. It sits on the item's FIRST line
|
|
211
|
+
(1lh — the line's own height, so it follows the text size), the words wrap beside it, never under it. */
|
|
212
|
+
.bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--bullets-gap, var(--sp-3)); font: var(--bullets-font, var(--type-body)); }
|
|
213
|
+
.bullets__item { display: flex; align-items: start; gap: var(--bullets-pic-gap, var(--sp-3)); min-width: 0; }
|
|
214
|
+
.bullets__pic { flex: none; inline-size: var(--bullets-pic-size, 1lh); block-size: var(--bullets-pic-size, 1lh); min-height: 0; object-fit: contain; border-radius: var(--bullets-pic-radius, 0rem); display: grid; place-items: center; }
|
|
215
|
+
.bullets__pic.ph { border-radius: var(--bullets-pic-radius, 0rem); }
|
|
216
|
+
.bullets__text { flex: 1; min-width: 0; }
|
package/css/sheet.css
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
border-radius: var(--r-card) var(--r-card) 0 0;
|
|
82
82
|
border-bottom: 0;
|
|
83
83
|
box-shadow: var(--sheet-shadow, var(--shadow-sheet));
|
|
84
|
-
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
84
|
+
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
85
85
|
}
|
|
86
86
|
.sheet--full { --_y: 0%; --_cover: 0rem; }
|
|
87
87
|
.sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
/* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
|
|
91
91
|
have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
|
|
92
92
|
inherits it. A sheet whose head is taller retunes it on the box. */
|
|
93
|
-
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
|
|
93
|
+
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
|
|
94
94
|
/* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
|
|
95
95
|
setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
|
|
96
96
|
:root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
|
|
@@ -148,12 +148,12 @@
|
|
|
148
148
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
149
149
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
150
150
|
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
151
|
-
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
|
|
151
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
|
|
152
152
|
.sheet > .card__foot {
|
|
153
153
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
154
154
|
align-items: end;
|
|
155
155
|
align-content: end;
|
|
156
|
-
padding: 0 var(--card-pad) var(--card-pad);
|
|
156
|
+
padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
|
|
157
157
|
background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
|
|
158
158
|
pointer-events: none;
|
|
159
159
|
translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
.sheet > :not(.sheet__body) { flex-shrink: 0; }
|
|
170
170
|
:is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
|
|
171
171
|
.sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
|
|
172
|
-
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
|
|
172
|
+
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
|
|
173
173
|
|
|
174
174
|
/* grab handle — first child of the sheet. Drawn as a small bar, but it is
|
|
175
175
|
the thumb's grab area for the whole strip above the title: a pseudo widens
|
|
@@ -208,6 +208,10 @@
|
|
|
208
208
|
whole ask is that switch: peek shows it (a row is --tap tall plus its own
|
|
209
209
|
padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
|
|
210
210
|
.sheet > .card__head { min-height: var(--sheet-head-h); }
|
|
211
|
+
/* a sheet's title is ONE line, cut with … when the phone is too narrow (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน"
|
|
212
|
+
wrapped to two lines at 390px) — the thing at its end keeps its place beside it; a caption still takes its own row */
|
|
213
|
+
.sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { flex-wrap: nowrap; }
|
|
214
|
+
.sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
211
215
|
.sheet__head {
|
|
212
216
|
display: grid;
|
|
213
217
|
grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
|
|
@@ -349,7 +353,9 @@
|
|
|
349
353
|
position: absolute;
|
|
350
354
|
inset-inline: 0;
|
|
351
355
|
bottom: 0;
|
|
352
|
-
height: var(--tap-lg);
|
|
356
|
+
height: calc(var(--tap-lg) + var(--_safe-b-open));
|
|
357
|
+
padding-bottom: var(--_safe-b-open);
|
|
358
|
+
box-sizing: border-box;
|
|
353
359
|
margin: 0;
|
|
354
360
|
display: grid;
|
|
355
361
|
place-items: center;
|
|
@@ -379,11 +385,11 @@
|
|
|
379
385
|
.sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
|
|
380
386
|
.sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; position: relative; }
|
|
381
387
|
.sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1rem; height: 1rem; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
|
|
382
|
-
.sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
|
|
388
|
+
.sheet-steps__track { list-style: none; margin: 0; padding: 0 0 var(--sheet-steps-end, var(--sp-6)); flex: 1; /* room under the LAST step: scrolled to the end it never touches the foot's button (Lh 2026-09-28, #153) */ display: flex; flex-direction: column; }
|
|
383
389
|
|
|
384
390
|
.sheet-step {
|
|
385
391
|
position: relative; min-width: 0;
|
|
386
|
-
display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
|
|
392
|
+
display: grid; grid-template-columns: minmax(0, 1fr); /* the column is the step's width, never its words' — a long summary is cut with …, it never pushes the row past the sheet's edge (Lh 2026-09-28, #153) */ grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
|
|
387
393
|
flex: 0 0 auto;
|
|
388
394
|
transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
|
|
389
395
|
--_row-h: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* the bar's own floor, read once (__head and __go below share it) */
|
|
@@ -394,7 +400,7 @@
|
|
|
394
400
|
.sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
|
|
395
401
|
.sheet-step + .sheet-step { margin-top: var(--sheet-steps-gap, var(--sp-1)); } /* the folded bars read as a pile, a little ground between them (Lh 2026-09-25) */
|
|
396
402
|
.sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the white room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
|
|
397
|
-
display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
|
|
403
|
+
display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); min-width: 0; /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
|
|
398
404
|
padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
|
|
399
405
|
background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
|
|
400
406
|
border-top: var(--sheet-step-border-w, 0rem) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
|
|
@@ -408,7 +414,7 @@
|
|
|
408
414
|
.sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
409
415
|
.sheet-step__check { margin-inline-start: auto; }
|
|
410
416
|
.sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
|
|
411
|
-
.sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); }
|
|
417
|
+
.sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
|
|
412
418
|
.sheet-step__inner > * { min-width: 0; }
|
|
413
419
|
.sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
|
|
414
420
|
.sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
|
package/css/step.css
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
|
|
137
137
|
margin-bottom: calc(var(--sp-2) - var(--screen-gap));
|
|
138
138
|
}
|
|
139
|
-
.screen.screen--fill:has(> .appbar) { padding-top:
|
|
139
|
+
.screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
|
|
140
140
|
/* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
|
|
141
141
|
them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
|
|
142
142
|
.screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
|
package/css/tokens.css
CHANGED
|
@@ -238,6 +238,13 @@
|
|
|
238
238
|
sheet.css sets the values; these initial values are what a sheet
|
|
239
239
|
falls back to when its own declaration cannot resolve.
|
|
240
240
|
============================================================ */
|
|
241
|
+
/* SAFE AREA (Lh 2026-09-28, timebank on Android: the bar's buttons lay under the gesture bar) — the room the phone's own
|
|
242
|
+
bars take over the page when the app runs edge to edge (viewport-fit=cover: Android Chrome, an iPhone home-screen app).
|
|
243
|
+
PUBLISHED: backgrounds still run to the edge; what the member reads or taps stands clear of them. 0 without cover.
|
|
244
|
+
--_safe-b-open = the bottom inset the keyboard does NOT already cover (a keyboard up hides the gesture bar). */
|
|
245
|
+
:root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
246
|
+
* { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
|
|
247
|
+
|
|
241
248
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
242
249
|
@property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
|
|
243
250
|
@property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
|
package/dist/cardds.css
CHANGED
|
@@ -279,6 +279,13 @@
|
|
|
279
279
|
sheet.css sets the values; these initial values are what a sheet
|
|
280
280
|
falls back to when its own declaration cannot resolve.
|
|
281
281
|
============================================================ */
|
|
282
|
+
/* SAFE AREA (Lh 2026-09-28, timebank on Android: the bar's buttons lay under the gesture bar) — the room the phone's own
|
|
283
|
+
bars take over the page when the app runs edge to edge (viewport-fit=cover: Android Chrome, an iPhone home-screen app).
|
|
284
|
+
PUBLISHED: backgrounds still run to the edge; what the member reads or taps stands clear of them. 0 without cover.
|
|
285
|
+
--_safe-b-open = the bottom inset the keyboard does NOT already cover (a keyboard up hides the gesture bar). */
|
|
286
|
+
:root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
287
|
+
* { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
|
|
288
|
+
|
|
282
289
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
283
290
|
@property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
|
|
284
291
|
@property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
|
|
@@ -553,6 +560,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
553
560
|
display: flex;
|
|
554
561
|
flex-direction: column;
|
|
555
562
|
height: 100dvh;
|
|
563
|
+
padding-top: var(--safe-top); /* under the phone's status bar when the app runs edge to edge (Lh 2026-09-28) */
|
|
556
564
|
overflow: clip;
|
|
557
565
|
position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
|
|
558
566
|
container-type: size; /* a share of the SCREEN's height (a no-bar step's lead, step.css) reads it as cqh — a real phone's height, or the gallery's frame */
|
|
@@ -1355,7 +1363,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1355
1363
|
border-radius: var(--r-card) var(--r-card) 0 0;
|
|
1356
1364
|
border-bottom: 0;
|
|
1357
1365
|
box-shadow: var(--sheet-shadow, var(--shadow-sheet));
|
|
1358
|
-
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
1366
|
+
--_foot: calc(var(--card-gap) + var(--btn-h) + var(--card-pad) + var(--_safe-b-open)); /* the foot strip's height (below) — merged here, one bare .sheet rule */
|
|
1359
1367
|
}
|
|
1360
1368
|
.sheet--full { --_y: 0%; --_cover: 0rem; }
|
|
1361
1369
|
.sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
|
|
@@ -1364,7 +1372,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1364
1372
|
/* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
|
|
1365
1373
|
have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
|
|
1366
1374
|
inherits it. A sheet whose head is taller retunes it on the box. */
|
|
1367
|
-
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
|
|
1375
|
+
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h) + var(--safe-bottom)); } /* + the gesture bar: the peeked head stands above it */
|
|
1368
1376
|
/* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
|
|
1369
1377
|
setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
|
|
1370
1378
|
:root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
|
|
@@ -1422,12 +1430,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1422
1430
|
stops that far above the sheet's edge. At peek there is only the handle and the title: the foot is gone.
|
|
1423
1431
|
One row, ≤ 2 buttons. <div class="card__foot"><button class="btn btn--primary btn--block">…</button></div> */
|
|
1424
1432
|
/* …and the link row under the button, when there is one (its line + the gap over it) */
|
|
1425
|
-
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad)); }
|
|
1433
|
+
.sheet:has(> .card__foot--link) { --_foot: calc(var(--card-gap) + var(--btn-h) + var(--sp-1) + var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) + var(--card-pad) + var(--_safe-b-open)); }
|
|
1426
1434
|
.sheet > .card__foot {
|
|
1427
1435
|
position: absolute; inset: calc(-1 * var(--border-w)) 0 0; z-index: 1; /* the sheet's border box: the same height the sheet's own % reads */
|
|
1428
1436
|
align-items: end;
|
|
1429
1437
|
align-content: end;
|
|
1430
|
-
padding: 0 var(--card-pad) var(--card-pad);
|
|
1438
|
+
padding: 0 var(--card-pad) calc(var(--card-pad) + var(--_safe-b-open));
|
|
1431
1439
|
background: linear-gradient(var(--card-bg), var(--card-bg)) bottom / 100% var(--_foot) no-repeat;
|
|
1432
1440
|
pointer-events: none;
|
|
1433
1441
|
translate: 0 calc(-1 * var(--_y) - var(--_drag, 0rem));
|
|
@@ -1443,7 +1451,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1443
1451
|
.sheet > :not(.sheet__body) { flex-shrink: 0; }
|
|
1444
1452
|
:is(.sheet--peek, .sheet--away) > .sheet__body { padding-bottom: 0; }
|
|
1445
1453
|
.sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
|
|
1446
|
-
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover)); }
|
|
1454
|
+
.sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: calc(var(--card-pad) + var(--_cover) + var(--_safe-b-open)); }
|
|
1447
1455
|
|
|
1448
1456
|
/* grab handle — first child of the sheet. Drawn as a small bar, but it is
|
|
1449
1457
|
the thumb's grab area for the whole strip above the title: a pseudo widens
|
|
@@ -1482,6 +1490,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1482
1490
|
whole ask is that switch: peek shows it (a row is --tap tall plus its own
|
|
1483
1491
|
padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
|
|
1484
1492
|
.sheet > .card__head { min-height: var(--sheet-head-h); }
|
|
1493
|
+
/* a sheet's title is ONE line, cut with … when the phone is too narrow (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน"
|
|
1494
|
+
wrapped to two lines at 390px) — the thing at its end keeps its place beside it; a caption still takes its own row */
|
|
1495
|
+
.sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { flex-wrap: nowrap; }
|
|
1496
|
+
.sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
1485
1497
|
.sheet__head {
|
|
1486
1498
|
display: grid;
|
|
1487
1499
|
grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
|
|
@@ -1623,7 +1635,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1623
1635
|
position: absolute;
|
|
1624
1636
|
inset-inline: 0;
|
|
1625
1637
|
bottom: 0;
|
|
1626
|
-
height: var(--tap-lg);
|
|
1638
|
+
height: calc(var(--tap-lg) + var(--_safe-b-open));
|
|
1639
|
+
padding-bottom: var(--_safe-b-open);
|
|
1640
|
+
box-sizing: border-box;
|
|
1627
1641
|
margin: 0;
|
|
1628
1642
|
display: grid;
|
|
1629
1643
|
place-items: center;
|
|
@@ -1653,11 +1667,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1653
1667
|
.sheet__body:has(> .sheet-steps) { display: flex; flex-direction: column; }
|
|
1654
1668
|
.sheet-steps { flex-shrink: 0; min-height: calc(100% + var(--kb, 0rem)); /* the keyboard's height more: the pile of next steps stays under the keyboard, it never rises with it */ display: flex; flex-direction: column; touch-action: pan-y; position: relative; }
|
|
1655
1669
|
.sheet-steps__holder { position: absolute; top: 0; left: 0; width: 1rem; height: 1rem; padding: 0; border: 0; opacity: 0; pointer-events: none; font-size: 1rem; } /* holds the cursor (and the phone's keyboard) while a step folds; font 1rem = the browser's own size, so iOS does not zoom to it */
|
|
1656
|
-
.sheet-steps__track { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; }
|
|
1670
|
+
.sheet-steps__track { list-style: none; margin: 0; padding: 0 0 var(--sheet-steps-end, var(--sp-6)); flex: 1; /* room under the LAST step: scrolled to the end it never touches the foot's button (Lh 2026-09-28, #153) */ display: flex; flex-direction: column; }
|
|
1657
1671
|
|
|
1658
1672
|
.sheet-step {
|
|
1659
1673
|
position: relative; min-width: 0;
|
|
1660
|
-
display: grid; grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
|
|
1674
|
+
display: grid; grid-template-columns: minmax(0, 1fr); /* the column is the step's width, never its words' — a long summary is cut with …, it never pushes the row past the sheet's edge (Lh 2026-09-28, #153) */ grid-template-rows: auto 0fr; align-content: start; /* mid-fold the rows sum to less than the box: the spare room stays BELOW, never spread into the title row (Lh: it looked like a spring) */
|
|
1661
1675
|
flex: 0 0 auto;
|
|
1662
1676
|
transition: grid-template-rows var(--motion-sheet) ease, flex-grow var(--motion-sheet) ease;
|
|
1663
1677
|
--_row-h: var(--sheet-steps-row-h, calc(var(--tap-base) * 2 / 3)); /* the bar's own floor, read once (__head and __go below share it) */
|
|
@@ -1668,7 +1682,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1668
1682
|
.sheet-step--now { grid-template-rows: auto 1fr; flex-grow: 1; }
|
|
1669
1683
|
.sheet-step + .sheet-step { margin-top: var(--sheet-steps-gap, var(--sp-1)); } /* the folded bars read as a pile, a little ground between them (Lh 2026-09-25) */
|
|
1670
1684
|
.sheet-step__head { /* every step's title line is a quiet BAR (a tint of the ink, not the brand — a brand bar read as a button, Lh 2026-09-25), the same height open or folded — opening shows the white room under it, never a taller bar (Lh 2026-09-25; was a thin rule on top) */
|
|
1671
|
-
display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
|
|
1685
|
+
display: flex; align-items: center; gap: var(--sp-3); min-height: var(--_row-h); min-width: 0; /* THIN: 32px, a summary line, not a button (Lh 2026-09-25) — the tap stays 44, see __go */
|
|
1672
1686
|
padding-inline: var(--sheet-step-bar-pad-x, var(--sp-3)); border-radius: var(--sheet-step-bar-radius, var(--r-chip));
|
|
1673
1687
|
background: var(--sheet-step-bar-bg, var(--ink-tint)); color: var(--sheet-step-bar-ink, inherit);
|
|
1674
1688
|
border-top: var(--sheet-step-border-w, 0rem) solid var(--sheet-step-border, currentColor); /* a rule instead of the bar: set this, the bar's bg transparent, radius 0 */
|
|
@@ -1682,7 +1696,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1682
1696
|
.sheet-step__summary { color: inherit; opacity: 0.8; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1683
1697
|
.sheet-step__check { margin-inline-start: auto; }
|
|
1684
1698
|
.sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
|
|
1685
|
-
.sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); }
|
|
1699
|
+
.sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
|
|
1686
1700
|
.sheet-step__inner > * { min-width: 0; }
|
|
1687
1701
|
.sheet-step__inner > .link { justify-self: center; } /* a link of its own row in a step (skip, "change number", "set PIN again"): centred under the step, as the skip (Lh 2026-09-25) */
|
|
1688
1702
|
.sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s linear var(--motion-sheet); }
|
|
@@ -1832,7 +1846,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1832
1846
|
margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge; its own padding is the inset */
|
|
1833
1847
|
margin-bottom: calc(var(--sp-2) - var(--screen-gap));
|
|
1834
1848
|
}
|
|
1835
|
-
.screen.screen--fill:has(> .appbar) { padding-top:
|
|
1849
|
+
.screen.screen--fill:has(> .appbar) { padding-top: var(--safe-top); --header-gap: 0rem; }
|
|
1836
1850
|
/* …and a STEP touches it, as it touches a TopBar: a full sheet's top edge on the bar's bottom edge (0.6.5 left 8px of base between
|
|
1837
1851
|
them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
|
|
1838
1852
|
.screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
|
|
@@ -2098,7 +2112,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2098
2112
|
.icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
|
|
2099
2113
|
.icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
|
|
2100
2114
|
/* fab: the square floating action, bottom-right of a stage or screen */
|
|
2101
|
-
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
|
|
2115
|
+
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
|
|
2102
2116
|
|
|
2103
2117
|
/* ---- buttons ---- */
|
|
2104
2118
|
.btn {
|
|
@@ -2224,13 +2238,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2224
2238
|
(step.css); on a scrolling screen a sticky last row (below). --bar-reserve stays published — the bar's nominal
|
|
2225
2239
|
height — for the one rule that still needs it without measuring: a .card--centre's cap (card.css).
|
|
2226
2240
|
--kb is the on-screen keyboard's height (cardds.js): the bar rides above it. */
|
|
2227
|
-
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2241
|
+
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
2228
2242
|
.action-bar {
|
|
2229
2243
|
position: relative;
|
|
2230
2244
|
z-index: var(--z-bar); /* above .sheet-stack sheets, which paint at --level 1-5 */
|
|
2231
2245
|
max-width: var(--screen-w);
|
|
2232
2246
|
margin-inline: auto;
|
|
2233
|
-
padding: var(--sp-3) var(--sp-4) var(--sp-5);
|
|
2247
|
+
padding: var(--sp-3) var(--sp-4) calc(var(--sp-5) + var(--_safe-b-open)); /* its ground runs under the phone's gesture bar, its buttons stand above it */
|
|
2234
2248
|
display: flex;
|
|
2235
2249
|
gap: var(--sp-2);
|
|
2236
2250
|
}
|
|
@@ -2297,12 +2311,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2297
2311
|
.action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
|
|
2298
2312
|
.action-bar__tier > .btn { flex: 1; }
|
|
2299
2313
|
.action-bar__tier--link { justify-content: center; min-height: 0; } /* a Link under the buttons, e.g. "details" that opens a Modal — its row as tall as a small link, not a tap (Lh 2026-09-23) */
|
|
2300
|
-
:has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2314
|
+
:has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
2301
2315
|
/* more rows (Lh 2026-09-23: a bar may hold several rows of controls — keeping it short is the designer's job): each row past two
|
|
2302
2316
|
adds a tap-tall row and its gap to the reserve, so the words above never run under the bar */
|
|
2303
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2304
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2305
|
-
:has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
2317
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(3)) { --bar-reserve: calc(2 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
2318
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(4)) { --bar-reserve: calc(3 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
2319
|
+
:has(> .action-bar--tiers > .action-bar__tier:nth-child(5)) { --bar-reserve: calc(4 * (var(--tap) + var(--sp-3)) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6) + var(--safe-bottom)); }
|
|
2306
2320
|
|
|
2307
2321
|
/* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
|
|
2308
2322
|
:is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
|
|
@@ -3234,6 +3248,16 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3234
3248
|
.link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
|
|
3235
3249
|
.card--3 .link--muted { color: var(--card-3-muted); }
|
|
3236
3250
|
|
|
3251
|
+
/* ---- bullets: a list, each item led by a small square picture as tall as a line of its words (Lh 2026-09-28, timebank #153) ----
|
|
3252
|
+
<ul class="bullets"><li class="bullets__item"><img class="bullets__pic" alt=""><span class="bullets__text">…</span></li></ul>
|
|
3253
|
+
The picture is the app's (an image or any node); none = the wireframe's crossed square. It sits on the item's FIRST line
|
|
3254
|
+
(1lh — the line's own height, so it follows the text size), the words wrap beside it, never under it. */
|
|
3255
|
+
.bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--bullets-gap, var(--sp-3)); font: var(--bullets-font, var(--type-body)); }
|
|
3256
|
+
.bullets__item { display: flex; align-items: start; gap: var(--bullets-pic-gap, var(--sp-3)); min-width: 0; }
|
|
3257
|
+
.bullets__pic { flex: none; inline-size: var(--bullets-pic-size, 1lh); block-size: var(--bullets-pic-size, 1lh); min-height: 0; object-fit: contain; border-radius: var(--bullets-pic-radius, 0rem); display: grid; place-items: center; }
|
|
3258
|
+
.bullets__pic.ph { border-radius: var(--bullets-pic-radius, 0rem); }
|
|
3259
|
+
.bullets__text { flex: 1; min-width: 0; }
|
|
3260
|
+
|
|
3237
3261
|
/* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
|
|
3238
3262
|
/* ============================================================
|
|
3239
3263
|
cardds/media.css — media & display inside cards:
|
|
@@ -3717,7 +3741,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3717
3741
|
.float-bar {
|
|
3718
3742
|
position: fixed;
|
|
3719
3743
|
inset-inline: 0;
|
|
3720
|
-
bottom: var(--sp-5);
|
|
3744
|
+
bottom: calc(var(--sp-5) + var(--safe-bottom));
|
|
3721
3745
|
max-width: var(--screen-w);
|
|
3722
3746
|
margin-inline: auto;
|
|
3723
3747
|
padding-inline: var(--float-bar-pad-x, var(--sp-4));
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ export * from './lists/Row.js';
|
|
|
65
65
|
export * from './lists/Kv.js';
|
|
66
66
|
export * from './lists/Timeline.js';
|
|
67
67
|
export * from './lists/Legend.js';
|
|
68
|
+
export * from './lists/Bullets.js';
|
|
68
69
|
export * from './lists/Bubble.js';
|
|
69
70
|
export * from './media/Quote.js';
|
|
70
71
|
export * from './media/Postcard.js';
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,7 @@ export * from './lists/Row.js';
|
|
|
69
69
|
export * from './lists/Kv.js';
|
|
70
70
|
export * from './lists/Timeline.js';
|
|
71
71
|
export * from './lists/Legend.js';
|
|
72
|
+
export * from './lists/Bullets.js';
|
|
72
73
|
export * from './lists/Bubble.js';
|
|
73
74
|
export * from './media/Quote.js';
|
|
74
75
|
export * from './media/Postcard.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
/** a bullet's picture: an image URL, or any node (an Icon, an <img> of the app's own) */
|
|
3
|
+
export type BulletPic = string | ReactNode;
|
|
4
|
+
export interface BulletsProps extends ComponentProps<'ul'> {
|
|
5
|
+
/** the picture every item leads with, unless the item gives its own; none = a placeholder square */
|
|
6
|
+
pic?: BulletPic;
|
|
7
|
+
}
|
|
8
|
+
/** Bullets — a list whose items each lead with a small square PICTURE as tall as a line of their words (Lh 2026-09-28: an agreement's
|
|
9
|
+
* short version, one point per item); children are Bullets. Pictures are decoration (alt=""), the words carry the meaning. */
|
|
10
|
+
export declare function Bullets({ pic, className, ...rest }: BulletsProps): import("react").JSX.Element;
|
|
11
|
+
export interface BulletProps extends ComponentProps<'li'> {
|
|
12
|
+
/** this item's own picture (wins over the list's) */
|
|
13
|
+
pic?: BulletPic;
|
|
14
|
+
}
|
|
15
|
+
/** Bullet — one item of Bullets: its picture, then its words. */
|
|
16
|
+
export declare function Bullet({ pic, className, children, ...rest }: BulletProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
const PicOfList = createContext(undefined);
|
|
5
|
+
/** Bullets — a list whose items each lead with a small square PICTURE as tall as a line of their words (Lh 2026-09-28: an agreement's
|
|
6
|
+
* short version, one point per item); children are Bullets. Pictures are decoration (alt=""), the words carry the meaning. */
|
|
7
|
+
export function Bullets({ pic, className, ...rest }) {
|
|
8
|
+
return (_jsx(PicOfList.Provider, { value: pic, children: _jsx("ul", { className: cx('bullets', className), ...rest }) }));
|
|
9
|
+
}
|
|
10
|
+
/** Bullet — one item of Bullets: its picture, then its words. */
|
|
11
|
+
export function Bullet({ pic, className, children, ...rest }) {
|
|
12
|
+
const ofList = useContext(PicOfList);
|
|
13
|
+
const shown = pic ?? ofList;
|
|
14
|
+
return (_jsxs("li", { className: cx('bullets__item', className), ...rest, children: [shown == null ? _jsx("span", { className: "bullets__pic ph", "aria-hidden": "true" })
|
|
15
|
+
: typeof shown === 'string' ? _jsx("img", { className: "bullets__pic", src: shown, alt: "" })
|
|
16
|
+
: _jsx("span", { className: "bullets__pic", "aria-hidden": "true", children: shown }), _jsx("span", { className: "bullets__text", children: children })] }));
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.20",
|
|
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.",
|