@usableapp/cardds 0.7.79 → 0.7.81

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,20 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.81 — 2026-10-10
6
+
7
+ ### Added
8
+ - **`IconBtnGroup form` and `form labelled`: a small panel of its own width in the middle, with its words** (Lh 2026-10-10, from timebank's make-stamp page: two chevrons walking nine ground colours stretched over the whole column, with no words) — before → after: an `IconBtnGroup` as a child of a `Page`, a `Centre` or a `CtaPack` was always the column's full width, and its `label` was spoken only, so "words + a small control" had to be hand-laid → `form` makes the panel as wide as its buttons (each one tap, `--icon-btn-group-form-min-w`; a button with a word beside its icon is as wide as the word) and stands it in the middle of its column the way a `Btn form` is; a card's or a sheet's foot no longer stretches it. `labelled` (with `form`) SHOWS the `label` as words at the panel's left: one line, centred together, the words in the label's type and plain ink outside the frame, the gap `--icon-btn-group-gap` (default `--sp-3`), the group named by those words. Where the line is too narrow (a long label, 160% text on a 320px phone) the words wrap above the panel and both stay centred. To use it: `<IconBtnGroup form labelled label="เปลี่ยนสีพื้น"><IconBtn icon="chevron-left" label="สีก่อนหน้า" onClick={…} /><IconBtn icon="chevron-right" label="สีถัดไป" onClick={…} /></IconBtnGroup>`; without the words, `<IconBtnGroup form label="…">`. With `labelled` the root is the line that holds the words and the panel (`className`, `style`, `ref` and the knobs go there). `form` with `vertical` keeps `form` and warns; `labelled` without `form` (or without a `label`) warns and draws no words. Without `form` nothing changes. Stories `IconBtnGroup · Form`, `FormInAFoot`, `FormInASheetFoot`, `FormInACentre`, `FormInACtaPack`, `FormLongWords`; `StampScreens · MakeStamp` now walks its ground with it.
9
+
10
+ ### Fixed
11
+ - **A card in a sheet's body folded the sheet when its heading was clicked** (timebank, 2026-10-10) — before → after: a neighbour `Card heading caption foot={<Btn/>}` written inside an open `StackSheet` (or `Home` sheet) ran its button on a click on its heading, and the same click folded the sheet, because the sheet took any heading under it for its own title; a pull down that began on that heading asked for no refresh (`onRefresh`), because any head under the sheet's body was refused; and the stack's head styling reached the nested card's head (`position: relative`, a `padding-left` transition). → only the sheet's own title folds it, its own back button closes it, a pull may begin on a nested card's heading and asks for the refresh, and a nested card's head is the card's as anywhere. A click on the nested card's heading, caption or empty room presses its one button and the sheet stays open. The heads of the cards resting in a closed stack's `base` lose the same `position: relative`; nothing in them was placed by it. Nothing to change in a screen. Story `SheetStack · CardInSheet`.
12
+
13
+ ## 0.7.80 — 2026-10-10
14
+
15
+ ### Changed
16
+ - **`Btn form` keeps its own width in a card's foot** (Lh 2026-10-10, from timebank's home feed: one button filling the card's foot read too big) — before → after: any `Btn` in `CardFoot` / `Card foot` / `FeedCard foot` was stretched over the row, `form` included → a `Btn form` there is as wide as its words (with its wide sides) and centred; a plain `Btn` still fills the foot, and several still share it. The card is still pressed whole. To use it: `foot={<Btn primary form>…</Btn>}`. Which button a card wears is the project's choice. Story `CardFoot · CardFootWordsWide`.
17
+ - **The catalog knows the functions an app calls, and the MCP server serves it as it is now** (Lh 2026-10-10) — before → after: `list_components` / `get_component` / `find_components` did not know `slideScreen`, `installScreenSlide`, `setWords`, `word`, `setTextScale`, `controlsFor` (they live in `.ts` files the catalog did not read) → all six are in, with their docs. And a `cardds mcp` server loaded the catalog once and kept it for the life of its session, so a session opened before a release never learned what the release added → it now loads again whenever the sources (a live repo) or the package's catalog file changed. A session whose server was started before this change must be restarted once to get the new server.
18
+
5
19
  ## 0.7.79 — 2026-10-10
6
20
 
7
21
  ### Changed
package/README.md CHANGED
@@ -83,7 +83,7 @@ lets anyone on the same network open the dev server: use it on a network you tru
83
83
  ### In another project
84
84
 
85
85
  **React (the main way, 2026-09-16):** the package is `@usableapp/cardds` on npm
86
- (published from a `v*` tag by `.github/workflows/publish.yml`, trusted publishing).
86
+ (published from this machine: `npm run release` sends every `v*` tag the registry lacks, oldest first).
87
87
 
88
88
  ```bash
89
89
  npm i @usableapp/cardds
@@ -487,7 +487,12 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
487
487
  paragraph. Frameless, per rule 9.
488
488
  - **Foot** (`.card__foot`): the card's action buttons on one line. One
489
489
  button takes the width; up to four share it in even parts; from three
490
- on they drop their side padding so the labels keep the room.
490
+ on they drop their side padding so the labels keep the room. A panel of
491
+ up to four icon buttons (`IconBtnGroup`) takes the foot the same way; with
492
+ `form` it is as wide as its buttons (one tap each) and stands in the middle,
493
+ as `Btn form` does — in a foot, a `Page`, a `Centre` or a `CtaPack` — and
494
+ with `form labelled` its `label` is SHOWN as words at its left, the two
495
+ centred as one line (Lh 2026-10-10).
491
496
  - In React the head and the foot are the card's **slots**:
492
497
  `<Card heading caption end foot footLink>` with the content as children —
493
498
  the card writes the order (head first, foot last). A head of its own shape
@@ -751,7 +756,9 @@ question and hunts for its answer. Rules 11–13 serve that:
751
756
  receipt: items left, prices right; `.card__head`, `.kv`, `.row`); centred
752
757
  only what is not read — dots, a lone number, an icon, a mark. Empty room is
753
758
  not filled with more content; a new edge is made instead (line the buttons
754
- up with the avatar).
759
+ up with the avatar). One exception: a small control with its own words
760
+ (`IconBtnGroup form labelled`) is one centred line — the words name the
761
+ control, they are not content to be read.
755
762
  12. **Show, don't explain.** Where a known picture carries it — an avatar for a
756
763
  person, an icon, a `Ring`/`Track`/`NumberBand`, a `Chip` for a category — use it
757
764
  instead of words. Never add a label, a caption line, a comparison or a
package/bin/cardds.mjs CHANGED
@@ -72,8 +72,9 @@ import them, never copy their markup: the behaviour lives in the component. Gall
72
72
  return 0;
73
73
  }
74
74
 
75
- // the exports beside the components that are not components (docs/roles.md rows `a function, not a component`…): shape `—` and nothing to tell them from a structural part, so they are named
76
- const HELPERS = new Set(['applyMask', 'slideTo', 'useLayer', 'useLayers', 'useOptionalLayer', 'registerIcons', 'LayerHistory']);
75
+ // the exports beside the components that are not components (docs/roles.md rows `a function, not a component`…): shape `—` and nothing to tell them from a structural part, so they are named.
76
+ // A new one joins here and in the list test of tests/catalog.spec.js, or it is listed as structure
77
+ const HELPERS = new Set(['applyMask', 'slideTo', 'slideScreen', 'installScreenSlide', 'useLayer', 'useLayers', 'useOptionalLayer', 'registerIcons', 'LayerHistory', 'setWords', 'word', 'setTextScale', 'controlsFor']);
77
78
  const one = (text) => text.replace(/\s+/g, ' ').trim();
78
79
  const indent = (text, by = ' ') => text.replace(/^(?=.)/gm, by);
79
80
 
package/css/actions.css CHANGED
@@ -114,6 +114,17 @@
114
114
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
115
115
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
116
116
  .icon-btn-group--vertical > .icon-btn + .icon-btn { border-inline-start: 0; border-block-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
117
+ /* --form: the panel as wide as its buttons (each one --tap, or its word if wider), in the middle of its column the way `.btn--form` is, next to the
118
+ panel across the row (Lh 2026-10-10, from timebank: two chevrons stretched over the whole column to walk nine colours). The container
119
+ does not stretch it: a foot's `flex: 1 1 0` is undone for it below, as the foot undoes it for `.btn--form` (card.css). */
120
+ .icon-btn-group--form { flex: 0 1 auto; justify-self: center; margin-inline: auto; } /* no align-self: a sheet's foot stands its buttons on the bottom edge by `align-items: end` */
121
+ .icon-btn-group--form > .icon-btn { flex: 0 1 auto; width: auto; min-width: var(--icon-btn-group-form-min-w, var(--tap)); }
122
+ .card__foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
123
+ /* --labelled: the label's words, then the panel, ONE line in the middle (the words are read, not pressed: no frame, plain ink); too narrow → the words wrap
124
+ above the panel, both centred */
125
+ .icon-btn-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--icon-btn-group-gap, var(--sp-3)); min-width: 0; justify-self: center; margin-inline: auto; }
126
+ .icon-btn-line__words { font: var(--type-label); text-align: center; min-width: 0; }
127
+ .icon-btn-line > .icon-btn-group--form { margin-inline: 0; }
117
128
  .icon-btn-group > .icon-btn[aria-pressed="true"] { background: var(--icon-btn-group-on-bg, var(--accent)); color: var(--icon-btn-group-on-ink, var(--on-accent)); } /* `select`: a pressed button STAYS filled (Lh 2026-09-29) */
118
129
  .icon-btn-group > .icon-btn:active:not(:disabled) { scale: 1; opacity: var(--press-dim); } /* a part of one frame DIMS, it never shrinks inside it */
119
130
 
package/css/card.css CHANGED
@@ -136,6 +136,9 @@
136
136
  three on they drop their side padding so the labels keep the room. */
137
137
  .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
138
138
  .card__foot > .btn { flex: 1 1 0; min-width: 0; }
139
+ /* …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
140
+ a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
141
+ .card__foot > .btn--form { flex: 0 1 auto; }
139
142
  .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
140
143
  /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
141
144
  whole short row of its own, centred — it never shares the buttons' line */
package/css/stack.css CHANGED
@@ -369,12 +369,13 @@
369
369
  into it — an opacity fade in place, never a slide in from the left (the
370
370
  slide read as a second thing moving while the sheet itself moved; Lh,
371
371
  2026-09-11). Only the sheet travels; the button appears where it is. */
372
- .sheet-stack--tap .card__head {
372
+ /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
373
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
373
374
  position: relative;
374
375
  padding-left: 0;
375
376
  transition: padding-left var(--motion-sheet) ease;
376
377
  }
377
- .sheet-stack--tap .sheet-stack__back {
378
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
378
379
  position: absolute;
379
380
  left: 0;
380
381
  top: 50%;
@@ -389,7 +390,7 @@
389
390
  min-height: 0;
390
391
  padding: 0;
391
392
  }
392
- .sheet-stack--tap > .card.is-open .sheet-stack__back {
393
+ .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
393
394
  opacity: 1;
394
395
  visibility: visible;
395
396
  }
@@ -512,7 +513,7 @@
512
513
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
513
514
  while open; the last tab then sits on the inset (no reach into the padding) */
514
515
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
515
- .sheet-stack--tap .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
516
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
516
517
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
517
518
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
518
519
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
@@ -7,7 +7,8 @@ export interface BtnProps extends Omit<ComponentProps<'button'>, 'form'> {
7
7
  /** full width */
8
8
  block?: boolean;
9
9
  /** a form's button: sized to its words with wide side padding, centred on its own row — it reads as part of the form above it,
10
- * not a page CTA (Lh 2026-09-25). Replaces the native `form` attribute (a button outside its <form> — not a cardds pattern). */
10
+ * not a page CTA (Lh 2026-09-25). In a card's foot (`CardFoot`, `Card foot`, `FeedCard foot`) it keeps that size instead of
11
+ * filling the row. Replaces the native `form` attribute (a button outside its <form> — not a cardds pattern). */
11
12
  form?: boolean;
12
13
  /** the one giant call to action in a tinted card ("Taxi!") */
13
14
  xl?: boolean;
@@ -14,11 +14,22 @@ export interface IconBtnGroupProps extends Omit<ComponentProps<'div'>, 'onChange
14
14
  onChange?: (value: Picked) => void;
15
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
+ /** 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
+ * 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.
19
+ * Without it the panel is the width of what it stands in. Not with `vertical`: that is already one button wide, at a row's end —
20
+ * `form` wins and warns */
21
+ form?: boolean;
22
+ /** with `form`: SHOW the `label` as words at the panel's left, on one line, the two centred together — plain ink in the label's type,
23
+ * outside the frame, and the words are the group's name (`aria-labelledby`). Where the line is too narrow (a big text size on a small
24
+ * phone) the words wrap ABOVE the panel and both stay centred */
25
+ labelled?: boolean;
17
26
  }
18
27
  /** IconBtnGroup — a panel of up to FOUR icon-only buttons in ONE frame, sharing its width, a rule between them in the frame's
19
28
  * colour (Lh 2026-09-29). Each is an `IconBtn` (its `label` is its spoken name); a WORD may stand beside the icon as its child
20
29
  * (`<IconBtn label="ชาย"><Icon name="man" />ชาย</IconBtn>`) — two named sides of a `Panes`. Plain: each does its own thing on its onClick.
21
30
  * With `select`, a pressed button stays filled (aria-pressed): `one` or `many`. At home in a card's or a sheet's foot;
22
- * `vertical` at a row's end. */
23
- export declare function IconBtnGroup({ label, select, value, defaultValue, onChange, vertical, className, children, ...rest }: IconBtnGroupProps): import("react").JSX.Element;
31
+ * `vertical` at a row's end; `form` as a small control of its own in the middle of a `Page` or a `Centre`
32
+ * (`<IconBtnGroup form labelled label="เปลี่ยนสีพื้น">` = the words, then the narrow panel, both centred). With `labelled` the root
33
+ * is the line holding the words and the panel — `className`, `style` and `ref` go there, the knobs too. */
34
+ export declare function IconBtnGroup({ label, labelled, form, select, value, defaultValue, onChange, vertical, className, children, ...rest }: IconBtnGroupProps): import("react").JSX.Element;
24
35
  export {};
@@ -1,15 +1,25 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Children, cloneElement, isValidElement } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement, useId } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { useControlled, warnOnce } from '../state.js';
5
5
  /** IconBtnGroup — a panel of up to FOUR icon-only buttons in ONE frame, sharing its width, a rule between them in the frame's
6
6
  * colour (Lh 2026-09-29). Each is an `IconBtn` (its `label` is its spoken name); a WORD may stand beside the icon as its child
7
7
  * (`<IconBtn label="ชาย"><Icon name="man" />ชาย</IconBtn>`) — two named sides of a `Panes`. Plain: each does its own thing on its onClick.
8
8
  * With `select`, a pressed button stays filled (aria-pressed): `one` or `many`. At home in a card's or a sheet's foot;
9
- * `vertical` at a row's end. */
10
- export function IconBtnGroup({ label, select, value, defaultValue, onChange, vertical, className, children, ...rest }) {
9
+ * `vertical` at a row's end; `form` as a small control of its own in the middle of a `Page` or a `Centre`
10
+ * (`<IconBtnGroup form labelled label="เปลี่ยนสีพื้น">` = the words, then the narrow panel, both centred). With `labelled` the root
11
+ * is the line holding the words and the panel — `className`, `style` and `ref` go there, the knobs too. */
12
+ export function IconBtnGroup({ label, labelled, form, select, value, defaultValue, onChange, vertical, className, children, ...rest }) {
13
+ const words = useId();
11
14
  if (Children.count(children) > 4)
12
15
  warnOnce('icon-btn-group', 'cardds IconBtnGroup: more than 4 buttons — keep 4, put the rest in a Menu behind one of them');
16
+ if (form && vertical)
17
+ warnOnce('icon-btn-group-form-vertical', 'cardds IconBtnGroup: `form` and `vertical` together — `vertical` is already one button wide at a row\'s end, `form` is the panel in the middle of its column; `form` is kept, `vertical` ignored');
18
+ if (labelled && !form)
19
+ warnOnce('icon-btn-group-labelled', 'cardds IconBtnGroup: `labelled` is for `form` (the words and the narrow panel as one centred line) — without `form` the label stays the spoken name only');
20
+ if (labelled && form && !label)
21
+ warnOnce('icon-btn-group-labelled-words', 'cardds IconBtnGroup: `labelled` shows the `label` — it has none, so no words are drawn');
22
+ const line = !!(form && labelled && label);
13
23
  const [shown, request] = useControlled(value, defaultValue ?? (select === 'many' ? [] : null), onChange);
14
24
  const on = (v) => (Array.isArray(shown) ? shown.includes(v) : shown === v);
15
25
  const items = !select ? children : Children.map(children, (c) => {
@@ -32,5 +42,8 @@ export function IconBtnGroup({ label, select, value, defaultValue, onChange, ver
32
42
  },
33
43
  });
34
44
  });
35
- return _jsx("div", { role: "group", "aria-label": label, className: cx('icon-btn-group', vertical && 'icon-btn-group--vertical', className), ...rest, children: items });
45
+ const panel = (_jsx("div", { role: "group", "aria-label": line ? undefined : label, "aria-labelledby": line ? words : undefined, className: cx('icon-btn-group', form && 'icon-btn-group--form', vertical && !form && 'icon-btn-group--vertical', !line && className), ...(line ? {} : rest), children: items }));
46
+ if (!line)
47
+ return panel;
48
+ return _jsxs("div", { className: cx('icon-btn-line', className), ...rest, children: [_jsx("span", { id: words, className: "icon-btn-line__words", children: label }), panel] });
36
49
  }
package/dist/cardds.css CHANGED
@@ -822,6 +822,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
822
822
  three on they drop their side padding so the labels keep the room. */
823
823
  .card__foot { display: flex; gap: var(--card-foot-gap, var(--sp-2)); align-items: center; }
824
824
  .card__foot > .btn { flex: 1 1 0; min-width: 0; }
825
+ /* …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
826
+ a card's foot read too big — which button a card wears is the project's choice, so the foot must not undo it) */
827
+ .card__foot > .btn--form { flex: 0 1 auto; }
825
828
  .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
826
829
  /* a small link under the buttons (CardFoot link — Lh 2026-09-23, as the ActionBar's ctaLink): the row wraps, the link takes a
827
830
  whole short row of its own, centred — it never shares the buttons' line */
@@ -1432,12 +1435,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1432
1435
  into it — an opacity fade in place, never a slide in from the left (the
1433
1436
  slide read as a second thing moving while the sheet itself moved; Lh,
1434
1437
  2026-09-11). Only the sheet travels; the button appears where it is. */
1435
- .sheet-stack--tap .card__head {
1438
+ /* a sheet's OWN head and back: the card is the stack's child or, with one host wrapper, the wrapper's. A card standing in the sheet's body wears its own head */
1439
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head {
1436
1440
  position: relative;
1437
1441
  padding-left: 0;
1438
1442
  transition: padding-left var(--motion-sheet) ease;
1439
1443
  }
1440
- .sheet-stack--tap .sheet-stack__back {
1444
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head > .sheet-stack__back {
1441
1445
  position: absolute;
1442
1446
  left: 0;
1443
1447
  top: 50%;
@@ -1452,7 +1456,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1452
1456
  min-height: 0;
1453
1457
  padding: 0;
1454
1458
  }
1455
- .sheet-stack--tap > .card.is-open .sheet-stack__back {
1459
+ .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
1456
1460
  opacity: 1;
1457
1461
  visibility: visible;
1458
1462
  }
@@ -1575,7 +1579,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1575
1579
  /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
1576
1580
  while open; the last tab then sits on the inset (no reach into the padding) */
1577
1581
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
1578
- .sheet-stack--tap .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
1582
+ :is(.sheet-stack--tap > .card, .sheet-stack--tap > :not(.card, .sheet-stack__base) > .card) > .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
1579
1583
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1580
1584
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1581
1585
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
@@ -2539,6 +2543,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2539
2543
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
2540
2544
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
2541
2545
  .icon-btn-group--vertical > .icon-btn + .icon-btn { border-inline-start: 0; border-block-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
2546
+ /* --form: the panel as wide as its buttons (each one --tap, or its word if wider), in the middle of its column the way `.btn--form` is, next to the
2547
+ panel across the row (Lh 2026-10-10, from timebank: two chevrons stretched over the whole column to walk nine colours). The container
2548
+ does not stretch it: a foot's `flex: 1 1 0` is undone for it below, as the foot undoes it for `.btn--form` (card.css). */
2549
+ .icon-btn-group--form { flex: 0 1 auto; justify-self: center; margin-inline: auto; } /* no align-self: a sheet's foot stands its buttons on the bottom edge by `align-items: end` */
2550
+ .icon-btn-group--form > .icon-btn { flex: 0 1 auto; width: auto; min-width: var(--icon-btn-group-form-min-w, var(--tap)); }
2551
+ .card__foot > :is(.icon-btn-group--form, .icon-btn-line) { flex: 0 1 auto; }
2552
+ /* --labelled: the label's words, then the panel, ONE line in the middle (the words are read, not pressed: no frame, plain ink); too narrow → the words wrap
2553
+ above the panel, both centred */
2554
+ .icon-btn-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--icon-btn-group-gap, var(--sp-3)); min-width: 0; justify-self: center; margin-inline: auto; }
2555
+ .icon-btn-line__words { font: var(--type-label); text-align: center; min-width: 0; }
2556
+ .icon-btn-line > .icon-btn-group--form { margin-inline: 0; }
2542
2557
  .icon-btn-group > .icon-btn[aria-pressed="true"] { background: var(--icon-btn-group-on-bg, var(--accent)); color: var(--icon-btn-group-on-ink, var(--on-accent)); } /* `select`: a pressed button STAYS filled (Lh 2026-09-29) */
2543
2558
  .icon-btn-group > .icon-btn:active:not(:disabled) { scale: 1; opacity: var(--press-dim); } /* a part of one frame DIMS, it never shrinks inside it */
2544
2559
 
@@ -5,7 +5,7 @@ import { warnOnce } from '../state.js';
5
5
  import { CardHead } from './CardHead.js';
6
6
  import { CardFoot } from './CardFoot.js';
7
7
  import { goControl } from './cardGo.js';
8
- import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
8
+ import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '../sheets/stackCard.js';
9
9
  /**
10
10
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
11
11
  * The head and the foot are SLOTS: `heading` · `caption` · `end` (the card writes its CardHead first) and `foot` · `footLink`
@@ -61,11 +61,12 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
61
61
  }
62
62
  if (!stack || e.defaultPrevented || swallow.current)
63
63
  return;
64
- if (target.closest('.sheet-stack__back')) {
64
+ const head = card && ownHead(card, target); // the sheet's own head: a card standing in its body has a heading and a back of its own
65
+ if (head && target.closest('.sheet-stack__back')) {
65
66
  stack.onClose();
66
67
  return;
67
68
  }
68
- if (stack.open && target.closest('.card__head > :is(h1, h2, h3)')) {
69
+ if (head && stack.open && target.closest('.card__head > :is(h1, h2, h3)')) {
69
70
  stack.onClose();
70
71
  return;
71
72
  } // the title folds it; the back stays the keyboard's way
@@ -5,5 +5,6 @@ export interface CardFootProps extends ComponentProps<'div'> {
5
5
  link?: ReactNode;
6
6
  }
7
7
  /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
8
- * link goes in `link`, under it. */
8
+ * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
9
+ * should not fill the card. */
9
10
  export declare function CardFoot({ link, className, children, ...rest }: CardFootProps): import("react").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
4
- * link goes in `link`, under it. */
4
+ * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
5
+ * should not fill the card. */
5
6
  export function CardFoot({ link, className, children, ...rest }) {
6
7
  return (_jsxs("div", { className: cx('card__foot', link != null && 'card__foot--link', className), ...rest, children: [children, link != null && _jsx("div", { className: "card__foot-link", children: link })] }));
7
8
  }