@usableapp/cardds 0.7.104 → 0.7.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap. Each release section after 0.7.97 carries one `Components:` line, written by `node scripts/components-changed.mjs <previous tag>`: the components the release changed, or `all` when a file changed that any component can read (a token, the build), or `none`; check only the screens that import those.
4
4
 
5
+ ## 0.7.106 — 2026-10-21
6
+
7
+ The map edge to edge under a step's controls (`MapArea bleed`, and a step whose bleeding child reaches the sheet's edges); `Chip soft`; a card's head led by its date; `Row` in parts (`RowLead`, `RowBody`, `RowEnd`, and the props that drew them are gone); a turning two-tone card no longer cut at its edges; the catalog names a whole's parts.
8
+
9
+ Components: all (a new root token, `--accent-wash`; the gallery, the catalog build and the lint rules changed. The components a screen sees change are MapArea, SheetStep, Chip, ChipRow, CardHead, JourneyBadge, Card, Row and its parts; `Row`'s old props fail the typecheck, see its migration table)
10
+
11
+ ### Added
12
+ - **`MapArea bleed`: with `inline`, the map edge to edge of what holds it, no corner** (Lh 2026-10-19, timebank's ask sheet, the จุดนัด step: the map under แถวบ้าน / แถวนี้ must touch the sheet's left and right edges). Before → after: a `MapArea inline` in a `SheetStep` was a rounded box inside the step's inset, and no knob could take it to the sheet's edge (`--sheet-step-inner-pad-x: 0` and `--map-radius: 0` left it inside the sheet's own 20px, cut by the fold's clip) → `<MapArea inline bleed>` gives the sheet's or the card's inset back (the same edge declaration as `.bleed`, read from `--card-pad`) and drops its corner; a card clips its corner for a first or last one as it does for a `ChipRow bleed`. Ignored without `inline`; `cardds check` warns (`map-bleed-without-inline`). Gallery `MapArea/InStep`.
13
+
14
+ - **`Chip soft`: a status to see, quieter than `on`** (Lh 2026-10-21: keep what we used, add the other style). Before → after: a status in a card's head was a plain `Chip` (an edge) or `Chip on` (the full accent); the inbox's `Ribbon` drew a third look, the accent washed in, as a separate piece → that look is `Chip soft` (`.chip--soft`: `--chip-soft-bg`, default the new token `--accent-wash`, the accent washed into the paper of fields and buttons, opaque, so its words read on every tone; `--chip-soft-ink`), and `inbox/Ribbon` is gone. `on` wins when both are set. Gallery `Chip/Soft`.
15
+
16
+ - **A card's head that leads with a date; words that lead a ChipRow** (Lh 2026-10-21, timebank's request card re-laid). Before → after: a `CardHead` put its `ChipRow` on a line of its own under the heading, and the date beside the heading → a `CardHead` whose first child is a `JourneyBadge stretch` and second a `ChipRow` stands the date at the left and the chips beside it, the date always as tall as the chips (its three-line cap lifted there; the chips are not cut at two lines there), and the heading with its caption takes the whole width under them (`--card-head-lead-gap`, `--sp-3`). `ChipRow lead` sets words before the chips ("นัดที่") in the chips' own type, at their middle (`.chip-row__lead`, `--chip-row-lead-ink`). Gallery `Card/Turns*`, `Card/PaperBands`.
17
+ - **The catalog names a whole's parts** (Lh 2026-10-21, modular parts). Before → after: `cardds get Row` listed what Row's source renders (`composes`) but not the exports it is built of, so a session learned of `RowLead`, `RowBody` and `RowEnd` only by reading the description → `get` prints `parts: RowBody · RowEnd · RowLead` on a whole and `part of: Row` on a part (JSON `parts`, `partOf`; MCP `get_component` the same). A part is an export named `<Whole><Part>` that draws `.whole__part`.
18
+
19
+ ### Changed
20
+ - **`Card paper="bands"`: the heading and the words under it read as one paragraph** (Lh 2026-10-21). Before → after: the head band's lower edge had the card's padding above it and the card's gap below (20 + 16 at 375) → `--sp-2` each side (`--card-band-seam`), so only the colour divides the heading from what follows. Every `paper="bands"` card moves up by the difference.
21
+ - **`Chip sm`: one height wherever it stands** (Lh 2026-10-21: a foot's chip beside a head's chip read smaller). Before → after: a small chip in a card's head took the body's line height (32.6 at 375) and everywhere else the label's (27.5) → every `Chip sm` takes the body's line (`--chip-sm-line`). Every screen with a small chip outside a head grows by 5px a chip.
22
+ - **`Row` is composed of parts: `RowLead`, `RowBody`, `RowEnd` are children, and the props that drew them are gone** (Lh 2026-10-21: every component is broken into small parts, each its own export; the whole's single-part props are retired, not kept as shorthand). Before → after: a row was one tag whose props drew its pieces (`<Row leadIcon="gift" title="Promotions" meta="See what's available" end={<IconBtn … />} />`) → the row renders its children in order, lead · body · end (`<Row><RowLead icon="gift" /><RowBody title="Promotions" meta="See what's available" /><IconBtn … /></Row>`). The markup is the same: `.row__lead`, `.row__body`, `.row__end` and every `--row-*` knob stand as they were, and the style snapshot of the stories did not move. `lg`, `done`, `as`, `htmlFor`, `href`, `more`, `open`, `defaultOpen` and `onOpenChange` stay on `Row`. Migration, old prop → new child:
23
+
24
+ | before | after |
25
+ | --- | --- |
26
+ | `lead={<Avatar>DI</Avatar>}` | `<Avatar>DI</Avatar>` as the first child, unwrapped |
27
+ | `leadIcon="gift"` | `<RowLead icon="gift" />` first |
28
+ | `title="…" meta="…"` | `<RowBody title="…" meta="…" />` |
29
+ | `body={<Wave />}` | `<RowBody><Wave /></RowBody>` (with the title and meta, if any) |
30
+ | `endValue="+1.0 h" endCaption="Tue"` | `<RowEnd value="+1.0 h" caption="Tue" />` after the body |
31
+ | `end={<IconBtn … />}` | `<IconBtn … />` after the body, unwrapped; `end={<>a b</>}` gives `a b` |
32
+ | `children` | stay between the body and the end |
33
+
34
+ A row left on the old props fails the typecheck (`title`, `lead`, `end`… are no props of `Row`). The lint rule `row-label-button` reads the row's children now, so a button before a label row's `Check` is still caught.
35
+
36
+ ### Fixed
37
+ - **A date at the corner of a turning card's head kept the plain badge corner.** Before → after: the concentric corner (the card's corner less its padding) reached a head that is the card's first child only, and a `Card back` face starts with its unseen turn button → the face's head is read too (21.6 at 375, was 13).
38
+ - **A bleeding strip inside a `SheetStep` stopped inside the sheet's inset.** Before → after: the step's body clips at its own box (the fold mechanic), and that box sat inside the sheet's padding, so a `ChipRow bleed` in a step reached the step's edge and the fold's clip cut it there → the step's body takes the sheet's inset back and pads by it again (as `.sheet__body` does), and the step's inner publishes the whole inset (the sheet's plus `--sheet-step-inner-pad-x`, which is the step's own inset beyond the sheet's) as `--card-pad`, so any child that bleeds reaches the sheet's edges; the step's head bar and its content keep their place. The knob is a length: a theme's unitless `0` still insets the step, but the sum a bleed reads is then invalid and the child stays in the inset. `Home/AskSheet`'s past-asks strip follows (the style snapshot names it).
39
+
40
+ - **A two-tone card that turns is no longer cut off at its edges while it turns** (`Card paper="bands"` with `back`; seen on `Card · TurnsRoundFoot`, and on a phone the same way). Before → after: the two-tone rule matched the turning card itself as well as its faces, so the card clipped (`overflow: hidden`) and painted the tinted middle behind the faces; as a face turned, its near edge (nearer the eye than the card, up to about 60px past the card's top and bottom at 375) was cut in a straight line at the card's box, and a pale rounded box showed behind it → the look and the clip are on the faces only, the turning box paints nothing and clips nothing. A clipping box around the card (a scrolling screen, a gallery cell) still cuts the near edge when the card is within that reach of its edge. Components: Card. Nothing for a consumer to change. tests/card-turns.spec.js.
41
+
42
+ ## 0.7.105 — 2026-10-21
43
+
44
+ A card foot of big round buttons, and a card drawn as the gallery draws it.
45
+
46
+ Components: all (a new root token, --tap-xl; the components that change are IconBtn, CardFoot, Card and JourneyBadge)
47
+
48
+ ### Added
49
+ - **Big round buttons, and a foot made of them** (a consumer's ask, approved: a card foot of three big round buttons, one centred group, `IconBtn` only). Before → after: `IconBtn` had two sizes (`sm`, the default 48px) and everything bigger was the knob `--icon-btn-size`, whose icon stayed 28px, with no primary fill and no ceiling for a word at a big text size; a card foot could only share its line between `Btn`s. Now: `IconBtn size="lg"` (56px, `--tap-lg`) and `size="xl"` (68px, the new `--tap-xl`), all on `--tap-base` like the other sizes (knobs `--icon-btn-lg-size`, `--icon-btn-xl-size`; `--icon-btn-size` and `sm` as before); in `lg` and `xl` the icon is a share of the circle (`--icon-btn-icon`, 7 / 12 by default, the share the 48px button's 28px icon has) and a short word child grows with the text size up to `--icon-btn-word-max` of the circle (0.45) and no further; `IconBtn primary` is the primary `Btn`'s fill from the same tokens (`--btn-primary-bg` / `--btn-primary-ink`, orchard's yellow included), beside `invert`, the accent; `CardFoot round` (on a `Card`: `footRound`) is the foot of a few round buttons: each keeps its size, they stand as one group in the middle with a gap of `--sp-6` (`--card-foot-gap` sets it), on a turning two-tone card too. The Btn foot, the default and `sm` buttons, and every story drawn before are unchanged (style snapshot identical). The script answers `all` because the new root token `--tap-xl` is in tokens.css; what a consumer sees change is only `IconBtn`, `CardFoot` and `Card`, and only where the new props are used. Stories: `IconBtn/Rounds`, `IconBtn/Primary`, `IconBtn/WordAndIconKnobs`, `Card/CardFootRound`, `Card/TurnsRoundFoot`.
50
+
51
+ ### Fixed
52
+ - **A card as the gallery draws it, on a phone too: the title stops at three lines, the date keeps its own height, a turning card hugs its faces** (timebank's help card on a phone, 2026-10-21: "ต้นแบบออกแบบไว้ดี แต่งานจริง เละ"). Components: BtnTrio, CallSheet, Card, CardFeed, CardHead, FeedCard, Home, JourneyBadge, Modal, ModalAsk, ModalBody, NumberBand, NumberBandStack, Sheet, SheetStack, StackSheet, Walkthrough (`node scripts/components-changed.mjs`: every piece that draws a card's head, for the three-line title). Nothing for a consumer to change.
53
+ - **A card's title: three lines at most** (Lh 2026-10-21: "มันควรจะ max สามบรรทัด"; reverses 2026-09-16's "no clamp"). Before → after: the heading in a card's own head wrapped to as many lines as its words took (a request's title ran to five lines on a phone) → it is cut after three lines with an ellipsis. Knob `--card-head-title-lines` (3).
54
+ - **`JourneyBadge stretch`: no taller than its own three lines.** Before → after: as tall as the title block beside it, so beside a long title the date was a tall block with its words in the middle → as tall as the title block up to its three lines (day · number · month, `--sp-3` above and below), at the head's top. It turns to three lines from 3.5rem of room (was 5.5rem), so a two-line title gets them, as the 2026-10-19 rule said. A one-line title: unchanged (64.7 at 375).
55
+ - **`Card back`: the card is as tall as its taller face.** Before → after: the middle was one fixed height on both faces (`--card-middle-h`, default 6.25 taps, 275 at 375), so a short front and a short back (a request and its code) stood in a tall empty band → each face fills the card's height and the middle takes what the head and the foot leave, so the card hugs the taller face and still never changes height as it turns. `--card-middle-h` still fixes the middle where a theme sets it; its default is now `auto`. The face is a flex column (was a grid). New story `Card · TurnsLongTitle`; tests/card-turns.spec.js.
56
+ - What the style snapshot of the 492 stories moved: every heading in a card's head computes `display: flow-root` and `overflow: hidden`, with 0.2em of room over its first line given back by a negative margin (a stacked Thai mark rises over the line's box and the clip would cut it), so the words stand where they stood in all 262 such stories; `Card · LongHeading` and `CardHead · CardHeadTop` are cut to three lines (28 and 55 shorter); `Card · Turns`, `TurnsTwoLineTitle` and `TurnsControlled` are 25 shorter (the middle hugs the taller face, no longer 6.25 taps); the date in `Turns`, `TurnsControlled` and `PaperBands` keeps its size (only its new cap shows), `TurnsTwoLineTitle`'s is 81 (was 128); `SheetStack · Display` moves by 0.02px (its heading's own margin takes the room back).
57
+
5
58
  ## 0.7.104 — 2026-10-21
