@usableapp/cardds 0.7.80 → 0.7.82

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,25 @@
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.82 — 2026-10-10
6
+
7
+ ### Changed
8
+ - **The edge of a `StampPlace` is round dots, close together** (Lh 2026-10-10: the dashes stood too far apart) — before → after: a stroke of 8px dashes with a gap of 12px → 2px round dots with 4px clear between them (a pitch of 6px from one centre to the next), still round at `--stamp-place-radius`, the same colours (`--stamp-place-border`, `--stamp-place-faint-border`, a card's muted ink), the same 4:5 size, the words and icon inside where they were. The ring closes wherever its length ends, so one gap, at the top left, is a little shorter or longer than the rest. A project that set `--stamp-place-dash` and `--stamp-place-gap` for 0.7.77's dashes keeps them, now with round ends (each dash is longer by one dot, each gap shorter by one dot); unset them for dots.
9
+
10
+ ### Added
11
+ - **`--stamp-place-dot`** — how wide each dot of a `StampPlace`'s edge is (default `--rule-w`). The place's clear border, the edge's inset and its corner follow it, so the dots' outer edge stays the box's edge and the words stay where they are.
12
+
13
+ ### Fixed
14
+ - **`docs/knobs.md` listed 13 knobs too few** — a knob whose default is a `calc()` with a `var()` inside (`--btn-form-pad-x`, `--card-head-stamp-w`, `--stack-back-size`…) was missed by `npm run knobs`, so the doc and the knob test never saw it. The script now reads one more level of brackets.
15
+
16
+ ## 0.7.81 — 2026-10-10
17
+
18
+ ### Added
19
+ - **`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.
20
+
21
+ ### Fixed
22
+ - **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`.
23
+
5
24
  ## 0.7.80 — 2026-10-10
6
25
 
7
26
  ### Changed
package/README.md CHANGED
@@ -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
@@ -574,9 +579,11 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
574
579
  same list and the same `--sticky-<tone>-bg` knobs as a note's paper). `name` is the big line on the picture's foot,
575
580
  `words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. It fills its column up to
576
581
  `--stamp-max-w`, and every inner length follows its width, so one stamp reads in a tray and in a sheet.
577
- - **`StampPlace`** — an empty dashed place the size of a stamp: a word inside, `onClick` for "make yours", `faint` for a
578
- gap nobody is asked to fill. The edge is a drawn stroke, so its dashes are yours: `--stamp-place-dash` and
579
- `--stamp-place-gap` (defaults 8px and 12px), its corner is `--stamp-place-radius`.
582
+ - **`StampPlace`** — an empty place the size of a stamp, its edge a ring of round dots: a word inside, `onClick` for "make
583
+ yours", `faint` for a gap nobody is asked to fill. The edge is a drawn stroke, so the dots are yours: `--stamp-place-dot`
584
+ is how wide each is (default `--rule-w`, 2px), `--stamp-place-gap` the distance from one dot's centre to the next
585
+ (default 6px), and a `--stamp-place-dash` above 0 stretches each dot into a round-ended dash. Its corner is
586
+ `--stamp-place-radius`. The dots' outer edge is the box's edge whatever their width.
580
587
  - **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
581
588
  the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. `strip` is one line
582
589
  of small stamps that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
@@ -751,7 +758,9 @@ question and hunts for its answer. Rules 11–13 serve that:
751
758
  receipt: items left, prices right; `.card__head`, `.kv`, `.row`); centred
752
759
  only what is not read — dots, a lone number, an icon, a mark. Empty room is
753
760
  not filled with more content; a new edge is made instead (line the buttons
754
- up with the avatar).
761
+ up with the avatar). One exception: a small control with its own words
762
+ (`IconBtnGroup form labelled`) is one centred line — the words name the
763
+ control, they are not content to be read.
755
764
  12. **Show, don't explain.** Where a known picture carries it — an avatar for a
756
765
  person, an icon, a `Ring`/`Track`/`NumberBand`, a `Chip` for a category — use it
757
766
  instead of words. Never add a label, a caption line, a comparison or a
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/post.css CHANGED
@@ -230,7 +230,7 @@
230
230
  .post-desk__line > span { font: var(--type-caption); color: var(--_muted); }
231
231
  .post-desk__line > input { width: 100%; min-width: 0; min-height: var(--tap); border: 0; outline: none; background: transparent; color: inherit; font: var(--type-title); }
232
232
  .post-desk__line > input::placeholder { color: var(--_muted); }
233
- /* the stamp box: empty it is a dashed place to press; pressed, the chosen stamp lies in it */
233
+ /* the stamp box: empty it is a place of dots to press; pressed, the chosen stamp lies in it */
234
234
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
235
235
  @media (prefers-reduced-motion: no-preference) {
236
236
  .post-desk__slot > .stamp { animation: post-desk-stick var(--motion-quick) ease-out both; }
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) */
package/css/stamp.css CHANGED
@@ -91,28 +91,32 @@
91
91
 
92
92
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
93
93
  <span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
94
- The dashed edge is a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose, a stroke's are two
95
- knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
96
- of the same width so what is inside sits where it did; the svg lies on that border, over it, and takes no part in the layout. */
94
+ The edge is a row of round dots drawn on a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose,
95
+ a stroke's are knobs and its corners still follow --stamp-place-radius. A dot is a zero-length dash under a round cap, --stamp-place-dot
96
+ wide; --stamp-place-gap is the distance from one dot's centre to the next, and a --stamp-place-dash above 0 makes round-ended dashes
97
+ of it. The pattern closes wherever the perimeter ends, so the last gap is shorter or longer than the rest by less than one pitch.
98
+ The element keeps a clear border of the same width so what is inside sits where it did; the svg lies on that border, over it, and
99
+ takes no part in the layout. */
97
100
  .stamp-place {
98
101
  position: relative;
99
102
  display: grid;
100
103
  place-content: center;
101
104
  justify-items: center;
102
105
  padding: var(--sp-1);
103
- border: var(--rule-w) solid transparent;
106
+ border: var(--_dot) solid transparent;
104
107
  border-radius: var(--stamp-place-radius, var(--sp-2));
105
108
  background: none;
106
109
  color: var(--stamp-place-ink, var(--on-surface-muted));
107
110
  font: var(--stamp-place-font, var(--type-caption));
108
111
  text-align: center;
109
112
  --_edge: var(--stamp-place-border, var(--on-surface-muted));
113
+ --_dot: var(--stamp-place-dot, var(--rule-w));
110
114
  }
111
115
  .stamp-place__edge {
112
116
  position: absolute;
113
- inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
114
- width: calc(100% + var(--rule-w));
115
- height: calc(100% + var(--rule-w));
117
+ inset: calc(var(--_dot) / -2) auto auto calc(var(--_dot) / -2);
118
+ width: calc(100% + var(--_dot));
119
+ height: calc(100% + var(--_dot));
116
120
  max-width: none; /* base.css caps every svg at its box */
117
121
  overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
118
122
  pointer-events: none;
@@ -120,11 +124,12 @@
120
124
  .stamp-place__edge > rect {
121
125
  width: 100%;
122
126
  height: 100%;
123
- rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--rule-w) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
127
+ rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--_dot) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
124
128
  fill: none;
125
129
  stroke: var(--_edge);
126
- stroke-width: var(--rule-w);
127
- stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
130
+ stroke-width: var(--_dot);
131
+ stroke-linecap: round;
132
+ stroke-dasharray: var(--stamp-place-dash, 0) var(--stamp-place-gap, calc(var(--sp-0) * 3));
128
133
  }
129
134
  .card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
130
135
  .stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
@@ -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
@@ -1435,12 +1435,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1435
1435
  into it — an opacity fade in place, never a slide in from the left (the
1436
1436
  slide read as a second thing moving while the sheet itself moved; Lh,
1437
1437
  2026-09-11). Only the sheet travels; the button appears where it is. */
1438
- .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 {
1439
1440
  position: relative;
1440
1441
  padding-left: 0;
1441
1442
  transition: padding-left var(--motion-sheet) ease;
1442
1443
  }
1443
- .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 {
1444
1445
  position: absolute;
1445
1446
  left: 0;
1446
1447
  top: 50%;
@@ -1455,7 +1456,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1455
1456
  min-height: 0;
1456
1457
  padding: 0;
1457
1458
  }
1458
- .sheet-stack--tap > .card.is-open .sheet-stack__back {
1459
+ .sheet-stack--tap > .card.is-open > .card__head > .sheet-stack__back {
1459
1460
  opacity: 1;
1460
1461
  visibility: visible;
1461
1462
  }
@@ -1578,7 +1579,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1578
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
1579
1580
  while open; the last tab then sits on the inset (no reach into the padding) */
1580
1581
  .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
1581
- .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; }
1582
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))); }
1583
1584
  .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1584
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) */
@@ -2542,6 +2543,17 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2542
2543
  .icon-btn-group--vertical { flex-direction: column; flex: none; width: var(--tap); }
2543
2544
  .icon-btn-group--vertical > .icon-btn { flex: 1 0 auto; min-height: var(--tap); }
2544
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; }
2545
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) */
2546
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 */
2547
2559
 
@@ -4068,28 +4080,32 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4068
4080
 
4069
4081
  /* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
4070
4082
  <span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
4071
- The dashed edge is a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose, a stroke's are two
4072
- knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
4073
- of the same width so what is inside sits where it did; the svg lies on that border, over it, and takes no part in the layout. */
4083
+ The edge is a row of round dots drawn on a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose,
4084
+ a stroke's are knobs and its corners still follow --stamp-place-radius. A dot is a zero-length dash under a round cap, --stamp-place-dot
4085
+ wide; --stamp-place-gap is the distance from one dot's centre to the next, and a --stamp-place-dash above 0 makes round-ended dashes
4086
+ of it. The pattern closes wherever the perimeter ends, so the last gap is shorter or longer than the rest by less than one pitch.
4087
+ The element keeps a clear border of the same width so what is inside sits where it did; the svg lies on that border, over it, and
4088
+ takes no part in the layout. */
4074
4089
  .stamp-place {
4075
4090
  position: relative;
4076
4091
  display: grid;
4077
4092
  place-content: center;
4078
4093
  justify-items: center;
4079
4094
  padding: var(--sp-1);
4080
- border: var(--rule-w) solid transparent;
4095
+ border: var(--_dot) solid transparent;
4081
4096
  border-radius: var(--stamp-place-radius, var(--sp-2));
4082
4097
  background: none;
4083
4098
  color: var(--stamp-place-ink, var(--on-surface-muted));
4084
4099
  font: var(--stamp-place-font, var(--type-caption));
4085
4100
  text-align: center;
4086
4101
  --_edge: var(--stamp-place-border, var(--on-surface-muted));
4102
+ --_dot: var(--stamp-place-dot, var(--rule-w));
4087
4103
  }
4088
4104
  .stamp-place__edge {
4089
4105
  position: absolute;
4090
- inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
4091
- width: calc(100% + var(--rule-w));
4092
- height: calc(100% + var(--rule-w));
4106
+ inset: calc(var(--_dot) / -2) auto auto calc(var(--_dot) / -2);
4107
+ width: calc(100% + var(--_dot));
4108
+ height: calc(100% + var(--_dot));
4093
4109
  max-width: none; /* base.css caps every svg at its box */
4094
4110
  overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
4095
4111
  pointer-events: none;
@@ -4097,11 +4113,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4097
4113
  .stamp-place__edge > rect {
4098
4114
  width: 100%;
4099
4115
  height: 100%;
4100
- rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--rule-w) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
4116
+ rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--_dot) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
4101
4117
  fill: none;
4102
4118
  stroke: var(--_edge);
4103
- stroke-width: var(--rule-w);
4104
- stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
4119
+ stroke-width: var(--_dot);
4120
+ stroke-linecap: round;
4121
+ stroke-dasharray: var(--stamp-place-dash, 0) var(--stamp-place-gap, calc(var(--sp-0) * 3));
4105
4122
  }
4106
4123
  .card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
4107
4124
  .stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
@@ -4422,7 +4439,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
4422
4439
  .post-desk__line > span { font: var(--type-caption); color: var(--_muted); }
4423
4440
  .post-desk__line > input { width: 100%; min-width: 0; min-height: var(--tap); border: 0; outline: none; background: transparent; color: inherit; font: var(--type-title); }
4424
4441
  .post-desk__line > input::placeholder { color: var(--_muted); }
4425
- /* the stamp box: empty it is a dashed place to press; pressed, the chosen stamp lies in it */
4442
+ /* the stamp box: empty it is a place of dots to press; pressed, the chosen stamp lies in it */
4426
4443
  .post-desk__slot { border: 0; background: transparent; color: inherit; }
4427
4444
  @media (prefers-reduced-motion: no-preference) {
4428
4445
  .post-desk__slot > .stamp { animation: post-desk-stick var(--motion-quick) ease-out both; }
@@ -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