@usableapp/cardds 0.1.6 → 0.2.0
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 +67 -8
- package/css/actions.css +1 -1
- package/css/base.css +1 -1
- package/css/choice.css +19 -22
- package/css/layover.css +0 -16
- package/css/lists.css +50 -0
- package/css/palette.css +5 -0
- package/css/theme.template.css +49 -0
- package/css/tokens.css +100 -69
- package/dist/actions/Chip.d.ts +8 -16
- package/dist/actions/Chip.js +7 -10
- package/dist/cardds.css +176 -109
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lists/Bubble.d.ts +18 -0
- package/dist/lists/Bubble.js +13 -0
- package/dist/scaffold/AppBar.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,8 +105,8 @@ cp -R ~/Sites/cardds/css ~/Sites/cardds/fonts ~/Sites/cardds/cardds.css \
|
|
|
105
105
|
folder next to it, and `fonts/` beside that (`css/fonts.css` reaches the faces
|
|
106
106
|
as `../fonts/`). `icons.svg` is referenced by path from your markup
|
|
107
107
|
(`<use href="icons.svg#bell">`), so put it where those references resolve.
|
|
108
|
-
Then follow the rules below — the short version is: tune `
|
|
109
|
-
nothing else, and never write a raw hex or px in a component.
|
|
108
|
+
Then follow the rules below — the short version is: tune your own `theme.css`
|
|
109
|
+
(a copy of `css/theme.template.css`, see "A project's theme") and nothing else, and never write a raw hex or px in a component.
|
|
110
110
|
|
|
111
111
|
The repo is private, so cloning needs an account with access (`everysundays`
|
|
112
112
|
on GitHub) — set that up as an SSH host alias if you already use a different
|
|
@@ -297,6 +297,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
297
297
|
|---|---|---|
|
|
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
|
+
| `css/theme.template.css` | **a project's theme** — the four size bases + the colour roles, at their defaults; NOT in the bundle: copied into the project as its `theme.css`, loaded after `cardds.css` | copy it, don't edit it |
|
|
300
301
|
| `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
|
|
301
302
|
| `css/base.css` | reset, surface, typography classes, `.base-content` | rarely |
|
|
302
303
|
| `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 |
|
|
@@ -308,10 +309,10 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
308
309
|
| `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
310
|
| `css/numbers.css` | `.ring` gauge, `.track` steps, `.card--band` + `.band-stack`, `.dotgrid`, `.picker`, `.badge`, `.bars` | big numbers |
|
|
310
311
|
| `css/people.css` | `.avatar` (sm/lg/xl, outline, add, on, halo), `.avatar-stack`, `.avatar-pick` | people |
|
|
311
|
-
| `css/choice.css` | `.check`, `.toggle`, `.chip
|
|
312
|
-
| `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.timeline`, `.legend`, `.link` | lists inside cards |
|
|
312
|
+
| `css/choice.css` | `.check`, `.toggle`, `.chip-grid` + `.chip--pick` (+ `--sign`), `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
|
|
313
|
+
| `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.thread` + `.bubble` (chat: theirs left, mine right, no tail), `.timeline`, `.legend`, `.link` | lists inside cards |
|
|
313
314
|
| `css/media.css` | `.card--cover`, `.quote`, `.tile-grid` + `.tile`, `.card--fold`, `.mosaic`, `.wave` | media & display |
|
|
314
|
-
| `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.
|
|
315
|
+
| `css/layover.css` | `.modal` (the ask: `__lift` + `__drawer`, closes on any drawer button → `cardds:modal`), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.deck` | cards over content |
|
|
315
316
|
| `icons.svg` | Lucide sprite (ISC), 56 minimal stroke icons | add symbols as needed |
|
|
316
317
|
| `src/` → `dist/` | the React library: `src/<group>/<Name>.tsx`, one thin component per pattern, JSDoc = the rule; built by `npm run build` (tsc) into `dist/` with `.d.ts`; `src/type/icons.ts` is generated from `icons.svg` | when adding a component |
|
|
317
318
|
| `gallery/` | the gallery site: `npm run dev` → `/` every story, `/:Name` one component (`gallery/src/Gallery.tsx` globs `.design-sync/previews/`) | the demo |
|
|
@@ -502,6 +503,63 @@ Demo: the *skeleton* block at the top of `elements.html`.
|
|
|
502
503
|
`--on-accent` to its own ink and bg, so every accent consumer (buttons,
|
|
503
504
|
checks, toggles, rings, selected days) inverts without extra rules.
|
|
504
505
|
|
|
506
|
+
## A project's theme
|
|
507
|
+
|
|
508
|
+
A project retunes cardds from ONE file of its own, and nothing else. Copy
|
|
509
|
+
`css/theme.template.css` into the project as `theme.css`, load it **after**
|
|
510
|
+
`cardds.css`, change values:
|
|
511
|
+
|
|
512
|
+
```css
|
|
513
|
+
:root {
|
|
514
|
+
--sp-base: 0.3rem; /* roomier */
|
|
515
|
+
--r-base: 0.5rem; /* tighter corners */
|
|
516
|
+
--color-surface: #faf6ee; --color-ink: #1f1b16; --color-paper: #fff;
|
|
517
|
+
--color-brand: #c2410c; --color-on-brand: #fff;
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
```html
|
|
522
|
+
<html data-palette="own"> <!-- colours of your own = coloured mode; any value no stock palette uses -->
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
That file is the whole public surface:
|
|
526
|
+
|
|
527
|
+
| what | tokens | moves |
|
|
528
|
+
|---|---|---|
|
|
529
|
+
| room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
|
|
530
|
+
| type | `--fs-base` (1rem = 16) | every `--fs-*`, `--badge`, `--icon-xs` |
|
|
531
|
+
| corners | `--r-base` (1rem = 16) | every `--r-*`; `0` = square. `--r-chip` stays fully round |
|
|
532
|
+
| the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
|
|
533
|
+
| faces | `--font-ui`, `--font-display` | |
|
|
534
|
+
| colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
|
|
535
|
+
| colour roles (the escape hatch) | `--surface`, `--on-surface(-muted)`, `--card-{1,2,3}-{bg,ink}`, `--card-border`, `--card-muted`, `--card-2-muted`, `--accent`, `--on-accent`, `--action-{bg,ink,border}`, `--pos`, `--neg`, `--dot-{a,b}` | a role set directly wins over its derivation; the tints derive from the roles |
|
|
536
|
+
|
|
537
|
+
Rules:
|
|
538
|
+
|
|
539
|
+
- **A base, never a multiplier.** Every size token is `calc(base × multiplier)`
|
|
540
|
+
in `tokens.css`; the multipliers are the system's proportions. A project
|
|
541
|
+
never sets a derived token (`--sp-4`, `--fs-h1`, `--r-card`, `--tap`…), a
|
|
542
|
+
private one (`--_*`), or a published one (`--card-pad`, `--bar-reserve`, `--sheet-*`).
|
|
543
|
+
- **On `:root` only.** The derived tokens are computed where they are declared
|
|
544
|
+
(`:root`), so a base set on a subtree does not reach them.
|
|
545
|
+
- **After `cardds.css`.** A stock palette's block and a project's `:root` weigh
|
|
546
|
+
the same; the later file wins.
|
|
547
|
+
- **Coloured mode is the attribute.** `data-palette`, whatever its value, is what
|
|
548
|
+
trades the wireframe's borders for the card's shadow and the controls' soft
|
|
549
|
+
edge (`palette.css`, `forms.css`). Colours of your own → `<html data-palette="own">`.
|
|
550
|
+
Without it the colours still apply, on wireframe borders.
|
|
551
|
+
- **cardds does not judge the colours.** No contrast check, no warning — a
|
|
552
|
+
theme's values are the project's call. The derivations are only starting points;
|
|
553
|
+
any role can be set outright.
|
|
554
|
+
- `--text-scale` (below) is the member's setting, not the theme's: it scales
|
|
555
|
+
everything, the bases included.
|
|
556
|
+
|
|
557
|
+
Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
|
|
558
|
+
palette `own` shows a colour well per colour base.
|
|
559
|
+
`tests/tokens.spec.js` holds the defaults to the px they always were and the
|
|
560
|
+
template to the defaults; the step / centre geometry runs at two extreme
|
|
561
|
+
themes (`tight`, `loose`).
|
|
562
|
+
|
|
505
563
|
## New palette
|
|
506
564
|
|
|
507
565
|
Copy any block in `palette.css`, rename, retune:
|
|
@@ -517,9 +575,10 @@ Copy any block in `palette.css`, rename, retune:
|
|
|
517
575
|
|
|
518
576
|
## Scale: everything is rem
|
|
519
577
|
|
|
520
|
-
Every length token
|
|
521
|
-
`--
|
|
522
|
-
|
|
578
|
+
Every length token resolves to rem — a multiplier × one of four rem bases
|
|
579
|
+
(`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
|
|
580
|
+
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.75; see "A project's theme") — so the
|
|
581
|
+
whole system still resizes from one number: the root font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
|
|
523
582
|
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
|
|
524
583
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
525
584
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
package/css/actions.css
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
.appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
|
|
59
59
|
/* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
|
|
60
60
|
Lh 2026-09-13) stand on the tap floor: a chip that is a switch grows to --tap here */
|
|
61
|
-
.appbar .chip--
|
|
61
|
+
.appbar .chip--pick { min-height: var(--tap); }
|
|
62
62
|
|
|
63
63
|
/* ---- icon button (round) ---- */
|
|
64
64
|
.icon-btn {
|
package/css/base.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
in cards, rows and bodies stay selectable (an address, a code, a number
|
|
9
9
|
the member wants to copy), and inputs are untouched. Native buttons and
|
|
10
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--
|
|
11
|
+
button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--pick,
|
|
12
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
13
|
user-select: none;
|
|
14
14
|
-webkit-user-select: none;
|
package/css/choice.css
CHANGED
|
@@ -71,27 +71,11 @@
|
|
|
71
71
|
.toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
|
|
72
72
|
.toggle:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
73
73
|
|
|
74
|
-
/* ----
|
|
75
|
-
|
|
76
|
-
.chip--toggle
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
content: "";
|
|
80
|
-
width: var(--icon-xs);
|
|
81
|
-
height: var(--icon-xs);
|
|
82
|
-
flex: none;
|
|
83
|
-
background: currentColor;
|
|
84
|
-
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
85
|
-
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
86
|
-
}
|
|
87
|
-
.chip--toggle:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
88
|
-
/* checked: the same plus turns 45° into an x — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
89
|
-
.chip--toggle:has(:checked)::after { rotate: 45deg; }
|
|
90
|
-
@media (prefers-reduced-motion: no-preference) { .chip--toggle::after { transition: rotate var(--motion-slide); } }
|
|
91
|
-
.chip--toggle:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
92
|
-
|
|
93
|
-
/* ---- chip grid: pick one slot / many options in a grid ----
|
|
94
|
-
<div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div> */
|
|
74
|
+
/* ---- chip pick: a chip that is a choice — a native radio/checkbox inside the label, the picked one filled with accent.
|
|
75
|
+
Full-width in a .chip-grid (one slot / many options), or hugging its word on a .chip-row with --sign (an interest
|
|
76
|
+
picker: + off, × on). Was two things, .chip--toggle and .chip--pick — one chip, one state, the sign is a variant (Lh 2026-09-16).
|
|
77
|
+
<div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div>
|
|
78
|
+
<div class="chip-row"><label class="chip chip--pick chip--sign"><input type="checkbox">Nature</label>…</div> */
|
|
95
79
|
.chip-grid {
|
|
96
80
|
display: grid;
|
|
97
81
|
grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
|
|
@@ -102,10 +86,23 @@
|
|
|
102
86
|
Matters most in scripts without word spaces (Thai): min-content is the
|
|
103
87
|
whole string, tracks get wide, and hugging chips leave a ragged grid. */
|
|
104
88
|
.chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); position: relative; }
|
|
105
|
-
.chip--pick { cursor: pointer; }
|
|
89
|
+
.chip--pick { cursor: pointer; position: relative; } /* the input covers the whole chip, on a row as in a grid */
|
|
106
90
|
.chip--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
|
|
107
91
|
.chip--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
108
92
|
.chip--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
93
|
+
/* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
94
|
+
.chip--sign { padding-right: var(--sp-2); }
|
|
95
|
+
.chip--sign::after {
|
|
96
|
+
content: "";
|
|
97
|
+
width: var(--icon-xs);
|
|
98
|
+
height: var(--icon-xs);
|
|
99
|
+
flex: none;
|
|
100
|
+
background: currentColor;
|
|
101
|
+
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
102
|
+
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
103
|
+
}
|
|
104
|
+
.chip--sign:has(:checked)::after { rotate: 45deg; }
|
|
105
|
+
@media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
|
|
109
106
|
|
|
110
107
|
/* ---- day strip: a week, one day selected ----
|
|
111
108
|
<div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
|
package/css/layover.css
CHANGED
|
@@ -271,22 +271,6 @@
|
|
|
271
271
|
}
|
|
272
272
|
.callout__title { font: var(--type-title); }
|
|
273
273
|
.callout__sub { font: var(--type-caption); opacity: 0.7; }
|
|
274
|
-
/* speech chip: a chip with the same tail (mascot / greeting) */
|
|
275
|
-
.chip--bubble { position: relative; }
|
|
276
|
-
.chip--bubble::after {
|
|
277
|
-
content: "";
|
|
278
|
-
position: absolute;
|
|
279
|
-
left: var(--sp-4);
|
|
280
|
-
bottom: calc(-1 * var(--sp-2) + var(--border-w));
|
|
281
|
-
width: var(--sp-3);
|
|
282
|
-
height: var(--sp-3);
|
|
283
|
-
rotate: 45deg;
|
|
284
|
-
background: inherit;
|
|
285
|
-
border: inherit;
|
|
286
|
-
border-top: 0;
|
|
287
|
-
border-left: 0;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
274
|
/* ---- deck: swipe cards; the last child is on top ----
|
|
291
275
|
<div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
|
|
292
276
|
<div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
|
package/css/lists.css
CHANGED
|
@@ -87,6 +87,56 @@
|
|
|
87
87
|
.kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
|
|
88
88
|
.card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
|
|
89
89
|
|
|
90
|
+
/* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
|
|
91
|
+
No tail: a rounded block whose corner nearest the sender is square-ish
|
|
92
|
+
(bottom-left for theirs, bottom-right for mine), the way messaging apps
|
|
93
|
+
draw it. Under it a caption line (time · a tick), beside it small round
|
|
94
|
+
actions (a heart, more). A .thread stacks them.
|
|
95
|
+
<div class="thread">
|
|
96
|
+
<div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
|
|
97
|
+
<div class="bubble bubble--right">…</div>
|
|
98
|
+
</div> */
|
|
99
|
+
.thread { display: grid; gap: var(--sp-4); }
|
|
100
|
+
/* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
|
|
101
|
+
and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
|
|
102
|
+
.bubble {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: minmax(0, max-content) auto;
|
|
105
|
+
justify-content: start;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: var(--sp-1) var(--sp-3);
|
|
108
|
+
max-width: 88%;
|
|
109
|
+
margin-right: auto; /* theirs: hugs the left */
|
|
110
|
+
}
|
|
111
|
+
.bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
|
|
112
|
+
.bubble__body, .bubble__meta { grid-column: 1; }
|
|
113
|
+
.bubble__actions { grid-column: 2; grid-row: 1; }
|
|
114
|
+
.bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
|
|
115
|
+
.bubble--right > .bubble__actions { grid-column: 1; }
|
|
116
|
+
.bubble__body {
|
|
117
|
+
margin: 0;
|
|
118
|
+
min-width: 0;
|
|
119
|
+
padding: var(--sp-3) var(--sp-4);
|
|
120
|
+
border-radius: var(--r-card);
|
|
121
|
+
border-bottom-left-radius: var(--r-control);
|
|
122
|
+
background: var(--card-2-bg);
|
|
123
|
+
color: var(--card-2-ink);
|
|
124
|
+
font: var(--type-body);
|
|
125
|
+
overflow-wrap: anywhere;
|
|
126
|
+
}
|
|
127
|
+
.bubble--right > .bubble__body {
|
|
128
|
+
border-bottom-left-radius: var(--r-card);
|
|
129
|
+
border-bottom-right-radius: var(--r-control);
|
|
130
|
+
background: var(--accent);
|
|
131
|
+
color: var(--on-accent);
|
|
132
|
+
text-align: right;
|
|
133
|
+
}
|
|
134
|
+
.bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
|
|
135
|
+
.bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
|
|
136
|
+
.bubble--right > .bubble__meta { justify-self: end; }
|
|
137
|
+
.bubble__meta > .icon { color: var(--accent); }
|
|
138
|
+
.card--3 .bubble__meta { color: inherit; opacity: 0.7; }
|
|
139
|
+
|
|
90
140
|
/* ---- timeline: a hairline with date chips, entries to the right ----
|
|
91
141
|
<ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
|
|
92
142
|
.timeline {
|
package/css/palette.css
CHANGED
|
@@ -108,5 +108,10 @@
|
|
|
108
108
|
|
|
109
109
|
/* Colored palettes trade wireframe outlines for soft elevation. */
|
|
110
110
|
[data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
|
|
111
|
+
/* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
|
|
112
|
+
--color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
|
|
113
|
+
"own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
|
|
114
|
+
edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
|
|
115
|
+
[data-palette] { --card-border: transparent; --action-border: transparent; }
|
|
111
116
|
/* …but controls keep a faint edge so they still read as controls */
|
|
112
117
|
[data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
cardds/theme.template.css — A PROJECT'S THEME. Copy, rename, retune.
|
|
3
|
+
|
|
4
|
+
Not part of the bundle: copy this file into the project as its own
|
|
5
|
+
`theme.css` and load it AFTER cardds.css. It is the whole public
|
|
6
|
+
surface of the system — the four size bases and the colour roles.
|
|
7
|
+
Everything else in tokens.css is derived from these; a project never
|
|
8
|
+
sets a derived token (--sp-4, --fs-h1, --r-card, --tap, --sheet-*…)
|
|
9
|
+
and never a multiplier.
|
|
10
|
+
|
|
11
|
+
The values below are the defaults (the wireframe): as shipped, this
|
|
12
|
+
file changes nothing (tests/tokens.spec.js holds it to that).
|
|
13
|
+
|
|
14
|
+
On :root only. The size tokens are computed where tokens.css declares
|
|
15
|
+
them (:root), so a base set on a subtree does not reach them.
|
|
16
|
+
A stock palette's colours and this file's have the same weight — the
|
|
17
|
+
later file wins, so: after cardds.css.
|
|
18
|
+
============================================================ */
|
|
19
|
+
|
|
20
|
+
:root {
|
|
21
|
+
/* ---- size: four bases, the multipliers stay the system's ---- */
|
|
22
|
+
--sp-base: 0.25rem; /* 4 the room BETWEEN things: every --sp-N = base × N, so paddings and gaps */
|
|
23
|
+
--fs-base: 1rem; /* 16 the type: every --fs-*, the count badge, the inline icon */
|
|
24
|
+
--r-base: 1rem; /* 16 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
|
|
25
|
+
--tap-base: 3rem; /* 48 the size of THINGS: touch targets, control heights, icons, avatars, ring, tiles
|
|
26
|
+
(a touch target never goes under 44px, whatever this says) */
|
|
27
|
+
/* the whole system at once (a member's text-size setting) is --text-scale, a unitless number — not a theme's job */
|
|
28
|
+
|
|
29
|
+
/* ---- faces ---- */
|
|
30
|
+
--font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif;
|
|
31
|
+
--font-display: "FC Pride", var(--font-ui);
|
|
32
|
+
|
|
33
|
+
/* ---- colour: five bases, the roles derive (tone 2, the muted inks, the tints) ----
|
|
34
|
+
No default — unset = the wireframe. With colours of your own, put the page in coloured mode:
|
|
35
|
+
<html data-palette="own"> (any value no stock palette uses): cards take a shadow for their
|
|
36
|
+
wireframe border, controls a soft edge. */
|
|
37
|
+
/* --color-surface: #f9e8dc; */ /* the app's ground */
|
|
38
|
+
/* --color-ink: #241f1b; */ /* the words */
|
|
39
|
+
/* --color-paper: #ffffff; */ /* the plain card, neutral buttons and fields */
|
|
40
|
+
/* --color-brand: #ef4e0b; */ /* the primary action, the strong card */
|
|
41
|
+
/* --color-on-brand: #fff4ec; */ /* the words on the brand */
|
|
42
|
+
|
|
43
|
+
/* ---- colour roles: only where a derivation is not what you want — a role set here wins over it ----
|
|
44
|
+
--surface --on-surface --on-surface-muted
|
|
45
|
+
--card-1-bg --card-1-ink --card-2-bg --card-2-ink --card-3-bg --card-3-ink (three card tones)
|
|
46
|
+
--card-border --card-muted --card-2-muted
|
|
47
|
+
--accent --on-accent --action-bg --action-ink --action-border
|
|
48
|
+
--pos --neg --dot-a --dot-b */
|
|
49
|
+
}
|
package/css/tokens.css
CHANGED
|
@@ -15,56 +15,69 @@
|
|
|
15
15
|
--screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
|
|
16
16
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
17
17
|
|
|
18
|
-
/* ----
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
--sp-
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
|
19
|
+
Every size token below is multiplier × its family's base. A project sets a BASE
|
|
20
|
+
and never a multiplier, so the proportions of the system hold at any size.
|
|
21
|
+
The px in the comments are at these defaults (1rem = 16px). */
|
|
22
|
+
--sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
|
|
23
|
+
--fs-base: 1rem; /* 16 type: --fs-* */
|
|
24
|
+
--r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
|
|
25
|
+
--tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
|
|
26
|
+
--header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
|
|
27
|
+
Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
|
|
28
|
+
Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
|
|
29
|
+
|
|
30
|
+
/* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
|
|
31
|
+
--sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
|
|
32
|
+
--sp-1: var(--sp-base); /* 4 */
|
|
33
|
+
--sp-2: calc(var(--sp-base) * 2); /* 8 */
|
|
34
|
+
--sp-3: calc(var(--sp-base) * 3); /* 12 */
|
|
35
|
+
--sp-4: calc(var(--sp-base) * 4); /* 16 */
|
|
36
|
+
--sp-5: calc(var(--sp-base) * 5); /* 20 */
|
|
37
|
+
--sp-6: calc(var(--sp-base) * 6); /* 24 */
|
|
38
|
+
--sp-8: calc(var(--sp-base) * 8); /* 32 */
|
|
39
|
+
--sp-10: calc(var(--sp-base) * 10); /* 40 */
|
|
40
|
+
--sp-12: calc(var(--sp-base) * 12); /* 48 */
|
|
29
41
|
|
|
30
42
|
/* ---- touch & control sizes ---- */
|
|
31
|
-
--tap:
|
|
32
|
-
--tap
|
|
33
|
-
--tap-
|
|
34
|
-
--
|
|
35
|
-
--btn-
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
43
|
+
--tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
|
|
44
|
+
--tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
|
|
45
|
+
--tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
|
|
46
|
+
--tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
|
|
47
|
+
--btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
|
|
48
|
+
--btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
|
|
49
|
+
--chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
|
|
50
|
+
--bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
|
|
51
|
+
--header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
|
|
39
52
|
--screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
|
|
40
|
-
--map-ring:
|
|
41
|
-
--fab:
|
|
42
|
-
--badge: 1.
|
|
43
|
-
--dot:
|
|
44
|
-
--dot-xs:
|
|
53
|
+
--map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
|
|
54
|
+
--fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
|
|
55
|
+
--badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
|
|
56
|
+
--dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
|
|
57
|
+
--dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
|
|
45
58
|
|
|
46
59
|
/* ---- icons (icons.svg sprite, Lucide) ---- */
|
|
47
|
-
--icon:
|
|
48
|
-
--icon-sm:
|
|
49
|
-
--icon-lg:
|
|
50
|
-
--icon-xs:
|
|
60
|
+
--icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
|
|
61
|
+
--icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
|
|
62
|
+
--icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
|
|
63
|
+
--icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
|
|
51
64
|
|
|
52
65
|
/* ---- people & big elements ---- */
|
|
53
|
-
--avatar-sm:
|
|
54
|
-
--avatar:
|
|
55
|
-
--avatar-lg:
|
|
56
|
-
--avatar-xl:
|
|
57
|
-
--ring:
|
|
58
|
-
--ring-w:
|
|
59
|
-
--tile-min:
|
|
60
|
-
--tile-h:
|
|
66
|
+
--avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
|
|
67
|
+
--avatar: calc(var(--tap-base) * 7 / 6); /* 56 card head, person row */
|
|
68
|
+
--avatar-lg: calc(var(--tap-base) * 5 / 3); /* 80 chooser row */
|
|
69
|
+
--avatar-xl: calc(var(--tap-base) * 2); /* 96 sheet head icon ring */
|
|
70
|
+
--ring: calc(var(--tap-base) * 11 / 3); /* 176 .ring gauge diameter */
|
|
71
|
+
--ring-w: calc(var(--tap-base) * 11 / 24); /* 22 .ring stroke */
|
|
72
|
+
--tile-min: calc(var(--tap-base) * 5 / 2); /* 120 .tile-grid column floor */
|
|
73
|
+
--tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
|
|
61
74
|
|
|
62
75
|
/* ---- radius ---- */
|
|
63
76
|
--r-chip: 999px; /* pills, chips, buttons — always fully round */
|
|
64
|
-
--r-card:
|
|
65
|
-
--r-media:
|
|
66
|
-
--r-control: 0.
|
|
67
|
-
--r-badge: 0.
|
|
77
|
+
--r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
|
|
78
|
+
--r-media: var(--r-base); /* 16 images inside cards */
|
|
79
|
+
--r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
|
|
80
|
+
--r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
|
|
68
81
|
|
|
69
82
|
/* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
|
|
70
83
|
--border-w: 1.5px;
|
|
@@ -85,22 +98,22 @@
|
|
|
85
98
|
--font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
|
|
86
99
|
--font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
|
|
87
100
|
|
|
88
|
-
--fs-xs: 0.
|
|
89
|
-
--fs-caption: 0.
|
|
90
|
-
--fs-label: 0.
|
|
91
|
-
--fs-sm: 0.
|
|
92
|
-
--fs-body: 0.
|
|
93
|
-
--fs-md:
|
|
94
|
-
--fs-title: 1.
|
|
95
|
-
--fs-lg: 1.
|
|
96
|
-
--fs-h2: 1.
|
|
97
|
-
--fs-2xl: 1.
|
|
98
|
-
--fs-h1: 1.
|
|
99
|
-
--fs-3xl:
|
|
100
|
-
--fs-display: clamp(2.
|
|
101
|
-
--fs-stat: clamp(1.
|
|
102
|
-
--fs-hero: 3.
|
|
103
|
-
--fs-giant: clamp(
|
|
101
|
+
--fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
|
|
102
|
+
--fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
|
|
103
|
+
--fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
|
|
104
|
+
--fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
|
|
105
|
+
--fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
|
|
106
|
+
--fs-md: var(--fs-base); /* 16 buttons */
|
|
107
|
+
--fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
|
|
108
|
+
--fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
|
|
109
|
+
--fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
|
|
110
|
+
--fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
|
|
111
|
+
--fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
|
|
112
|
+
--fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
|
|
113
|
+
--fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
|
|
114
|
+
--fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
|
|
115
|
+
--fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
|
|
116
|
+
--fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
|
|
104
117
|
|
|
105
118
|
/* level weight size / line-height family */
|
|
106
119
|
--type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
|
|
@@ -120,35 +133,53 @@
|
|
|
120
133
|
/* ============================================================
|
|
121
134
|
COLOR ROLES — wireframe defaults (grayscale).
|
|
122
135
|
palette.css overrides these under [data-palette="…"].
|
|
136
|
+
|
|
137
|
+
THE FIVE COLOUR BASES (Lh, 2026-09-20) — a project's own colours, the
|
|
138
|
+
same idea as the size bases: set a base, the roles follow.
|
|
139
|
+
--color-surface the app's ground
|
|
140
|
+
--color-ink the words
|
|
141
|
+
--color-paper the plain card, neutral buttons and fields
|
|
142
|
+
--color-brand the primary action, the strong card, the signal dot
|
|
143
|
+
--color-on-brand the words on the brand
|
|
144
|
+
They have NO default: unset, every role below is its exact wireframe
|
|
145
|
+
value. That is what the fallbacks are for — a private mix (--_*) that
|
|
146
|
+
reads an unset base is invalid, so var(--_x, #grey) lands on the grey.
|
|
147
|
+
Set, the roles derive: tone 2 = the brand washed into the surface, the
|
|
148
|
+
muted inks = the ink washed into their ground. A role set directly
|
|
149
|
+
(a palette, or a project's theme.css) always wins over its derivation.
|
|
123
150
|
============================================================ */
|
|
151
|
+
--_muted-on-surface: color-mix(in srgb, var(--color-ink) 66%, var(--color-surface, #f2f2f0));
|
|
152
|
+
--_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
|
|
153
|
+
--_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
|
|
154
|
+
--_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
|
|
124
155
|
|
|
125
156
|
/* app background ("earth-toned surface or shiny color, up to the app") */
|
|
126
|
-
--surface: #f2f2f0;
|
|
127
|
-
--on-surface: #141414;
|
|
128
|
-
--on-surface-muted: #6b6b6b;
|
|
157
|
+
--surface: var(--color-surface, #f2f2f0);
|
|
158
|
+
--on-surface: var(--color-ink, #141414);
|
|
159
|
+
--on-surface-muted: var(--_muted-on-surface, #6b6b6b);
|
|
129
160
|
|
|
130
161
|
/* card boundaries — 3 slots so stacked/adjacent cards can differ */
|
|
131
|
-
--card-1-bg: #ffffff; --card-1-ink: #141414;
|
|
132
|
-
--card-2-bg: #e6e6e3; --card-2-ink: #141414;
|
|
133
|
-
--card-3-bg: #141414; --card-3-ink: #ffffff;
|
|
162
|
+
--card-1-bg: var(--color-paper, #ffffff); --card-1-ink: var(--color-ink, #141414);
|
|
163
|
+
--card-2-bg: var(--_tone-2, #e6e6e3); --card-2-ink: var(--color-ink, #141414);
|
|
164
|
+
--card-3-bg: var(--color-brand, #141414); --card-3-ink: var(--color-on-brand, #ffffff);
|
|
134
165
|
|
|
135
166
|
--card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
|
|
136
|
-
--card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
|
|
137
|
-
--card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
|
|
167
|
+
--card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
|
|
168
|
+
--card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
|
|
138
169
|
a palette whose tone-2 ground is strong
|
|
139
170
|
sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
|
|
140
171
|
|
|
141
172
|
/* actions */
|
|
142
|
-
--accent: #141414; /* primary CTA fill */
|
|
143
|
-
--on-accent: #ffffff;
|
|
144
|
-
--action-bg: #ffffff; /* neutral buttons/chips/inputs */
|
|
145
|
-
--action-ink: #141414;
|
|
173
|
+
--accent: var(--color-brand, #141414); /* primary CTA fill */
|
|
174
|
+
--on-accent: var(--color-on-brand, #ffffff);
|
|
175
|
+
--action-bg: var(--color-paper, #ffffff); /* neutral buttons/chips/inputs */
|
|
176
|
+
--action-ink: var(--color-ink, #141414);
|
|
146
177
|
--action-border: #141414;
|
|
147
178
|
|
|
148
179
|
/* signals (chip dots, deltas) */
|
|
149
180
|
--pos: #8a8a8a;
|
|
150
181
|
--neg: #3d3d3d;
|
|
151
|
-
--dot-a: #141414;
|
|
182
|
+
--dot-a: var(--color-brand, #141414);
|
|
152
183
|
--dot-b: #8a8a8a;
|
|
153
184
|
|
|
154
185
|
/* derived tints — read the element's own ink, so they work on any slot */
|
package/dist/actions/Chip.d.ts
CHANGED
|
@@ -4,15 +4,13 @@ export interface ChipProps extends ComponentPropsWithoutRef<'span'> {
|
|
|
4
4
|
on?: boolean;
|
|
5
5
|
/** a leading signal dot: `a` (the accent signal) or `b` (the secondary) */
|
|
6
6
|
dot?: 'a' | 'b';
|
|
7
|
-
/** a speech tail under the chip (a mascot's greeting) */
|
|
8
|
-
bubble?: boolean;
|
|
9
7
|
/** the element: span (default), a link, or a button */
|
|
10
8
|
as?: 'span' | 'a' | 'button';
|
|
11
9
|
href?: string;
|
|
12
10
|
children?: ReactNode;
|
|
13
11
|
}
|
|
14
|
-
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For
|
|
15
|
-
export declare function Chip({ on, dot,
|
|
12
|
+
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
|
|
13
|
+
export declare function Chip({ on, dot, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
|
|
16
14
|
slot?: string | undefined | undefined;
|
|
17
15
|
style?: import("react").CSSProperties | undefined;
|
|
18
16
|
title?: string | undefined | undefined;
|
|
@@ -296,26 +294,20 @@ export declare function Chip({ on, dot, bubble, as, href, className, children, .
|
|
|
296
294
|
type: string | undefined;
|
|
297
295
|
className: string;
|
|
298
296
|
}, HTMLElement>;
|
|
299
|
-
export interface ChipToggleProps extends Omit<ComponentPropsWithoutRef<'label'>, 'onChange'> {
|
|
300
|
-
checked?: boolean;
|
|
301
|
-
defaultChecked?: boolean;
|
|
302
|
-
name?: string;
|
|
303
|
-
value?: string;
|
|
304
|
-
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
|
305
|
-
}
|
|
306
|
-
/** ChipToggle — an interest / filter chip: shows + off and × on; the state is a native checkbox inside the label. */
|
|
307
|
-
export declare function ChipToggle({ checked, defaultChecked, name, value, onChange, className, children, ...rest }: ChipToggleProps): import("react").JSX.Element;
|
|
308
297
|
export interface ChipPickProps extends Omit<ComponentPropsWithoutRef<'label'>, 'onChange'> {
|
|
309
|
-
/** radio for pick-one (a time slot), checkbox for pick-many (skills) */
|
|
298
|
+
/** radio for pick-one (a time slot), checkbox for pick-many (skills); `sign` defaults to checkbox */
|
|
310
299
|
type?: 'radio' | 'checkbox';
|
|
300
|
+
/** the +/× at the end: an interest / filter chip on a row, hugging its word (was ChipToggle) */
|
|
301
|
+
sign?: boolean;
|
|
311
302
|
name?: string;
|
|
312
303
|
value?: string;
|
|
313
304
|
checked?: boolean;
|
|
314
305
|
defaultChecked?: boolean;
|
|
315
306
|
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
|
316
307
|
}
|
|
317
|
-
/** ChipPick — a full-width
|
|
318
|
-
|
|
308
|
+
/** ChipPick — a chip that is a choice: full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
|
|
309
|
+
* interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */
|
|
310
|
+
export declare function ChipPick({ type, sign, name, value, checked, defaultChecked, onChange, className, children, ...rest }: ChipPickProps): import("react").JSX.Element;
|
|
319
311
|
export interface ChipRowProps extends ComponentPropsWithoutRef<'div'> {
|
|
320
312
|
/** run the row to the card's edges as a one-line scrolling strip (`.bleed`: nowrap, overflow, the hairline ink scrollbar) */
|
|
321
313
|
bleed?: boolean;
|
package/dist/actions/Chip.js
CHANGED
|
@@ -2,18 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { createElement } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Dot } from './Dot.js';
|
|
5
|
-
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For
|
|
6
|
-
export function Chip({ on, dot,
|
|
5
|
+
/** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
|
|
6
|
+
export function Chip({ on, dot, as, href, className, children, ...rest }) {
|
|
7
7
|
const el = as ?? (href ? 'a' : 'span');
|
|
8
|
-
return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on',
|
|
8
|
+
return createElement(el, { href, type: el === 'button' ? 'button' : undefined, className: cx('chip', on && 'chip--on', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b' }), children);
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
/** ChipPick — a full-width pick target in a ChipGrid (9:00 · 9:30 · …); the picked one fills with accent. */
|
|
15
|
-
export function ChipPick({ type = 'radio', name, value, checked, defaultChecked, onChange, className, children, ...rest }) {
|
|
16
|
-
return (_jsxs("label", { className: cx('chip chip--pick', className), ...rest, children: [_jsx("input", { type: type, name: name, value: value, checked: checked, defaultChecked: defaultChecked, onChange: onChange }), children] }));
|
|
10
|
+
/** ChipPick — a chip that is a choice: full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
|
|
11
|
+
* interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */
|
|
12
|
+
export function ChipPick({ type, sign, name, value, checked, defaultChecked, onChange, className, children, ...rest }) {
|
|
13
|
+
return (_jsxs("label", { className: cx('chip chip--pick', sign && 'chip--sign', className), ...rest, children: [_jsx("input", { type: type ?? (sign ? 'checkbox' : 'radio'), name: name, value: value, checked: checked, defaultChecked: defaultChecked, onChange: onChange }), children] }));
|
|
17
14
|
}
|
|
18
15
|
/** ChipRow — chips on a wrapping line (or a bleeding strip). */
|
|
19
16
|
export function ChipRow({ bleed, className, ...rest }) {
|
package/dist/cardds.css
CHANGED
|
@@ -56,56 +56,69 @@
|
|
|
56
56
|
--screen-w: 25rem; /* 400: mobile column — .screen, .action-bar, .sheet-stage */
|
|
57
57
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
58
58
|
|
|
59
|
-
/* ----
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
--sp-
|
|
64
|
-
--
|
|
65
|
-
--
|
|
66
|
-
--
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
|
60
|
+
Every size token below is multiplier × its family's base. A project sets a BASE
|
|
61
|
+
and never a multiplier, so the proportions of the system hold at any size.
|
|
62
|
+
The px in the comments are at these defaults (1rem = 16px). */
|
|
63
|
+
--sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
|
|
64
|
+
--fs-base: 1rem; /* 16 type: --fs-* */
|
|
65
|
+
--r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
|
|
66
|
+
--tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
|
|
67
|
+
--header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
|
|
68
|
+
Text-holders follow the type instead: --badge and --icon-xs (inline with captions) are on --fs-base.
|
|
69
|
+
Not on a base: --screen-w (the column), borders / rules / focus (px, crisp), shadows, motion. */
|
|
70
|
+
|
|
71
|
+
/* ---- spacing (4pt grid): the number in the name is the multiplier ---- */
|
|
72
|
+
--sp-0: calc(var(--sp-base) * 0.5); /* 2 hairline gaps (title→meta) */
|
|
73
|
+
--sp-1: var(--sp-base); /* 4 */
|
|
74
|
+
--sp-2: calc(var(--sp-base) * 2); /* 8 */
|
|
75
|
+
--sp-3: calc(var(--sp-base) * 3); /* 12 */
|
|
76
|
+
--sp-4: calc(var(--sp-base) * 4); /* 16 */
|
|
77
|
+
--sp-5: calc(var(--sp-base) * 5); /* 20 */
|
|
78
|
+
--sp-6: calc(var(--sp-base) * 6); /* 24 */
|
|
79
|
+
--sp-8: calc(var(--sp-base) * 8); /* 32 */
|
|
80
|
+
--sp-10: calc(var(--sp-base) * 10); /* 40 */
|
|
81
|
+
--sp-12: calc(var(--sp-base) * 12); /* 48 */
|
|
70
82
|
|
|
71
83
|
/* ---- touch & control sizes ---- */
|
|
72
|
-
--tap:
|
|
73
|
-
--tap
|
|
74
|
-
--tap-
|
|
75
|
-
--
|
|
76
|
-
--btn-
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
--
|
|
84
|
+
--tap-floor: 2.75rem; /* 44 the HIG floor — no --tap-base takes a touch target under it */
|
|
85
|
+
--tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
|
|
86
|
+
--tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
|
|
87
|
+
--tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: track steps, send, field with action */
|
|
88
|
+
--btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
|
|
89
|
+
--btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
|
|
90
|
+
--chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
|
|
91
|
+
--bar-h: calc(var(--tap-base) * 4 / 3); /* 64 float bar, day cell, deck actions */
|
|
92
|
+
--header-h: calc(var(--tap-base) * 5 / 4); /* 60 top bar — .topbar and .appbar alike */
|
|
80
93
|
--screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
|
|
81
|
-
--map-ring:
|
|
82
|
-
--fab:
|
|
83
|
-
--badge: 1.
|
|
84
|
-
--dot:
|
|
85
|
-
--dot-xs:
|
|
94
|
+
--map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
|
|
95
|
+
--fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
|
|
96
|
+
--badge: calc(var(--fs-base) * 1.625); /* 26 count badge */
|
|
97
|
+
--dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
|
|
98
|
+
--dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
|
|
86
99
|
|
|
87
100
|
/* ---- icons (icons.svg sprite, Lucide) ---- */
|
|
88
|
-
--icon:
|
|
89
|
-
--icon-sm:
|
|
90
|
-
--icon-lg:
|
|
91
|
-
--icon-xs:
|
|
101
|
+
--icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
|
|
102
|
+
--icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
|
|
103
|
+
--icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
|
|
104
|
+
--icon-xs: var(--fs-base); /* 16 inline with captions/chips only (row meta, link arrows) */
|
|
92
105
|
|
|
93
106
|
/* ---- people & big elements ---- */
|
|
94
|
-
--avatar-sm:
|
|
95
|
-
--avatar:
|
|
96
|
-
--avatar-lg:
|
|
97
|
-
--avatar-xl:
|
|
98
|
-
--ring:
|
|
99
|
-
--ring-w:
|
|
100
|
-
--tile-min:
|
|
101
|
-
--tile-h:
|
|
107
|
+
--avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
|
|
108
|
+
--avatar: calc(var(--tap-base) * 7 / 6); /* 56 card head, person row */
|
|
109
|
+
--avatar-lg: calc(var(--tap-base) * 5 / 3); /* 80 chooser row */
|
|
110
|
+
--avatar-xl: calc(var(--tap-base) * 2); /* 96 sheet head icon ring */
|
|
111
|
+
--ring: calc(var(--tap-base) * 11 / 3); /* 176 .ring gauge diameter */
|
|
112
|
+
--ring-w: calc(var(--tap-base) * 11 / 24); /* 22 .ring stroke */
|
|
113
|
+
--tile-min: calc(var(--tap-base) * 5 / 2); /* 120 .tile-grid column floor */
|
|
114
|
+
--tile-h: calc(var(--tap-base) * 11 / 4); /* 132 tile / band height */
|
|
102
115
|
|
|
103
116
|
/* ---- radius ---- */
|
|
104
117
|
--r-chip: 999px; /* pills, chips, buttons — always fully round */
|
|
105
|
-
--r-card:
|
|
106
|
-
--r-media:
|
|
107
|
-
--r-control: 0.
|
|
108
|
-
--r-badge: 0.
|
|
118
|
+
--r-card: calc(var(--r-base) * 2); /* 32 the one card radius: every card, row, tile, menu, sheet, fab */
|
|
119
|
+
--r-media: var(--r-base); /* 16 images inside cards */
|
|
120
|
+
--r-control: calc(var(--r-base) * 0.875); /* 14 fields, thumbs, menu rows */
|
|
121
|
+
--r-badge: calc(var(--r-base) * 0.625); /* 10 tile badge, bars */
|
|
109
122
|
|
|
110
123
|
/* ---- borders / rules / elevation (px on purpose: crisp at any scale) ---- */
|
|
111
124
|
--border-w: 1.5px;
|
|
@@ -126,22 +139,22 @@
|
|
|
126
139
|
--font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
|
|
127
140
|
--font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
|
|
128
141
|
|
|
129
|
-
--fs-xs: 0.
|
|
130
|
-
--fs-caption: 0.
|
|
131
|
-
--fs-label: 0.
|
|
132
|
-
--fs-sm: 0.
|
|
133
|
-
--fs-body: 0.
|
|
134
|
-
--fs-md:
|
|
135
|
-
--fs-title: 1.
|
|
136
|
-
--fs-lg: 1.
|
|
137
|
-
--fs-h2: 1.
|
|
138
|
-
--fs-2xl: 1.
|
|
139
|
-
--fs-h1: 1.
|
|
140
|
-
--fs-3xl:
|
|
141
|
-
--fs-display: clamp(2.
|
|
142
|
-
--fs-stat: clamp(1.
|
|
143
|
-
--fs-hero: 3.
|
|
144
|
-
--fs-giant: clamp(
|
|
142
|
+
--fs-xs: calc(var(--fs-base) * 0.6875); /* 11 overline */
|
|
143
|
+
--fs-caption: calc(var(--fs-base) * 0.75); /* 12 */
|
|
144
|
+
--fs-label: calc(var(--fs-base) * 0.8125); /* 13 */
|
|
145
|
+
--fs-sm: calc(var(--fs-base) * 0.875); /* 14 chips, segments */
|
|
146
|
+
--fs-body: calc(var(--fs-base) * 0.9375); /* 15 */
|
|
147
|
+
--fs-md: var(--fs-base); /* 16 buttons */
|
|
148
|
+
--fs-title: calc(var(--fs-base) * 1.0625); /* 17 */
|
|
149
|
+
--fs-lg: calc(var(--fs-base) * 1.25); /* 20 day numbers, tile values */
|
|
150
|
+
--fs-h2: calc(var(--fs-base) * 1.375); /* 22 */
|
|
151
|
+
--fs-2xl: calc(var(--fs-base) * 1.5); /* 24 composer, small ring */
|
|
152
|
+
--fs-h1: calc(var(--fs-base) * 1.75); /* 28 */
|
|
153
|
+
--fs-3xl: calc(var(--fs-base) * 2); /* 32 xl button */
|
|
154
|
+
--fs-display: clamp(calc(var(--fs-base) * 2.125), 9vw, calc(var(--fs-base) * 2.625)); /* 34–42 */
|
|
155
|
+
--fs-stat: clamp(calc(var(--fs-base) * 1.875), 8vw, calc(var(--fs-base) * 2.375)); /* 30–38 big numbers */
|
|
156
|
+
--fs-hero: calc(var(--fs-base) * 3.5); /* 56 the one number on a sheet */
|
|
157
|
+
--fs-giant: clamp(calc(var(--fs-base) * 3), 15vw, calc(var(--fs-base) * 4)); /* 48–64 data bands */
|
|
145
158
|
|
|
146
159
|
/* level weight size / line-height family */
|
|
147
160
|
--type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
|
|
@@ -161,35 +174,53 @@
|
|
|
161
174
|
/* ============================================================
|
|
162
175
|
COLOR ROLES — wireframe defaults (grayscale).
|
|
163
176
|
palette.css overrides these under [data-palette="…"].
|
|
177
|
+
|
|
178
|
+
THE FIVE COLOUR BASES (Lh, 2026-09-20) — a project's own colours, the
|
|
179
|
+
same idea as the size bases: set a base, the roles follow.
|
|
180
|
+
--color-surface the app's ground
|
|
181
|
+
--color-ink the words
|
|
182
|
+
--color-paper the plain card, neutral buttons and fields
|
|
183
|
+
--color-brand the primary action, the strong card, the signal dot
|
|
184
|
+
--color-on-brand the words on the brand
|
|
185
|
+
They have NO default: unset, every role below is its exact wireframe
|
|
186
|
+
value. That is what the fallbacks are for — a private mix (--_*) that
|
|
187
|
+
reads an unset base is invalid, so var(--_x, #grey) lands on the grey.
|
|
188
|
+
Set, the roles derive: tone 2 = the brand washed into the surface, the
|
|
189
|
+
muted inks = the ink washed into their ground. A role set directly
|
|
190
|
+
(a palette, or a project's theme.css) always wins over its derivation.
|
|
164
191
|
============================================================ */
|
|
192
|
+
--_muted-on-surface: color-mix(in srgb, var(--color-ink) 66%, var(--color-surface, #f2f2f0));
|
|
193
|
+
--_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
|
|
194
|
+
--_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
|
|
195
|
+
--_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
|
|
165
196
|
|
|
166
197
|
/* app background ("earth-toned surface or shiny color, up to the app") */
|
|
167
|
-
--surface: #f2f2f0;
|
|
168
|
-
--on-surface: #141414;
|
|
169
|
-
--on-surface-muted: #6b6b6b;
|
|
198
|
+
--surface: var(--color-surface, #f2f2f0);
|
|
199
|
+
--on-surface: var(--color-ink, #141414);
|
|
200
|
+
--on-surface-muted: var(--_muted-on-surface, #6b6b6b);
|
|
170
201
|
|
|
171
202
|
/* card boundaries — 3 slots so stacked/adjacent cards can differ */
|
|
172
|
-
--card-1-bg: #ffffff; --card-1-ink: #141414;
|
|
173
|
-
--card-2-bg: #e6e6e3; --card-2-ink: #141414;
|
|
174
|
-
--card-3-bg: #141414; --card-3-ink: #ffffff;
|
|
203
|
+
--card-1-bg: var(--color-paper, #ffffff); --card-1-ink: var(--color-ink, #141414);
|
|
204
|
+
--card-2-bg: var(--_tone-2, #e6e6e3); --card-2-ink: var(--color-ink, #141414);
|
|
205
|
+
--card-3-bg: var(--color-brand, #141414); --card-3-ink: var(--color-on-brand, #ffffff);
|
|
175
206
|
|
|
176
207
|
--card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
|
|
177
|
-
--card-muted: #6b6b6b; /* secondary text inside cards (the default, tone-1 card) */
|
|
178
|
-
--card-2-muted: #5f5f5f; /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
|
|
208
|
+
--card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
|
|
209
|
+
--card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
|
|
179
210
|
a palette whose tone-2 ground is strong
|
|
180
211
|
sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
|
|
181
212
|
|
|
182
213
|
/* actions */
|
|
183
|
-
--accent: #141414; /* primary CTA fill */
|
|
184
|
-
--on-accent: #ffffff;
|
|
185
|
-
--action-bg: #ffffff; /* neutral buttons/chips/inputs */
|
|
186
|
-
--action-ink: #141414;
|
|
214
|
+
--accent: var(--color-brand, #141414); /* primary CTA fill */
|
|
215
|
+
--on-accent: var(--color-on-brand, #ffffff);
|
|
216
|
+
--action-bg: var(--color-paper, #ffffff); /* neutral buttons/chips/inputs */
|
|
217
|
+
--action-ink: var(--color-ink, #141414);
|
|
187
218
|
--action-border: #141414;
|
|
188
219
|
|
|
189
220
|
/* signals (chip dots, deltas) */
|
|
190
221
|
--pos: #8a8a8a;
|
|
191
222
|
--neg: #3d3d3d;
|
|
192
|
-
--dot-a: #141414;
|
|
223
|
+
--dot-a: var(--color-brand, #141414);
|
|
193
224
|
--dot-b: #8a8a8a;
|
|
194
225
|
|
|
195
226
|
/* derived tints — read the element's own ink, so they work on any slot */
|
|
@@ -330,6 +361,11 @@
|
|
|
330
361
|
|
|
331
362
|
/* Colored palettes trade wireframe outlines for soft elevation. */
|
|
332
363
|
[data-palette] :is(.card, .band-stack) { box-shadow: var(--shadow-card); }
|
|
364
|
+
/* COLOURED MODE is the attribute, whatever its value: a project with colours of its own (the five
|
|
365
|
+
--color-* bases, or roles, in its theme.css) writes <html data-palette="own"> — no block here matches
|
|
366
|
+
"own", so only the three generic rules apply: the card's shadow, the soft outline, the field's soft
|
|
367
|
+
edge (forms.css); and the two wireframe borders step back, as in every stock palette. */
|
|
368
|
+
[data-palette] { --card-border: transparent; --action-border: transparent; }
|
|
333
369
|
/* …but controls keep a faint edge so they still read as controls */
|
|
334
370
|
[data-palette] { --outline: color-mix(in srgb, currentColor 25%, transparent); }
|
|
335
371
|
|
|
@@ -344,7 +380,7 @@
|
|
|
344
380
|
in cards, rows and bodies stay selectable (an address, a code, a number
|
|
345
381
|
the member wants to copy), and inputs are untouched. Native buttons and
|
|
346
382
|
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--
|
|
383
|
+
button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--pick,
|
|
348
384
|
.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
385
|
user-select: none;
|
|
350
386
|
-webkit-user-select: none;
|
|
@@ -1603,7 +1639,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1603
1639
|
.appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
|
|
1604
1640
|
/* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
|
|
1605
1641
|
Lh 2026-09-13) stand on the tap floor: a chip that is a switch grows to --tap here */
|
|
1606
|
-
.appbar .chip--
|
|
1642
|
+
.appbar .chip--pick { min-height: var(--tap); }
|
|
1607
1643
|
|
|
1608
1644
|
/* ---- icon button (round) ---- */
|
|
1609
1645
|
.icon-btn {
|
|
@@ -2237,27 +2273,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
2237
2273
|
.toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
|
|
2238
2274
|
.toggle:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2239
2275
|
|
|
2240
|
-
/* ----
|
|
2241
|
-
|
|
2242
|
-
.chip--toggle
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
content: "";
|
|
2246
|
-
width: var(--icon-xs);
|
|
2247
|
-
height: var(--icon-xs);
|
|
2248
|
-
flex: none;
|
|
2249
|
-
background: currentColor;
|
|
2250
|
-
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
2251
|
-
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
2252
|
-
}
|
|
2253
|
-
.chip--toggle:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
2254
|
-
/* checked: the same plus turns 45° into an x — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
2255
|
-
.chip--toggle:has(:checked)::after { rotate: 45deg; }
|
|
2256
|
-
@media (prefers-reduced-motion: no-preference) { .chip--toggle::after { transition: rotate var(--motion-slide); } }
|
|
2257
|
-
.chip--toggle:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2258
|
-
|
|
2259
|
-
/* ---- chip grid: pick one slot / many options in a grid ----
|
|
2260
|
-
<div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div> */
|
|
2276
|
+
/* ---- chip pick: a chip that is a choice — a native radio/checkbox inside the label, the picked one filled with accent.
|
|
2277
|
+
Full-width in a .chip-grid (one slot / many options), or hugging its word on a .chip-row with --sign (an interest
|
|
2278
|
+
picker: + off, × on). Was two things, .chip--toggle and .chip--pick — one chip, one state, the sign is a variant (Lh 2026-09-16).
|
|
2279
|
+
<div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div>
|
|
2280
|
+
<div class="chip-row"><label class="chip chip--pick chip--sign"><input type="checkbox">Nature</label>…</div> */
|
|
2261
2281
|
.chip-grid {
|
|
2262
2282
|
display: grid;
|
|
2263
2283
|
grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
|
|
@@ -2268,10 +2288,23 @@ button { font: inherit; cursor: pointer; }
|
|
|
2268
2288
|
Matters most in scripts without word spaces (Thai): min-content is the
|
|
2269
2289
|
whole string, tracks get wide, and hugging chips leave a ragged grid. */
|
|
2270
2290
|
.chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); position: relative; }
|
|
2271
|
-
.chip--pick { cursor: pointer; }
|
|
2291
|
+
.chip--pick { cursor: pointer; position: relative; } /* the input covers the whole chip, on a row as in a grid */
|
|
2272
2292
|
.chip--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
|
|
2273
2293
|
.chip--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
2274
2294
|
.chip--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2295
|
+
/* --sign: the trailing + that turns 45° into an × when picked — one mask, one rotate, no swap (Lh 2026-09-16) */
|
|
2296
|
+
.chip--sign { padding-right: var(--sp-2); }
|
|
2297
|
+
.chip--sign::after {
|
|
2298
|
+
content: "";
|
|
2299
|
+
width: var(--icon-xs);
|
|
2300
|
+
height: var(--icon-xs);
|
|
2301
|
+
flex: none;
|
|
2302
|
+
background: currentColor;
|
|
2303
|
+
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
2304
|
+
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
|
|
2305
|
+
}
|
|
2306
|
+
.chip--sign:has(:checked)::after { rotate: 45deg; }
|
|
2307
|
+
@media (prefers-reduced-motion: no-preference) { .chip--sign::after { transition: rotate var(--motion-slide); } }
|
|
2275
2308
|
|
|
2276
2309
|
/* ---- day strip: a week, one day selected ----
|
|
2277
2310
|
<div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
|
|
@@ -2550,6 +2583,56 @@ button { font: inherit; cursor: pointer; }
|
|
|
2550
2583
|
.kv-cell__key { font: var(--type-caption); color: var(--card-muted); }
|
|
2551
2584
|
.card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
|
|
2552
2585
|
|
|
2586
|
+
/* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
|
|
2587
|
+
No tail: a rounded block whose corner nearest the sender is square-ish
|
|
2588
|
+
(bottom-left for theirs, bottom-right for mine), the way messaging apps
|
|
2589
|
+
draw it. Under it a caption line (time · a tick), beside it small round
|
|
2590
|
+
actions (a heart, more). A .thread stacks them.
|
|
2591
|
+
<div class="thread">
|
|
2592
|
+
<div class="bubble"><p class="bubble__body">…</p><span class="bubble__actions">…icon-btns…</span><span class="bubble__meta">Dec 4 · 8:15</span></div>
|
|
2593
|
+
<div class="bubble bubble--right">…</div>
|
|
2594
|
+
</div> */
|
|
2595
|
+
.thread { display: grid; gap: var(--sp-4); }
|
|
2596
|
+
/* two columns: the message (max-content, but it gives way — minmax(0) — so a long one shrinks
|
|
2597
|
+
and wraps while the actions stay beside it) and the actions; the meta on a second row under the message */
|
|
2598
|
+
.bubble {
|
|
2599
|
+
display: grid;
|
|
2600
|
+
grid-template-columns: minmax(0, max-content) auto;
|
|
2601
|
+
justify-content: start;
|
|
2602
|
+
align-items: center;
|
|
2603
|
+
gap: var(--sp-1) var(--sp-3);
|
|
2604
|
+
max-width: 88%;
|
|
2605
|
+
margin-right: auto; /* theirs: hugs the left */
|
|
2606
|
+
}
|
|
2607
|
+
.bubble--right { grid-template-columns: auto minmax(0, max-content); justify-content: end; margin-right: 0; margin-left: auto; } /* mine: hugs the right, the actions on its left */
|
|
2608
|
+
.bubble__body, .bubble__meta { grid-column: 1; }
|
|
2609
|
+
.bubble__actions { grid-column: 2; grid-row: 1; }
|
|
2610
|
+
.bubble--right > :is(.bubble__body, .bubble__meta) { grid-column: 2; }
|
|
2611
|
+
.bubble--right > .bubble__actions { grid-column: 1; }
|
|
2612
|
+
.bubble__body {
|
|
2613
|
+
margin: 0;
|
|
2614
|
+
min-width: 0;
|
|
2615
|
+
padding: var(--sp-3) var(--sp-4);
|
|
2616
|
+
border-radius: var(--r-card);
|
|
2617
|
+
border-bottom-left-radius: var(--r-control);
|
|
2618
|
+
background: var(--card-2-bg);
|
|
2619
|
+
color: var(--card-2-ink);
|
|
2620
|
+
font: var(--type-body);
|
|
2621
|
+
overflow-wrap: anywhere;
|
|
2622
|
+
}
|
|
2623
|
+
.bubble--right > .bubble__body {
|
|
2624
|
+
border-bottom-left-radius: var(--r-card);
|
|
2625
|
+
border-bottom-right-radius: var(--r-control);
|
|
2626
|
+
background: var(--accent);
|
|
2627
|
+
color: var(--on-accent);
|
|
2628
|
+
text-align: right;
|
|
2629
|
+
}
|
|
2630
|
+
.bubble__actions { display: flex; gap: var(--sp-1); flex: none; opacity: 0.7; }
|
|
2631
|
+
.bubble__meta { display: flex; align-items: center; gap: var(--sp-1); font: var(--type-caption); color: var(--on-surface-muted); }
|
|
2632
|
+
.bubble--right > .bubble__meta { justify-self: end; }
|
|
2633
|
+
.bubble__meta > .icon { color: var(--accent); }
|
|
2634
|
+
.card--3 .bubble__meta { color: inherit; opacity: 0.7; }
|
|
2635
|
+
|
|
2553
2636
|
/* ---- timeline: a hairline with date chips, entries to the right ----
|
|
2554
2637
|
<ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
|
|
2555
2638
|
.timeline {
|
|
@@ -3122,22 +3205,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
3122
3205
|
}
|
|
3123
3206
|
.callout__title { font: var(--type-title); }
|
|
3124
3207
|
.callout__sub { font: var(--type-caption); opacity: 0.7; }
|
|
3125
|
-
/* speech chip: a chip with the same tail (mascot / greeting) */
|
|
3126
|
-
.chip--bubble { position: relative; }
|
|
3127
|
-
.chip--bubble::after {
|
|
3128
|
-
content: "";
|
|
3129
|
-
position: absolute;
|
|
3130
|
-
left: var(--sp-4);
|
|
3131
|
-
bottom: calc(-1 * var(--sp-2) + var(--border-w));
|
|
3132
|
-
width: var(--sp-3);
|
|
3133
|
-
height: var(--sp-3);
|
|
3134
|
-
rotate: 45deg;
|
|
3135
|
-
background: inherit;
|
|
3136
|
-
border: inherit;
|
|
3137
|
-
border-top: 0;
|
|
3138
|
-
border-left: 0;
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
3208
|
/* ---- deck: swipe cards; the last child is on top ----
|
|
3142
3209
|
<div class="deck"><article class="card">…</article><article class="card">…</article><article class="card card--fold">front</article></div>
|
|
3143
3210
|
<div class="deck__actions"><button class="icon-btn">×</button><button class="icon-btn icon-btn--invert">✓</button></div> */
|
package/dist/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export * from './lists/Row.js';
|
|
|
59
59
|
export * from './lists/Kv.js';
|
|
60
60
|
export * from './lists/Timeline.js';
|
|
61
61
|
export * from './lists/Legend.js';
|
|
62
|
+
export * from './lists/Bubble.js';
|
|
62
63
|
export * from './media/Quote.js';
|
|
63
64
|
export * from './media/Postcard.js';
|
|
64
65
|
export * from './media/MapArea.js';
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,7 @@ export * from './lists/Row.js';
|
|
|
62
62
|
export * from './lists/Kv.js';
|
|
63
63
|
export * from './lists/Timeline.js';
|
|
64
64
|
export * from './lists/Legend.js';
|
|
65
|
+
export * from './lists/Bubble.js';
|
|
65
66
|
export * from './media/Quote.js';
|
|
66
67
|
export * from './media/Postcard.js';
|
|
67
68
|
export * from './media/MapArea.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
|
+
export interface ThreadProps extends ComponentPropsWithoutRef<'div'> {
|
|
3
|
+
}
|
|
4
|
+
/** Thread — a stack of Bubbles: a conversation. */
|
|
5
|
+
export declare function Thread({ className, ...rest }: ThreadProps): import("react").JSX.Element;
|
|
6
|
+
export interface BubbleProps extends ComponentPropsWithoutRef<'div'> {
|
|
7
|
+
/** whose: theirs on the left (default), mine on the right */
|
|
8
|
+
side?: 'left' | 'right';
|
|
9
|
+
/** the caption under the message — a time, a tick (an Icon) */
|
|
10
|
+
meta?: ReactNode;
|
|
11
|
+
/** small round buttons beside the message (a heart, more) */
|
|
12
|
+
actions?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
|
|
16
|
+
* for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
|
|
17
|
+
*/
|
|
18
|
+
export declare function Bubble({ side, meta, actions, className, children, ...rest }: BubbleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../cx.js';
|
|
3
|
+
/** Thread — a stack of Bubbles: a conversation. */
|
|
4
|
+
export function Thread({ className, ...rest }) {
|
|
5
|
+
return _jsx("div", { className: cx('thread', className), ...rest });
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Bubble — a chat message: a rounded block, its corner nearest the sender squared (bottom-left for theirs, bottom-right
|
|
9
|
+
* for mine), no tail. `meta` sits under it on the sender's side, `actions` beside it. Stack them in a Thread.
|
|
10
|
+
*/
|
|
11
|
+
export function Bubble({ side = 'left', meta, actions, className, children, ...rest }) {
|
|
12
|
+
return (_jsxs("div", { className: cx('bubble', side === 'right' && 'bubble--right', className), ...rest, children: [_jsx("p", { className: "bubble__body", children: children }), actions != null && _jsx("span", { className: "bubble__actions", children: actions }), meta != null && _jsx("span", { className: "bubble__meta", children: meta })] }));
|
|
13
|
+
}
|
|
@@ -7,7 +7,7 @@ export interface AppBarProps extends Omit<ComponentPropsWithoutRef<'header'>, 't
|
|
|
7
7
|
/** the second line under the title */
|
|
8
8
|
sub?: ReactNode;
|
|
9
9
|
/** the trailing control(s) — an IconBtn with a chevron-down to switch context; on the home bar a labelled
|
|
10
|
-
* switch chip (
|
|
10
|
+
* switch chip (ChipPick sign, --tap tall here) and the wallet Btn, side by side (Lh 2026-09-13) */
|
|
11
11
|
action?: ReactNode;
|
|
12
12
|
}
|
|
13
13
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.",
|