6
59
 
7
60
  Every value a component shows comes from a token, so what the gallery shows is what a theme gets (Lh 2026-10-20). Also: the gallery shows every foundation token, the catalog finds a piece by a plain intention, the number pieces are ruled (Stat and Tile stat drop as twins of NumberBand and KvCell), and CSS and JS written twice are written once.
package/bin/cardds.mjs CHANGED
@@ -106,6 +106,8 @@ const asText = {
106
106
  ...(c.role ? [`role: represents ${[c.role.represents, c.role.count, c.role.density, c.role.job].flat().join(' · ')}${c.role.standsInFor.length ? `; stands in for: ${c.role.standsInFor.map((s) => (s.condition ? `${s.name} (${s.condition})` : s.name)).join('; ')}` : ''}`] : []),
107
107
  ...(c.composes.length ? [`composes: ${c.composes.join(' · ')}`] : []),
108
108
  ...(c.composedBy.length ? [`composed by: ${c.composedBy.join(' · ')}`] : []),
109
+ ...(c.parts?.length ? [`parts: ${c.parts.join(' · ')} — its children, each its own export`] : []),
110
+ ...(c.partOf ? [`part of: ${c.partOf}`] : []),
109
111
  ...(c.params ? ['\nparameters', ...(c.params.length ? [] : [' (none)']), ...c.params.map((p) => ` ${p.name}${p.optional && !p.default ? '?' : ''}: ${p.type}${p.default ? ` = ${p.default}` : ''}`)] : [
110
112
  '\nprops',
111
113
  ...c.props.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type}${p.slot ? ' [slot]' : ''}${p.description ? ` — ${one(p.description)}` : ''}`)]),
package/css/actions.css CHANGED
@@ -82,6 +82,14 @@
82
82
  }
83
83
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
84
84
  .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
85
+ /* the bigger round sizes (`IconBtn size`): lg is the hero control's size (--tap-lg), xl the biggest (--tap-xl), both on --tap-base like every size.
86
+ What stands inside is a share of the circle (`--_ib`, its side), so it grows with it: the ICON is --icon-btn-icon of the side (7/12, the share of the default 48px
87
+ button's 28px icon), and a WORD (a string child) keeps to --icon-btn-word-max of the side (0.45: a word of four letters stays inside) — it still
88
+ grows with the member's text size, to that ceiling and no further. The default and the sm button keep their icon tokens and their word as before. */
89
+ .icon-btn--lg { --_ib: var(--icon-btn-lg-size, var(--tap-lg)); width: var(--_ib); height: var(--_ib); }
90
+ .icon-btn--xl { --_ib: var(--icon-btn-xl-size, var(--tap-xl)); width: var(--_ib); height: var(--_ib); }
91
+ :is(.icon-btn--lg, .icon-btn--xl) { font-size: min(var(--fs-lg), calc(var(--_ib) * var(--icon-btn-word-max, 0.45))); }
92
+ :is(.icon-btn--lg, .icon-btn--xl) > .icon { --icon-size: calc(var(--_ib) * var(--icon-btn-icon, 7 / 12)); --icon-sm-size: var(--icon-size); --icon-lg-size: var(--icon-size); } /* the Icon's own knobs */
85
93
  /* ---- icon button group: up to four icon-only commands in ONE frame (Lh 2026-09-29) ----
86
94
  <div class="icon-btn-group" role="group"><button class="icon-btn" aria-label="…">…</button>…</div>
87
95
  One pill as tall as a button, the buttons share its width, a hairline between them; in a foot it takes the row. */
@@ -139,7 +147,8 @@
139
147
  min-height: var(--btn-h);
140
148
  text-decoration: none; /* .btn works as <a> too */
141
149
  }
142
- .btn--primary { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
150
+ /* primary: a Btn and a round IconBtn fill alike, from the same tokens (the palette's primary — orchard's yellow — retunes both) */
151
+ :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--accent)); color: var(--btn-primary-ink, var(--on-accent)); border-color: transparent; }
143
152
  /* quiet keeps a faint outline so it still reads as a button on colored palettes;
144
153
  it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
145
154
  would take the light-card action ink and vanish */
@@ -187,12 +196,17 @@
187
196
  .dot--error { background: var(--signal-error); }
188
197
  .dot--info { background: var(--signal-info); }
189
198
  /* --sm: a small LABEL (a name on a note, a tag in a tight head) — low and tight; never a tap target */
190
- .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); }
199
+ /* one height wherever it stands (Lh 2026-10-21): the body's line, so a head that cuts its words with an ellipsis keeps the Thai marks over them, and a foot's or a middle's chip reads the same */
200
+ .chip--sm { min-height: 0; gap: var(--chip-sm-gap, var(--sp-1)); padding: var(--chip-sm-pad-y, var(--sp-0)) var(--chip-sm-pad-x, var(--sp-2)); line-height: var(--chip-sm-line, var(--lh-body)); }
201
+ /* soft: the accent washed into the chip's ground, no edge — a status to see, quieter than on (was the inbox Ribbon, Lh 2026-10-21) */
202
+ .chip--soft { background: var(--chip-soft-bg, var(--accent-wash)); color: var(--chip-soft-ink, var(--card-ink)); border-color: transparent; } /* the wash on the chip's own paper, opaque (--accent-wash): see-through, a dark tone came through under the light side's ink (ink.spec) */
191
203
  .chip--on { background: var(--chip-on-bg, var(--accent)); --dark: var(--accent-dark); color: var(--chip-on-ink, var(--card-ink)); border-color: transparent; }
192
204
  /* muted: an untouched default — the words wash out, the edge stays; emphasis is the difference from the neighbours (docs/reference.md 13) */
193
205
  .chip--muted { color: var(--chip-muted-ink, var(--card-muted)); } /* the chip's own fill's muted words: a muted chip on the dark card is still a light chip */
194
206
 
195
207
  .chip-row { display: flex; gap: var(--sp-2); }
208
+ /* the words that lead the chips (ChipRow `lead`, "นัดที่" before a place, Lh 2026-10-21): the chips' own type and size, at the middle of their height */
209
+ .chip-row__lead { align-self: center; font: var(--type-label); font-size: var(--chip-font-size, var(--fs-sm)); color: var(--chip-row-lead-ink, inherit); }
196
210
  .chip-row:not(.bleed) { flex-wrap: wrap; } /* a strip (.bleed, card.css) stays one line */
197
211
  .chip-row.bleed > .chip { flex: none; white-space: nowrap; } /* a chip of the strip keeps its words on one line at its own width: the strip scrolls, a chip never shrinks to fit it */
198
212
 
@@ -445,8 +459,10 @@
445
459
 
446
460
  /* ---- where it sits in what card.css draws (docs/reference.md rule 15) ---- */
447
461
  /* a head's ChipRow takes a line of its own under the title: at most two lines of chips, a chip's words cut with an ellipsis */
448
- .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * 1.5 + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
449
- .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: var(--lh-body); }
462
+ .card__head > .chip-row { flex: 1 1 100%; order: 2; max-height: calc(2 * (var(--fs-sm) * var(--chip-sm-line, var(--lh-body)) + 2 * var(--chip-sm-pad-y, var(--sp-0)) + 2 * var(--border-w)) + var(--sp-2)); overflow: hidden; }
463
+ /* …beside a date that leads the head (card.css) they stand in the date's row, every chip in sight: the meeting place may take a line of its own */
464
+ .card__head:has(> .tile-badge:first-child + .chip-row) > .chip-row { grid-column: 2; grid-row: 1; max-height: none; }
465
+ .card__head > .chip-row > .chip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
450
466
  .card__foot > .btn { flex: 1 1 0; min-width: 0; }
451
467
  /* …unless the button says its own size: `Btn form` is as wide as its words in a foot too, centred (Lh 2026-10-10: one button filling
452
468
  a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
package/css/card.css CHANGED
@@ -75,12 +75,23 @@
75
75
  .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
76
76
 
77
77
  /* Skeleton head — the heading in a card's OWN head, and only there.
78
- The heading wraps freely: no line budget, no squeeze, no clamp (Lh
79
- 2026-09-16 — a height rule on words only distorts them; content that needs
80
- more rows takes more rows). The group that holds heading ·
78
+ The heading wraps, no squeeze, up to three lines and cut there with an
79
+ ellipsis (Lh 2026-10-21: "มันควรจะ max สามบรรทัด" — a request's title ran to
80
+ four lines on a phone; reverses 2026-09-16's "no clamp"). The count is the knob
81
+ --card-head-title-lines. The group that holds heading ·
81
82
  caption is the head's own wrapping row (above) — no wrapper element, no
82
83
  height, only the row gap. */
83
- .card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
84
+ .card__head :is(h1, h2, h3) {
85
+ overflow-wrap: anywhere;
86
+ display: -webkit-box;
87
+ -webkit-box-orient: vertical;
88
+ -webkit-line-clamp: var(--card-head-title-lines, 3);
89
+ line-clamp: var(--card-head-title-lines, 3);
90
+ overflow: hidden;
91
+ padding-block-start: var(--_mark-room); /* a stacked Thai mark rises over the first line's box: room inside the clip, given back outside */
92
+ margin-block-start: calc(-1 * var(--_mark-room));
93
+ --_mark-room: 0.2em;
94
+ }
84
95
 
85
96
  /* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right BESIDE BOTH LINES — the heading and the
86
97
  caption pair up beside it, not the stamp beside the heading alone with the caption left under it. The stamp is its own size (--stamp-size,
@@ -89,19 +100,33 @@
89
100
  .card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
90
101
  .card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
91
102
 
103
+ /* a head that LEADS with a date (Lh 2026-10-21, timebank's request card): a `CardHead` whose first child is a JourneyBadge and whose second is a
104
+ ChipRow. The date stands at the left and the chips beside it, the two as tall as each other; the heading and its caption take the whole
105
+ width under them, so the heading sits right over the words of the middle. The date is always as tall as the chips. The gap under the date row is --card-head-lead-gap; the date and the chips place themselves (journey.css, actions.css).
106
+ <div class="card__head"><span class="tile-badge tile-badge--stretch">…</span><div class="chip-row">…</div><div><h2 class="t-h1">…</h2>…</div></div> */
107
+ .card__head:has(> .tile-badge:first-child + .chip-row) {
108
+ display: grid;
109
+ grid-template-columns: auto minmax(0, 1fr);
110
+ align-items: center;
111
+ row-gap: var(--card-head-lead-gap, var(--sp-3));
112
+ }
113
+ .card__head:has(> .tile-badge:first-child + .chip-row) > :nth-child(n + 3) { grid-column: 1 / -1; }
114
+
92
115
  /* a JourneyBadge at the edge of the card's own first head (the date box in the top-left or top-right corner) keeps its corner concentric
93
116
  with the card's: the card's corner (--r-card) less the room the padding leaves round it. journey.css reads it behind the theme's --tile-badge-radius,
94
117
  so a theme that sets that knob still wins. */
95
- .card > .card__head:first-child > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); }
118
+ :is(.card > .card__head:first-child, .card__face > .card__turn + .card__head) > .tile-badge:is(:first-child, :last-child) { --_badge-radius: calc(var(--r-card) - var(--card-pad)); } /* a turning card's face: its head follows the unseen turn button */
96
119
 
97
120
  /* ---- paper bands: the two-tone card (`Card paper="bands"`, Lh 2026-10-19): the head and the foot are paper bands edge to
98
121
  edge over the card's padding, no hairline; the middle between them keeps the card's tone, lightened (--card-middle-bg). The card
99
- clips, so the bands keep its corners. A two-faced card (below) carries the look on each face. */
122
+ clips, so the bands keep its corners. A two-faced card (below) carries the look on each face, never on the turning box: a clip
123
+ there cut the face off at the card's edges while it turned, its near edge being nearer the eye than the box (2026-10-21). */
100
124
  :is(.card--bands, .card--bands > .card__face) {
101
125
  background: var(--card-middle-bg, color-mix(in srgb, var(--card-bg) 35%, var(--card-1-bg)));
102
126
  --dark: var(--card-1-dark); /* the middle is mostly paper: its words take the paper's side */
103
127
  overflow: hidden;
104
128
  }
129
+ .card--bands.card--turns { background: none; overflow: visible; } /* the turning box keeps --dark: the faces' ink is computed from it */
105
130
  :is(.card--bands, .card--bands > .card__face) > :is(.card__head, .card__foot) {
106
131
  --card-bg: var(--card-1-bg);
107
132
  --dark: var(--card-1-dark);
@@ -111,16 +136,22 @@
111
136
  padding-inline: var(--card-pad);
112
137
  }
113
138
  :is(.card--bands, .card--bands > .card__face) > .card__head { margin-top: calc(-1 * var(--card-pad)); padding-block: var(--card-pad); }
139
+ :is(.card--bands, .card--bands > .card__face) > .card__head { padding-block-end: var(--card-band-seam, var(--sp-2)); }
140
+ /* the seam: the heading and the words under it read as ONE paragraph that changes colour at the band's edge (Lh 2026-10-21), so the room on
141
+ each side of the head band's lower edge is --card-band-seam, not the card's padding and gap */
142
+ :is(.card--bands, .card--bands > .card__face) > .card__head + * { margin-block-start: calc(var(--card-band-seam, var(--sp-2)) - var(--card-gap)); }
114
143
  :is(.card--bands, .card--bands > .card__face) > .card__foot { margin-bottom: calc(-1 * var(--card-pad)); padding-block: var(--sp-3); } /* the foot band is slim */
115
144
 
116
145
  /* ---- two faces: the WHOLE card turns (`Card back`, Lh 2026-10-19). The article is the turning box (.flip, base.css) and only that;
117
146
  each face is the card's paper — head · middle · foot — and both lie in one grid cell. The head and the foot are the same on both
118
- faces; the middle is one fixed height (--card-middle-h, the theme's call), so the card never changes height as it turns. The hidden
147
+ faces; the card is as tall as its taller face and each face fills it, the middle taking what the head and the foot leave (2026-10-21,
148
+ timebank: a fixed middle left a short front a tall empty band), so the card never changes height as it turns; --card-middle-h fixes
149
+ the middle's height instead, the theme's call. The hidden
119
150
  face is inert. Each face holds an unseen turn button: the keyboard's way to turn it; its ring is drawn on the face. */
120
151
  .card--turns { background: none; border: 0; padding: 0; cursor: pointer; touch-action: manipulation; }
121
152
  .card__face {
122
- display: grid;
123
- align-content: start;
153
+ display: flex;
154
+ flex-direction: column;
124
155
  gap: var(--card-gap);
125
156
  min-width: 0;
126
157
  padding: var(--card-pad);
@@ -131,7 +162,7 @@
131
162
  position: relative;
132
163
  }
133
164
  .card__face > * { min-width: 0; }
134
- .card__middle { display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, calc(var(--tap-base) * 25 / 4)); overflow: hidden; }
165
+ .card__middle { flex: 1 0 auto; display: grid; align-content: start; gap: var(--card-gap); height: var(--card-middle-h, auto); overflow: hidden; }
135
166
  .card__turn { position: absolute; inset: 0; width: var(--px); height: var(--px); padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
136
167
  .card__face:has(> .card__turn:focus-visible) { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
137
168
  .card__head > .chip-row > .chip > .icon { --icon-display: inline-block; --icon-align: -0.15em; --icon-margin: 0 var(--chip-sm-gap, var(--sp-1)) 0 0; }
@@ -155,7 +186,7 @@
155
186
  nothing jumps. Chromium/Safari paint it through the ::-webkit-scrollbar
156
187
  parts; Firefox gets its thin bar in the same ink. */
157
188
  .bleed {
158
- margin-inline: calc(-1 * var(--card-pad, 0rem));
189
+ margin-inline: calc(-1 * var(--card-pad, 0rem)); /* the edge a bleeding child takes back from what holds it; .map--bleed (sheet.css) reads the same --card-pad */
159
190
  padding-inline: var(--card-pad, 0rem);
160
191
  padding-block-end: var(--sp-2);
161
192
  scroll-padding-inline: var(--card-pad, 0rem);
@@ -172,7 +203,7 @@
172
203
  }
173
204
  /* a bleeding child sits over the container's rounded corner, so a strip that
174
205
  is genuinely first or last needs the corner clipped for it */
175
- .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
206
+ .card:has(> :is(.bleed, .map--bleed):first-child), .card:has(> :is(.bleed, .map--bleed):last-child) { overflow: clip; }
176
207
 
177
208
  /* foot: the card's action buttons on one line. One button takes the width,
178
209
  up to four share it in even parts — so the gaps read even too — and from
@@ -183,6 +214,9 @@
183
214
  whole short row of its own, centred — it never shares the buttons' line */
184
215
  .card__foot--link { flex-wrap: wrap; align-content: end; row-gap: var(--card-foot-link-gap, var(--sp-1)); }
185
216
  .card__foot-link { flex: 1 0 100%; display: flex; justify-content: center; }
217
+ /* round (CardFoot round): the foot holds a few round IconBtns, each its own size (they never share the line), as ONE group in the middle with
218
+ a gap that does not follow the card's width. The gap is wider than the Btns' (a miss between two round targets): --card-foot-gap sets it */
219
+ .card__foot--round { justify-content: center; gap: var(--card-foot-gap, var(--sp-6)); }
186
220
 
187
221
  /* media placeholder for wireframes — currentColor so it survives any palette */
188
222
  .ph {
package/css/finish.css CHANGED
@@ -76,7 +76,7 @@
76
76
  }
77
77
  /* the main button throws its own colour */
78
78
  [data-finish="lit"] .btn--primary,
79
- [data-finish="lit"] .icon-btn--invert:not(.icon-btn-group > *) {
79
+ [data-finish="lit"] :is(.icon-btn--invert, .icon-btn--primary):not(.icon-btn-group > *) {
80
80
  box-shadow: var(--finish-edge),
81
81
  0 0.0625rem 0.125rem color-mix(in srgb, var(--_shade) 20%, transparent),
82
82
  0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
package/css/journey.css CHANGED
@@ -51,9 +51,12 @@
51
51
 
52
52
  /* stretch: a date in a head (Lh 2026-10-19) — as tall as the title block beside it and no taller (the head stretches it; size
53
53
  containment keeps its own words from adding height). Its height picks the form: short (a one-line title) = two lines, the day name
54
- over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. */
54
+ over the number and the month on one line; tall (a two-line title) = three lines, day · number · month. It grows no taller than
55
+ its three lines (2026-10-21, timebank: beside a four-line title it was a tall empty block); held there, it stands at the head's top. */
55
56
  .tile-badge--stretch {
57
+ line-height: var(--tile-badge-line, var(--lh-badge)); /* the knob's own property; the cap below reads it too */
56
58
  align-self: stretch;
59
+ max-block-size: calc((2 * var(--fs-caption) + var(--fs-lg)) * var(--tile-badge-line, var(--lh-badge)) + 2 * var(--sp-3));
57
60
  container-type: size;
58
61
  inline-size: calc(var(--fs-lg) * 3.25);
59
62
  grid-template-columns: auto auto;
@@ -63,14 +66,16 @@
63
66
  column-gap: var(--sp-1);
64
67
  row-gap: 0;
65
68
  padding-block: var(--sp-3);
66
- line-height: var(--tile-badge-line, var(--lh-badge));
67
69
  }
68
70
  .tile-badge--stretch > * { line-height: inherit; }
69
71
  .tile-badge--stretch > :first-child { grid-column: 1 / -1; }
70
- @container (min-height: 5.5rem) {
72
+ @container (min-height: 3.5rem) { /* just under the three lines' own height, the most the badge grows to */
71
73
  .tile-badge--stretch > * { grid-column: 1 / -1; }
72
74
  }
73
75
 
76
+ /* where it sits in a head that leads with the date (card.css): the first cell, as tall as the chips beside it however many rows, so no cap */
77
+ .card__head:has(> .tile-badge:first-child + .chip-row) > .tile-badge--stretch:first-child { grid-column: 1; grid-row: 1; max-block-size: none; }
78
+
74
79
  /* corner badge: pin a tile badge on media (rating, price) — .card__media is relative */
75
80
  .tile-badge--corner { position: absolute; right: var(--sp-3); bottom: var(--sp-3); }
76
81
 
package/css/palette.css CHANGED
@@ -98,7 +98,7 @@
98
98
  [data-palette="orchard"] .field { background: var(--field-bg, #ffffff); color: var(--ink-onlight); }
99
99
  [data-palette="orchard"] .pin__cell { background: var(--pin-cell-bg, #ffffff); color: var(--ink-onlight); }
100
100
  /* the vivid ones, tried on the live parts (Lh 2026-09-25): primary = yellow · a user avatar = fuchsia · ring = sky · a switch on = sky · maple only on small dots (the count, dot a); the chosen stack tab stays moss */
101
- [data-palette="orchard"] .btn--primary { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
101
+ [data-palette="orchard"] :is(.btn--primary, .icon-btn--primary) { background: var(--btn-primary-bg, var(--highlight-2)); color: var(--btn-primary-ink, var(--ink-onlight)); }
102
102
  [data-palette="orchard"] .avatar:not(.avatar--outline, .avatar--add, .avatar--on) { background: var(--avatar-bg, var(--highlight-4)); --dark: 1; }
103
103
  [data-palette="orchard"] .ring::before { background: conic-gradient(var(--highlight-3) calc(var(--value) * 1%), var(--ink-tint) 0); }
104
104
  [data-palette="orchard"] .toggle:checked { background: var(--highlight-3); --accent-dark: 0; } /* sky is a light ground: its knob is onlight */
package/css/sheet.css CHANGED
@@ -434,6 +434,7 @@
434
434
  border-radius: var(--map-radius, var(--r-media));
435
435
  overflow: hidden;
436
436
  }
437
+ .map--bleed { margin-inline: calc(-1 * var(--card-pad, 0rem)); border-radius: 0; } /* edge to edge is a band, and a band has no corner; --map-radius is for the box in the inset */
437
438
  .sheet__body:has(> .map--inline:only-child) { display: flex; flex-direction: column; }
438
439
  .sheet__body > .map--inline:only-child { flex: 1 0 auto; }
439
440
 
@@ -558,8 +559,15 @@
558
559
  }
559
560
  .sheet-step__summary { color: var(--sheet-step-summary-ink, var(--card-muted)); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
560
561
  .sheet-step__check { margin-inline-start: auto; }
561
- .sheet-step__body { min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
562
- .sheet-step__inner { display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
562
+ /* a step's child that bleeds (ChipRow bleed, MapArea bleed, a Banner) reaches the SHEET's edges (Lh 2026-10-19, the ask sheet's
563
+ จุดนัด step: the map edge to edge under the step's buttons). The fold's clip is the step body's box, which sat inside the sheet's
564
+ inset, so the body takes that inset back and pads by it again, the way .sheet__body does; the inner keeps its own box and its own
565
+ inset (--sheet-step-inner-pad-x: the inset BEYOND the sheet's) and publishes the whole, the sheet's plus its own, as --card-pad,
566
+ so a .bleed cancels it exactly. The sum is computed on the body, before the inner's --card-pad takes over; outside a card there is
567
+ no --card-pad and the step keeps its own inset (0px, as .bleed reads it). The inner reads the knob itself, not the sum: a theme's
568
+ unitless 0 (SheetSteps · Ruled) is a length there but not in a calc, so the step stays right and only the bleed is lost. */
569
+ .sheet-step__body { --_step-pad: var(--sheet-step-inner-pad-x, var(--sheet-step-bar-pad-x, var(--sp-3))); --_pad: calc(var(--card-pad, 0rem) + var(--_step-pad)); margin-inline: calc(-1 * var(--card-pad, 0rem)); padding-inline: var(--card-pad, 0rem); min-height: 0; overflow: clip; overflow-clip-margin: var(--sp-1); }
570
+ .sheet-step__inner { --card-pad: var(--_pad); display: grid; gap: var(--card-gap); padding-block: var(--card-gap); padding-inline: var(--_step-pad); } /* the open step's content in the frame of its title's words (the bar's own inset), so it reads as that step's (Lh 2026-09-28, #153) */
563
571
  .sheet-step__inner > * { min-width: 0; }
564
572
  .sheet-step:not(.sheet-step--now) > .sheet-step__body { visibility: hidden; transition: visibility 0s var(--ease-linear) var(--motion-sheet); }
565
573
  .sheet-step__go { position: absolute; inset: min(0rem, (var(--_row-h) - var(--tap-sm)) / 2) 0 auto; min-height: max(100%, var(--tap-sm)); /* a thin bar still takes a 44px tap */ margin: 0; cursor: pointer; }
package/css/stack.css CHANGED
@@ -90,7 +90,7 @@
90
90
  letter-spacing: var(--track-display);
91
91
  /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
92
92
  measured from the card's top edge); line two runs under the next card */
93
- margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
93
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5) - var(--_mark-room)); /* --_mark-room: card.css, the room for the marks over line one */
94
94
  }
95
95
  /* fanned stack: a toggle opens the stack wider so every peek shows more */
96
96
  .sheet-stack--fanned { --stack-step: calc(var(--tap-base) * 19 / 6); }
package/css/tokens.css CHANGED
@@ -47,6 +47,7 @@
47
47
  --tap: max(var(--tap-floor), var(--tap-base)); /* 48 primary touch target */
48
48
  --tap-sm: max(var(--tap-floor), calc(var(--tap-base) * 11 / 12)); /* 44 minimum touch target */
49
49
  --tap-lg: max(var(--tap-floor), calc(var(--tap-base) * 7 / 6)); /* 56 hero controls: send, field with action */
50
+ --tap-xl: max(var(--tap-floor), calc(var(--tap-base) * 17 / 12)); /* 68 the biggest round control: a foot of a few big round buttons (IconBtn xl) */
50
51
  --btn-h: calc(var(--tap-base) * 13 / 12); /* 52 button height */
51
52
  --btn-xl-h: calc(var(--tap-base) * 11 / 6); /* 88 the one giant CTA */
52
53
  --chip-h: calc(var(--tap-base) * 19 / 24); /* 38 chip height */
@@ -291,6 +292,7 @@
291
292
  --ink-tint: color-mix(in srgb, currentColor 12%, transparent); /* hover, rails, rests */
292
293
  --ink-rail: color-mix(in srgb, currentColor 30%, transparent); /* todo steps, ghost edges */
293
294
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent); /* calendar marks */
295
+ --accent-wash: color-mix(in srgb, var(--accent) 22%, var(--action-bg)); /* the same wash laid on the paper of fields and buttons, opaque: a soft chip reads the same on any card (Chip soft) */
294
296
  --accent-mid: color-mix(in srgb, var(--accent) 50%, transparent);
295
297
  /* PASTELS (Lh 2026-10-20: a component never brings a colour of its own; every colour is the theme's): the nine pastel papers of a
296
298
  sticky note, a note chooser's swatch and a stamp's picture ground (src/sticky/tones.ts names them), and a stamp's paper. Light
@@ -3,6 +3,8 @@ import { type DotTone } from './Dot.js';
3
3
  export interface ChipProps extends ComponentProps<'span'> {
4
4
  /** filled accent: the selected / live state */
5
5
  on?: boolean;
6
+ /** a soft accent fill, no edge: a status that should be seen but is not the live one (a "new" in a card's head); `on` is the stronger */
7
+ soft?: boolean;
6
8
  /** quiet: an untouched default value — muted words, the thin edge, nothing to find the eye (docs/reference.md rule 13) */
7
9
  muted?: boolean;
8
10
  /** a leading signal dot: `a` (the accent signal), `b` (the secondary), or one of the signal set — ok · warn · error · info */
@@ -16,8 +18,8 @@ export interface ChipProps extends ComponentProps<'span'> {
16
18
  /** the chip's words */
17
19
  children?: ReactNode;
18
20
  }
19
- /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
20
- export declare function Chip({ on, muted, dot, sm, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
21
+ /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `soft` for a status that should be seen, `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
22
+ export declare function Chip({ on, soft, muted, dot, sm, as, href, className, children, ...rest }: ChipProps): import("react").DetailedReactHTMLElement<{
21
23
  ref?: import("react").Ref<HTMLSpanElement> | undefined;
22
24
  key?: import("react").Key | null | undefined;
23
25
  defaultChecked?: boolean | undefined;
@@ -324,9 +326,11 @@ export declare function ChipPick({ type, sign, name, value, checked, defaultChec
324
326
  export interface ChipRowProps extends ComponentProps<'div'> {
325
327
  /** run the row to the card's edges as a one-line scrolling strip (`.bleed`: nowrap, overflow, the hairline ink scrollbar) */
326
328
  bleed?: boolean;
329
+ /** the words that LEAD the chips ("Meet at" before a place): set in the chips' own type, at the middle of their height */
330
+ lead?: ReactNode;
327
331
  }
328
332
  /** ChipRow — chips on a wrapping line (or a bleeding strip). */
329
- export declare function ChipRow({ bleed, className, ...rest }: ChipRowProps): import("react").JSX.Element;
333
+ export declare function ChipRow({ bleed, lead, className, children, ...rest }: ChipRowProps): import("react").JSX.Element;
330
334
  export interface ChipGridProps extends ComponentProps<'div'> {
331
335
  }
332
336
  /** ChipGrid — equal-width ChipPicks in a grid (time slots, skills). */
@@ -3,10 +3,10 @@ import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { tagged } from '../pressable.js';
5
5
  import { Dot } from './Dot.js';
6
- /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
7
- export function Chip({ on, muted, dot, sm, as, href, className, children, ...rest }) {
6
+ /** Chip — a status or attribute tag (with an optional signal Dot), or a small tap target; `sm` = a small label only; `soft` for a status that should be seen, `on` when it is the live one. For a choice use ChipPick (`sign` for the +/× interest picker). */
7
+ export function Chip({ on, soft, muted, dot, sm, as, href, className, children, ...rest }) {
8
8
  const { el, props } = tagged(as, href);
9
- return createElement(el, { ...props, className: cx('chip', on && 'chip--on', muted && 'chip--muted', sm && 'chip--sm', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
9
+ return createElement(el, { ...props, className: cx('chip', on && 'chip--on', soft && !on && 'chip--soft', muted && 'chip--muted', sm && 'chip--sm', className), ...rest }, dot && _jsx(Dot, { b: dot === 'b', tone: dot === 'a' || dot === 'b' ? undefined : dot }), children);
10
10
  }
11
11
  /** ChipPick — a chip that is a choice, on a sheet and never in a Card (the sheet is the paper the member fills in, Lh 2026-10-18): full-width in a ChipGrid (9:00 · 9:30 · …), or `sign` on a ChipRow (+ off, × on — the
12
12
  * interest picker); the picked one fills with accent. A native radio/checkbox inside the label carries the state. */
@@ -14,8 +14,8 @@ export function ChipPick({ type, sign, name, value, checked, defaultChecked, onC
14
14
  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] }));
15
15
  }
16
16
  /** ChipRow — chips on a wrapping line (or a bleeding strip). */
17
- export function ChipRow({ bleed, className, ...rest }) {
18
- return _jsx("div", { className: cx('chip-row', bleed && 'bleed', className), ...rest });
17
+ export function ChipRow({ bleed, lead, className, children, ...rest }) {
18
+ return _jsxs("div", { className: cx('chip-row', bleed && 'bleed', className), ...rest, children: [lead != null && _jsx("span", { className: "chip-row__lead", children: lead }), children] });
19
19
  }
20
20
  /** ChipGrid — equal-width ChipPicks in a grid (time slots, skills). */
21
21
  export function ChipGrid({ className, ...rest }) {
@@ -5,17 +5,26 @@ export interface IconBtnProps extends ComponentProps<'button'> {
5
5
  label: string;
6
6
  /** the icon to show (sized to the button); or pass an Icon as children */
7
7
  icon?: IconName;
8
- /** 40px, for a card head or a top bar; default 48px */
8
+ /** 40px, for a card head or a top bar; default 48px (the short form of `size="sm"`) */
9
9
  sm?: boolean;
10
+ /** the round sizes, all on `--tap-base`: `sm` (44) · default (48) · `lg` (56, the hero control) · `xl` (68, the biggest — a card foot of a few big
11
+ * round buttons). In `lg` and `xl` the icon is a share of the circle (knob `--icon-btn-icon`) and a word child keeps inside it
12
+ * (knob `--icon-btn-word-max`). Each size has its knob: `--icon-btn-lg-size`, `--icon-btn-xl-size` */
13
+ size?: 'sm' | 'lg' | 'xl';
10
14
  /** filled accent (the forward arrow, the send button) */
11
15
  invert?: boolean;
16
+ /** filled with the primary Btn's fill (the palette's primary, `--btn-primary-bg`): the one answer, on a card whose foot is round buttons.
17
+ * `invert` is the accent, `primary` the primary — the same fill on the wireframe, two on a palette that gives them two colours */
18
+ primary?: boolean;
12
19
  /** a link styled as a round button */
13
20
  href?: string;
14
21
  }
15
- /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move.
22
+ /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move, `primary` for the
23
+ * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<CardFoot round>`); a short WORD may stand in one
24
+ * as its child (`<IconBtn size="xl" primary label="ช่วยเหลือ">ช่วย</IconBtn>`): it grows with the member's text size up to the circle's share.
16
25
  * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
17
26
  * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */
18
- export declare function IconBtn({ label, icon, sm, invert, href, className, type, children, ...rest }: IconBtnProps): import("react").DetailedReactHTMLElement<{
27
+ export declare function IconBtn({ label, icon, sm, size, invert, primary, href, className, type, children, ...rest }: IconBtnProps): import("react").DetailedReactHTMLElement<{
19
28
  ref?: import("react").Ref<HTMLButtonElement> | undefined;
20
29
  key?: import("react").Key | null | undefined;
21
30
  disabled?: boolean | undefined;
@@ -1,11 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createElement } from 'react';
3
3
  import { cx } from '../cx.js';
4
+ import { warnOnce } from '../state.js';
4
5
  import { Icon } from '../type/Icon.js';
5
- /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move.
6
+ /** IconBtn — the round icon button: `sm` in heads and bars, full size in rows and composers, `invert` for the one filled move, `primary` for the
7
+ * primary fill. `size="lg"` / `"xl"` are the big rounds (the foot of a few round answers: `<CardFoot round>`); a short WORD may stand in one
8
+ * as its child (`<IconBtn size="xl" primary label="ช่วยเหลือ">ช่วย</IconBtn>`): it grows with the member's text size up to the circle's share.
6
9
  * It is a button and nothing more: to open a Menu, give it the platform's `popoverTarget={the Menu's id}` and render that `<Menu popover actions id>`
7
10
  * wherever you like — the panel anchors to the button that opened it. To open anything else, `onClick`. */
8
- export function IconBtn({ label, icon, sm, invert, href, className, type, children, ...rest }) {
9
- const btn = createElement(href ? 'a' : 'button', { href, type: href ? undefined : type ?? 'button', 'aria-label': label, className: cx('icon-btn', sm && 'icon-btn--sm', invert && 'icon-btn--invert', className), ...rest }, icon ? _jsx(Icon, { name: icon, size: sm ? 'sm' : 'md' }) : children);
11
+ export function IconBtn({ label, icon, sm, size, invert, primary, href, className, type, children, ...rest }) {
12
+ if (invert && primary)
13
+ warnOnce('icon-btn-fill', 'cardds IconBtn: `invert` and `primary` together — two fills for one button; `primary` is drawn');
14
+ const step = size ?? (sm ? 'sm' : undefined);
15
+ const btn = createElement(href ? 'a' : 'button', { href, type: href ? undefined : type ?? 'button', 'aria-label': label, className: cx('icon-btn', step && `icon-btn--${step}`, invert && 'icon-btn--invert', primary && 'icon-btn--primary', className), ...rest }, icon ? _jsx(Icon, { name: icon, size: step === 'sm' ? 'sm' : 'md' }) : children);
10
16
  return btn;
11
17
  }
@@ -12,7 +12,7 @@ export interface IconBtnGroupProps extends Omit<ComponentProps<'div'>, 'onChange
12
12
  defaultValue?: Picked;
13
13
  /** asks the app: `one` → the pressed value · `many` → the new list */
14
14
  onChange?: (value: Picked) => void;
15
- /** the buttons one OVER the other, the panel one button wide — at a `Row`'s `end`, where a panel across would take the words' room */
15
+ /** the buttons one OVER the other, the panel one button wide — at a `Row`'s end, where a panel across would take the words' room */
16
16
  vertical?: boolean;
17
17
  /** the panel as wide as its buttons (each one tap wide, or as wide as its word), in the middle of its column the way a `Btn form` is —
18
18
  * in a `Page`, a `Centre`, a `CtaPack`, and in a card's or a sheet's foot, where it keeps that width instead of filling the row.