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