@usableapp/cardds 0.7.106 → 0.7.108

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/bin/cardds.mjs +5 -3
  3. package/cardds.css +1 -0
  4. package/css/actions.css +9 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +19 -186
  7. package/css/choice.css +26 -1
  8. package/css/finish.css +4 -4
  9. package/css/journey.css +7 -3
  10. package/css/layover.css +4 -14
  11. package/css/lists.css +6 -18
  12. package/css/media.css +1 -7
  13. package/css/palette.css +1 -1
  14. package/css/parts.css +380 -0
  15. package/css/sheet.css +12 -139
  16. package/css/stack.css +15 -26
  17. package/css/stamp.css +1 -1
  18. package/css/step.css +3 -3
  19. package/css/theme.template.css +1 -1
  20. package/css/tokens.css +4 -4
  21. package/dist/actions/BarNote.d.ts +2 -2
  22. package/dist/actions/BarNote.js +2 -2
  23. package/dist/actions/Btn.d.ts +2 -2
  24. package/dist/actions/Btn.js +1 -1
  25. package/dist/actions/BtnRow.d.ts +1 -1
  26. package/dist/actions/BtnRow.js +1 -1
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/cardds.css +500 -426
  30. package/dist/cards/Card.d.ts +6 -22
  31. package/dist/cards/Card.js +11 -22
  32. package/dist/cards/CardFeed.d.ts +1 -1
  33. package/dist/cards/CardFeed.js +5 -5
  34. package/dist/cards/CardParts.d.ts +2 -13
  35. package/dist/cards/CardParts.js +2 -13
  36. package/dist/cards/cardGo.js +1 -1
  37. package/dist/cards/cardGrounds.js +2 -2
  38. package/dist/catalog.json +1 -1
  39. package/dist/choice/Calendar.d.ts +12 -3
  40. package/dist/choice/Calendar.js +17 -5
  41. package/dist/choice/CalendarWeeks.d.ts +10 -2
  42. package/dist/choice/CalendarWeeks.js +18 -4
  43. package/dist/choice/HourGrid.d.ts +36 -0
  44. package/dist/choice/HourGrid.js +21 -0
  45. package/dist/choice/calendarDay.d.ts +7 -2
  46. package/dist/choice/calendarDay.js +6 -3
  47. package/dist/choice/span.d.ts +34 -0
  48. package/dist/choice/span.js +80 -0
  49. package/dist/forms/Field.d.ts +1 -1
  50. package/dist/forms/Field.js +1 -1
  51. package/dist/index.d.ts +8 -5
  52. package/dist/index.js +7 -5
  53. package/dist/journey/JourneyBadge.d.ts +2 -1
  54. package/dist/lists/Kv.d.ts +4 -2
  55. package/dist/lists/Kv.js +3 -3
  56. package/dist/lists/Row.d.ts +2 -28
  57. package/dist/lists/Row.js +2 -17
  58. package/dist/media/MapArea.d.ts +2 -2
  59. package/dist/media/MapArea.js +1 -1
  60. package/dist/media/Tile.d.ts +3 -7
  61. package/dist/media/Tile.js +5 -5
  62. package/dist/parts/Body.d.ts +7 -0
  63. package/dist/parts/Body.js +11 -0
  64. package/dist/parts/End.d.ts +10 -0
  65. package/dist/parts/End.js +7 -0
  66. package/dist/parts/Foot.d.ts +17 -0
  67. package/dist/parts/Foot.js +11 -0
  68. package/dist/parts/Head.d.ts +15 -0
  69. package/dist/parts/Head.js +34 -0
  70. package/dist/parts/Lead.d.ts +10 -0
  71. package/dist/parts/Lead.js +9 -0
  72. package/dist/parts/Words.d.ts +13 -0
  73. package/dist/parts/Words.js +20 -0
  74. package/dist/parts/headCtx.d.ts +7 -0
  75. package/dist/parts/headCtx.js +9 -0
  76. package/dist/scaffold/EmptyState.d.ts +1 -1
  77. package/dist/scaffold/EmptyState.js +1 -1
  78. package/dist/scaffold/Screen.js +1 -1
  79. package/dist/sheets/Modal.d.ts +3 -7
  80. package/dist/sheets/Modal.js +3 -7
  81. package/dist/sheets/Sheet.d.ts +2 -2
  82. package/dist/sheets/Sheet.js +8 -9
  83. package/dist/sheets/SheetStack.d.ts +3 -3
  84. package/dist/sheets/SheetStack.js +4 -4
  85. package/dist/sheets/SheetSteps.d.ts +1 -1
  86. package/dist/sheets/SheetSteps.js +2 -2
  87. package/dist/sheets/StackBack.d.ts +1 -1
  88. package/dist/sheets/StackBack.js +1 -1
  89. package/dist/sheets/StackSheet.js +7 -7
  90. package/dist/sheets/sheetMeasure.js +3 -3
  91. package/dist/sheets/sheetStepsPile.js +2 -2
  92. package/dist/sheets/stackCard.js +6 -6
  93. package/dist/templates/CallSheet.js +6 -5
  94. package/dist/templates/Walkthrough.js +3 -2
  95. package/docs/reference.md +76 -37
  96. package/lib/catalog.mjs +1 -1
  97. package/lib/mcp.mjs +1 -1
  98. package/lint/rules.mjs +14 -7
  99. package/package.json +2 -1
  100. package/scripts/codemod-parts.mjs +403 -0
  101. package/dist/cards/CardFoot.d.ts +0 -15
  102. package/dist/cards/CardFoot.js +0 -9
  103. package/dist/cards/CardHead.d.ts +0 -21
  104. package/dist/cards/CardHead.js +0 -39
  105. package/dist/cards/Stat.d.ts +0 -19
  106. package/dist/cards/Stat.js +0 -14
  107. package/dist/sheets/SheetBody.d.ts +0 -5
  108. package/dist/sheets/SheetBody.js +0 -9
  109. package/dist/sheets/SheetHead.d.ts +0 -15
  110. package/dist/sheets/SheetHead.js +0 -6
@@ -4,7 +4,7 @@ import { Icon } from '../type/Icon.js';
4
4
  /**
5
5
  * MapArea — the box a map engine draws in: the first child is the engine's mount and fills it; before tiles arrive (or
6
6
  * offline) the box shows the wireframe street pattern, never a hole. Two places: behind a stage sheet (the default), or
7
- * `inline`, in flow inside a card or a sheet's body — as a SheetBody's only child it fills the body, so the open sheet
7
+ * `inline`, in flow inside a card or a sheet's body — as a sheet's Body's only child it fills the body, so the open sheet
8
8
  * becomes the map while its head stays. The ring marks the rough area: behind a stage it is centred in the band a half
9
9
  * sheet leaves open, inline at the centre of its own box. Dragging the map underneath is that choice, never a pin to place;
10
10
  * `ringRef` is how the engine learns where that is. Places to pick go after the mount, as `MapPick`s.
@@ -13,9 +13,7 @@ export interface TileProps extends Omit<ComponentProps<'div'>, 'onChange'> {
13
13
  icon?: IconName;
14
14
  /** the label at the bottom */
15
15
  label?: ReactNode;
16
- /** a big value (stat tiles) */
17
- value?: ReactNode;
18
- /** a caption under the label or value */
16
+ /** a caption under the label */
19
17
  sub?: ReactNode;
20
18
  /** a pick tile: a checkbox (or radio) inside, filled when checked */
21
19
  pick?: boolean;
@@ -31,8 +29,6 @@ export interface TileProps extends Omit<ComponentProps<'div'>, 'onChange'> {
31
29
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
32
30
  /** filled accent (a static "on" tile) */
33
31
  on?: boolean;
34
- /** icon beside a value + caption, shorter */
35
- stat?: boolean;
36
32
  /** dashed, centred: an upload slot */
37
33
  upload?: boolean;
38
34
  /** spans the whole grid */
@@ -42,8 +38,8 @@ export interface TileProps extends Omit<ComponentProps<'div'>, 'onChange'> {
42
38
  /** div (default), button for a tap tile — pick tiles render as a label */
43
39
  as?: 'div' | 'button';
44
40
  }
45
- /** Tile — a big square: icon top, label bottom; `pick` toggles, `stat` shows a value, `upload` is the dashed slot. A `pick` or `upload` tile is filled in: On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). */
46
- export declare function Tile({ icon, label, value, sub, pick, type, name, checked, defaultChecked, onChange, on, stat, upload, wide, tone, as, className, children, ...rest }: TileProps): import("react").DetailedReactHTMLElement<{
41
+ /** Tile — a big square: icon top, label bottom; `pick` toggles, `upload` is the dashed slot. A `pick` or `upload` tile is filled in: On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A number with its icon and label in a cell is a `KvCell` (Lh 2026-10-20). */
42
+ export declare function Tile({ icon, label, sub, pick, type, name, checked, defaultChecked, onChange, on, upload, wide, tone, as, className, children, ...rest }: TileProps): import("react").DetailedReactHTMLElement<{
47
43
  translate?: "yes" | "no" | undefined | undefined;
48
44
  slot?: string | undefined | undefined;
49
45
  style?: import("react").CSSProperties | undefined;
@@ -6,11 +6,11 @@ import { Icon } from '../type/Icon.js';
6
6
  export function TileGrid({ cols3, bleed, className, ...rest }) {
7
7
  return _jsx("div", { className: cx('tile-grid', cols3 && 'tile-grid--3', bleed && 'bleed', className), ...rest });
8
8
  }
9
- /** Tile — a big square: icon top, label bottom; `pick` toggles, `stat` shows a value, `upload` is the dashed slot. A `pick` or `upload` tile is filled in: On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). */
10
- export function Tile({ icon, label, value, sub, pick, type = 'checkbox', name, checked, defaultChecked, onChange, on, stat, upload, wide, tone, as, className, children, ...rest }) {
9
+ /** Tile — a big square: icon top, label bottom; `pick` toggles, `upload` is the dashed slot. A `pick` or `upload` tile is filled in: On a sheet, never in a Card: the sheet is the paper the member fills in (Lh 2026-10-18). A number with its icon and label in a cell is a `KvCell` (Lh 2026-10-20). */
10
+ export function Tile({ icon, label, sub, pick, type = 'checkbox', name, checked, defaultChecked, onChange, on, upload, wide, tone, as, className, children, ...rest }) {
11
11
  const el = pick ? 'label' : as ?? 'div';
12
- const text = value != null || (label != null && sub != null)
13
- ? _jsxs("span", { className: "tile__text", children: [value != null && _jsx("span", { className: "tile__value", children: value }), label != null && _jsx("span", { className: "tile__label", children: label }), sub != null && _jsx("span", { className: "tile__sub", children: sub })] })
12
+ const text = label != null && sub != null
13
+ ? _jsxs("span", { className: "tile__text", children: [_jsx("span", { className: "tile__label", children: label }), _jsx("span", { className: "tile__sub", children: sub })] })
14
14
  : label != null && _jsx("span", { className: "tile__label", children: label });
15
- return createElement(el, { type: el === 'button' ? 'button' : undefined, className: cx('tile', pick && 'tile--pick', on && 'tile--on', stat && 'tile--stat', upload && 'tile--upload', wide && 'tile--wide', tone && `tile--${tone}`, className), ...rest }, pick && _jsx("input", { type: type, name: name, checked: checked, defaultChecked: defaultChecked, onChange: onChange }), icon && _jsx(Icon, { name: icon }), text, children);
15
+ return createElement(el, { type: el === 'button' ? 'button' : undefined, className: cx('tile', pick && 'tile--pick', on && 'tile--on', upload && 'tile--upload', wide && 'tile--wide', tone && `tile--${tone}`, className), ...rest }, pick && _jsx("input", { type: type, name: name, checked: checked, defaultChecked: defaultChecked, onChange: onChange }), icon && _jsx(Icon, { name: icon }), text, children);
16
16
  }
@@ -0,0 +1,7 @@
1
+ import { type ComponentProps } from 'react';
2
+ export interface BodyProps extends ComponentProps<'div'> {
3
+ }
4
+ /** Body — the content row, under the `Head` and over the `Foot`: what the member reads or fills in. In a `Sheet` and a stack sheet it is the one thing that
5
+ * scrolls, and its scroll padding keeps a focused control above the cut; content never goes straight into the Sheet. In a `Modal` it is the light part the
6
+ * question is about, scrolling inside when tall; a Modal with no Body is the confirm. Knobs `--body-gap`, `--body-pad-top`. */
7
+ export declare function Body({ className, ...rest }: BodyProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useContext } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { StackPullCtx } from '../sheets/stackCard.js';
5
+ /** Body — the content row, under the `Head` and over the `Foot`: what the member reads or fills in. In a `Sheet` and a stack sheet it is the one thing that
6
+ * scrolls, and its scroll padding keeps a focused control above the cut; content never goes straight into the Sheet. In a `Modal` it is the light part the
7
+ * question is about, scrolling inside when tall; a Modal with no Body is the confirm. Knobs `--body-gap`, `--body-pad-top`. */
8
+ export function Body({ className, ...rest }) {
9
+ const pulling = useContext(StackPullCtx);
10
+ return _jsx("div", { className: cx('body', pulling && 'is-refreshing', className), ...rest });
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface EndProps extends ComponentProps<'span'> {
3
+ /** the trailing value, right-aligned (title type) */
4
+ value?: ReactNode;
5
+ /** the muted caption under `value` */
6
+ caption?: ReactNode;
7
+ }
8
+ /** End — the value at the end of a line over its caption, right-aligned (+1.0 h over Tue); or its children.
9
+ * A control at the end (an IconBtn, a Chip, a Check) takes no End: it goes straight in the line after the Words. */
10
+ export declare function End({ value, caption, className, children, ...rest }: EndProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** End — the value at the end of a line over its caption, right-aligned (+1.0 h over Tue); or its children.
4
+ * A control at the end (an IconBtn, a Chip, a Check) takes no End: it goes straight in the line after the Words. */
5
+ export function End({ value, caption, className, children, ...rest }) {
6
+ return (_jsxs("span", { className: cx('end', className), ...rest, children: [value != null && _jsx("span", { className: "t-title", children: value }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children] }));
7
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface FootProps extends ComponentProps<'div'> {
3
+ /** a SMALL LINK under the buttons, centred on its own short row (a `Link sm` — "details for developers") — the same as an
4
+ * ActionBar's ctaLink (Lh 2026-09-23). It never takes a share of the buttons' line */
5
+ link?: ReactNode;
6
+ /** a foot of a few ROUND buttons (`IconBtn`, a size `lg` / `xl` for the big ones): they keep their own size instead of sharing the line, and
7
+ * stand as ONE group in the middle of the foot, a fixed gap apart (wider than the Btns' — `--foot-gap` sets it). The Btn foot is
8
+ * unchanged: this is the other foot, for a few big round answers */
9
+ round?: boolean;
10
+ }
11
+ /** Foot — the last row of a card, a sheet or a stack sheet: what the member can do next, its buttons on one line. One takes the width, up to
12
+ * four share it evenly. Btns only in the row; a small link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words,
13
+ * centred — for a foot whose one button should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one
14
+ * `primary` with a word, the others with icons): one centred group, a fixed gap between them. In a `Sheet` it stands on the box's bottom edge
15
+ * at every state. A `Card` finds its `Foot` by type (a turning card draws it on both faces), so a consumer's own component that wraps a `Foot`
16
+ * is not found. */
17
+ export declare function Foot({ link, round, className, children, ...rest }: FootProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /** Foot — the last row of a card, a sheet or a stack sheet: what the member can do next, its buttons on one line. One takes the width, up to
4
+ * four share it evenly. Btns only in the row; a small link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words,
5
+ * centred — for a foot whose one button should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one
6
+ * `primary` with a word, the others with icons): one centred group, a fixed gap between them. In a `Sheet` it stands on the box's bottom edge
7
+ * at every state. A `Card` finds its `Foot` by type (a turning card draws it on both faces), so a consumer's own component that wraps a `Foot`
8
+ * is not found. */
9
+ export function Foot({ link, round, className, children, ...rest }) {
10
+ return (_jsxs("div", { className: cx('foot', link != null && 'foot--link', round && 'foot--round', className), ...rest, children: [children, link != null && _jsx("div", { className: "foot__link", children: link })] }));
11
+ }
@@ -0,0 +1,15 @@
1
+ import { type ComponentProps } from 'react';
2
+ export interface HeadProps extends ComponentProps<'div'> {
3
+ /** top-align a genuinely tall multi-line title with its control */
4
+ top?: boolean;
5
+ /** a stage sheet's head: the title and its meta centred between a round button either side (`lead` · `Words` · `trail`), the three columns kept when one is missing */
6
+ centre?: boolean;
7
+ /** the sheet measured this title as wider than its line, so the title runs sideways */
8
+ long?: boolean;
9
+ }
10
+ /**
11
+ * Head — the first row of a card, a sheet or a stack sheet: what this is, and the one thing beside it. Its children are, in order, an optional
12
+ * lead (a `Lead`, a `StackBack`, a `JourneyBadge`), the `Words` (the title takes the room, its meta under it), chips, and an end (an `IconBtn`, a `Chip`, an `End`).
13
+ * Never put buttons here that finish the card: they go in the foot. A `Card` finds its `Head` by type, so a consumer's own component that wraps a `Head` is not found.
14
+ */
15
+ export declare function Head({ top, centre, long, className, children, ...rest }: HeadProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement, useContext } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { Text } from '../type/Text.js';
5
+ import { StackHeadCtx } from '../sheets/stackCard.js';
6
+ import { HeadCtx, onTitleKey } from './headCtx.js';
7
+ /** a title written by hand (a `Text` h1/h2/display, a bare h1-h3) is the stack's keyboard way in as a `Words` title is. The head renders the tab stop and, when
8
+ * the sheet measured it long, `.is-long`. Nothing sets either on the node afterwards */
9
+ function markTitle(node, tab, long) {
10
+ if (!isValidElement(node))
11
+ return node;
12
+ const el = node;
13
+ const level = el.type === Text ? el.props.level : null;
14
+ const intrinsic = typeof el.type === 'string' && /^h[1-3]$/.test(el.type);
15
+ if (level !== 'h1' && level !== 'h2' && level !== 'display' && !intrinsic)
16
+ return node;
17
+ return cloneElement(el, {
18
+ ...(long ? { className: cx(el.props.className, 'is-long') } : {}),
19
+ ...(tab != null ? {
20
+ tabIndex: tab,
21
+ onKeyDown: (e) => { el.props.onKeyDown?.(e); if (!e.defaultPrevented)
22
+ onTitleKey(e); },
23
+ } : {}),
24
+ });
25
+ }
26
+ /**
27
+ * Head — the first row of a card, a sheet or a stack sheet: what this is, and the one thing beside it. Its children are, in order, an optional
28
+ * lead (a `Lead`, a `StackBack`, a `JourneyBadge`), the `Words` (the title takes the room, its meta under it), chips, and an end (an `IconBtn`, a `Chip`, an `End`).
29
+ * Never put buttons here that finish the card: they go in the foot. A `Card` finds its `Head` by type, so a consumer's own component that wraps a `Head` is not found.
30
+ */
31
+ export function Head({ top, centre, long, className, children, ...rest }) {
32
+ const tab = useContext(StackHeadCtx);
33
+ return (_jsx("div", { className: cx('head', top && 'head--top', centre && 'head--centre', className), ...rest, children: _jsx(HeadCtx.Provider, { value: { tab, long: !!long }, children: tab == null && !long ? children : Children.map(children, (c) => markTitle(c, tab, !!long)) }) }));
34
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { type IconName } from '../type/Icon.js';
3
+ export interface LeadProps extends ComponentProps<'span'> {
4
+ /** an icon drawn in the box; without it, the box holds its children (a number, an initial) */
5
+ icon?: IconName;
6
+ }
7
+ /** Lead — the thing at the start of a line: the outlined box holding an `icon` or its children. In a `Row` it is the 48 box of a menu row;
8
+ * in a `Card row` (and a FeedCard's head) the 44 box in the card's own ink, a number or an initial in label type (`.card--row > .lead`, css/parts.css).
9
+ * A person's or a group's lead takes no box: an Avatar or an AvatarStack goes straight in the line as its first child. */
10
+ export declare function Lead({ icon, className, children, ...rest }: LeadProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ import { Icon } from '../type/Icon.js';
4
+ /** Lead — the thing at the start of a line: the outlined box holding an `icon` or its children. In a `Row` it is the 48 box of a menu row;
5
+ * in a `Card row` (and a FeedCard's head) the 44 box in the card's own ink, a number or an initial in label type (`.card--row > .lead`, css/parts.css).
6
+ * A person's or a group's lead takes no box: an Avatar or an AvatarStack goes straight in the line as its first child. */
7
+ export function Lead({ icon, className, children, ...rest }) {
8
+ return _jsx("span", { className: cx('lead', className), ...rest, children: icon ? _jsx(Icon, { name: icon }) : children });
9
+ }
@@ -0,0 +1,13 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export interface WordsProps extends Omit<ComponentProps<'span'>, 'title'> {
3
+ /** the title line */
4
+ title?: ReactNode;
5
+ /** the caption under it (may hold a Dot) */
6
+ meta?: ReactNode;
7
+ /** the title as a heading element in that type level; without it the title is a span in title type. In a `Head` the title is always a heading: `h2` unless this says otherwise (`h1` for the one-feature page) */
8
+ level?: 'h1' | 'h2' | 'display';
9
+ }
10
+ /** Words — the words of a line, filling the room between the lead and the end: a `title` over a `meta` caption;
11
+ * its children come under them, or instead of them (a Wave for a voice note). In a `Head` there is no box round them: the title and the meta
12
+ * stand in the head, and a `className`, an `id` or any other attribute goes on the title. */
13
+ export declare function Words({ title, meta, level, className, children, ...rest }: WordsProps): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { HeadCtx, onTitleKey } from './headCtx.js';
5
+ /** Words — the words of a line, filling the room between the lead and the end: a `title` over a `meta` caption;
6
+ * its children come under them, or instead of them (a Wave for a voice note). In a `Head` there is no box round them: the title and the meta
7
+ * stand in the head, and a `className`, an `id` or any other attribute goes on the title. */
8
+ export function Words({ title, meta, level, className, children, ...rest }) {
9
+ const head = useContext(HeadCtx);
10
+ if (head) {
11
+ const as = level ?? 'h2';
12
+ const Title = as === 'h1' ? 'h1' : 'h2';
13
+ const { onKeyDown, ...own } = rest;
14
+ const attrs = head.tab != null ? { tabIndex: head.tab, onKeyDown: (e) => { onKeyDown?.(e); if (!e.defaultPrevented)
15
+ onTitleKey(e); } } : { onKeyDown };
16
+ return (_jsxs(_Fragment, { children: [title != null && _jsx(Title, { className: cx('words__title', `t-${as}`, head.long && 'is-long', className), ...own, ...attrs, children: title }), meta != null && _jsx("span", { className: "words__meta t-caption t-muted", children: meta }), children] }));
17
+ }
18
+ const Title = level === 'h1' ? 'h1' : level ? 'h2' : 'span';
19
+ return (_jsxs("span", { className: cx('words', className), ...rest, children: [title != null && _jsx(Title, { className: cx('words__title', level && `t-${level}`), children: title }), meta != null && _jsx("span", { className: "words__meta", children: meta }), children] }));
20
+ }
@@ -0,0 +1,7 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ /** what a `Words` in a head reads: the stack's tab stop and whether the title runs. `null` outside a head */
3
+ export declare const HeadCtx: import("react").Context<{
4
+ tab: 0 | -1 | null;
5
+ long: boolean;
6
+ } | null>;
7
+ export declare const onTitleKey: (e: KeyboardEvent<HTMLElement>) => void;
@@ -0,0 +1,9 @@
1
+ import { createContext } from 'react';
2
+ /** what a `Words` in a head reads: the stack's tab stop and whether the title runs. `null` outside a head */
3
+ export const HeadCtx = createContext(null);
4
+ export const onTitleKey = (e) => {
5
+ if (e.key !== 'Enter' && e.key !== ' ')
6
+ return;
7
+ e.preventDefault();
8
+ e.currentTarget.click();
9
+ };
@@ -11,5 +11,5 @@ export interface EmptyStateProps extends Omit<ComponentProps<'div'>, 'title'> {
11
11
  }
12
12
  /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
13
13
  * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
14
- * Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
14
+ * Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a Sheet's `Body` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
15
15
  export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. A component in the content: alone in a `Step` it stands at the content's human centre, 40% down (Lh 2026-09-23 — no longer a `Centre` over the screen)
4
4
  * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16).
5
- * Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a `SheetBody` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
5
+ * Knob `--empty-state-at`, a number 0–1: the share of the holder's free room that stands above it. In a `Step` the default is 0.4 (two parts above, three below); alone in a Sheet's `Body` it is 0 (at the top). A theme's `:root { --empty-state-at: 0.2 }` lifts it everywhere, or set it on a `Screen`, `Step` or `Sheet` for the ones inside. */
6
6
  export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
7
7
  const H = level;
8
8
  return (_jsxs("div", { className: cx('empty-state', className), ...rest, children: [icon, title != null && _jsx(H, { className: "t-h2", children: title }), typeof children === 'string' ? _jsx("p", { className: "t-body", children: children }) : children, action != null && _jsx("div", { className: "empty-state__action", children: action })] }));
@@ -16,7 +16,7 @@ export function Screen({ fill, palette, as = 'div', top, bottom, ready = true, c
16
16
  const el = own.current;
17
17
  if (!el || !el.querySelector(':scope > .action-bar') || !el.querySelector(':scope > .step > .sheet'))
18
18
  return;
19
- warnOnce('screen-sheet-bar', 'cardds Screen: a bottom bar on a screen with a Sheet — the sheet finishes on its own foot (CardFoot); drop the bar.');
19
+ warnOnce('screen-sheet-bar', 'cardds Screen: a bottom bar on a screen with a Sheet — the sheet finishes on its own foot (Foot); drop the bar.');
20
20
  });
21
21
  /* the BOTTOM layer's height, published on the screen as --bottom-h (Lh 2026-09-23): the walk's pages end with that much room
22
22
  more, so every last word scrolls up clear of the layer. Measured, because the layer is as tall as its rows and the text size. */
@@ -10,9 +10,9 @@ export interface ModalProps extends ComponentProps<'article'> {
10
10
  onDismiss?: (reason: 'scrim' | 'escape') => void;
11
11
  }
12
12
  /**
13
- * Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `ModalBody` (the thing being asked about, light, scrolls
13
+ * Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `Body` (the thing being asked about, light, scrolls
14
14
  * when tall) and a `ModalAsk` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the ModalAsk comes
15
- * out below while the ModalBody moves up.
15
+ * out below while the Body moves up.
16
16
  * A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
17
17
  * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
18
18
  * the scrim, Esc). When it has arrived, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the question — write no focus code.
@@ -21,10 +21,6 @@ export interface ModalProps extends ComponentProps<'article'> {
21
21
  * effect's first run sets `shown` to false, its cleanup drops the wait, and the second run returns early.
22
22
  */
23
23
  export declare function Modal({ open, onClosed, onDismiss, className, ...rest }: ModalProps): import("react").JSX.Element;
24
- export interface ModalBodyProps extends ComponentProps<'div'> {
25
- }
26
- /** ModalBody — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
27
- export declare function ModalBody({ className, ...rest }: ModalBodyProps): import("react").JSX.Element;
28
24
  export interface ModalAskProps extends Omit<ComponentProps<'div'>, 'title'> {
29
25
  /** the question */
30
26
  title: ReactNode;
@@ -34,7 +30,7 @@ export interface ModalAskProps extends Omit<ComponentProps<'div'>, 'title'> {
34
30
  light?: boolean;
35
31
  }
36
32
  /** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and the answers: a `BtnTrio` of round icon buttons, or a `BtnRow` of word `Btn`s (it fills the card, each button's words on one line; a `Btn quiet` there is edged in the ask's own ink) — plain buttons
37
- * with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `ModalBody`) is the CONFIRM: a question with nothing
33
+ * with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `Body`) is the CONFIRM: a question with nothing
38
34
  * to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
39
35
  export declare function ModalAsk({ title, caption, light, className, children, ...rest }: ModalAskProps): import("react").JSX.Element;
40
36
  export interface BtnTrioProps extends ComponentProps<'div'> {
@@ -3,9 +3,9 @@ import { Children, Fragment, useEffect, useRef } from 'react';
3
3
  import { focusWhenActive, useLatest } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  /**
6
- * Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `ModalBody` (the thing being asked about, light, scrolls
6
+ * Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `Body` (the thing being asked about, light, scrolls
7
7
  * when tall) and a `ModalAsk` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the ModalAsk comes
8
- * out below while the ModalBody moves up.
8
+ * out below while the Body moves up.
9
9
  * A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
10
10
  * records the answer and sets `open={false}`; the modal's one behaviour is how it leaves. `onDismiss` asks to leave unanswered (a tap on
11
11
  * the scrim, Esc). When it has arrived, cardds puts the cursor in the field marked `data-autofocus`, else the first field, else the question — write no focus code.
@@ -57,12 +57,8 @@ export function Modal({ open = true, onClosed, onDismiss, className, ...rest })
57
57
  }, [has, open]);
58
58
  return _jsx("article", { ref: ref, className: cx('card modal', className), ...rest });
59
59
  }
60
- /** ModalBody — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
61
- export function ModalBody({ className, ...rest }) {
62
- return _jsx("div", { className: cx('modal__lift', className), ...rest });
63
- }
64
60
  /** ModalAsk — the dark end of a `Modal` (`light` = paper and dark words): the question, its fine print, and the answers: a `BtnTrio` of round icon buttons, or a `BtnRow` of word `Btn`s (it fills the card, each button's words on one line; a `Btn quiet` there is edged in the ask's own ink) — plain buttons
65
- * with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `ModalBody`) is the CONFIRM: a question with nothing
61
+ * with the app's `onClick`; the ModalAsk does nothing with them. A `Modal` holding ONLY a ModalAsk (no `Body`) is the CONFIRM: a question with nothing
66
62
  * to show above it — the ModalAsk is the whole card and comes in with it in one beat. */
67
63
  export function ModalAsk({ title, caption, light, className, children, ...rest }) {
68
64
  return (_jsxs("div", { className: cx('modal__drawer', light && 'modal__drawer--light', className), ...rest, children: [_jsx("h2", { className: "t-h2", tabIndex: -1, children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
@@ -33,8 +33,8 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
33
33
  /**
34
34
  * Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
35
35
  * box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
36
- * ONE SHAPE, written top-down: handle · head (a CardHead / SheetHead — pinned, never scrolls) · SheetBody (scrolls, as far as
37
- * it needs) · CardFoot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
36
+ * ONE SHAPE, written top-down: handle · head (a Head — pinned, never scrolls) · Body (scrolls, as far as
37
+ * it needs) · Foot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
38
38
  *
39
39
  * It is a COMPONENT, not a wiring: it knows nothing of what opens it. The app owns `state` and sets it from whatever it
40
40
  * likes — a button, a route, a timer: `<Btn onClick={() => setSheet('full')}>` … `<Sheet state={sheet} onStateChange={setSheet}>`.
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { focusWhenActive, useControlled, visibleBottom, watchSize } from '../state.js';
4
- import { CardHead } from '../cards/CardHead.js';
5
- import { SheetHead } from './SheetHead.js';
4
+ import { Head } from '../parts/Head.js';
6
5
  import { cx } from '../cx.js';
7
6
  import { word } from '../words.js';
8
7
  import { ORDER, slid, stopsOf, useFootRoom, usePeekStrip, useTitleRun } from './sheetMeasure.js';
@@ -13,15 +12,15 @@ const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
13
12
  function markLong(node, long) {
14
13
  if (!long || !isValidElement(node))
15
14
  return node;
16
- if (node.type !== CardHead && node.type !== SheetHead)
15
+ if (node.type !== Head)
17
16
  return node;
18
17
  return cloneElement(node, { long: true });
19
18
  }
20
19
  /**
21
20
  * Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
22
21
  * box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
23
- * ONE SHAPE, written top-down: handle · head (a CardHead / SheetHead — pinned, never scrolls) · SheetBody (scrolls, as far as
24
- * it needs) · CardFoot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
22
+ * ONE SHAPE, written top-down: handle · head (a Head — pinned, never scrolls) · Body (scrolls, as far as
23
+ * it needs) · Foot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
25
24
  *
26
25
  * It is a COMPONENT, not a wiring: it knows nothing of what opens it. The app owns `state` and sets it from whatever it
27
26
  * likes — a button, a route, a timer: `<Btn onClick={() => setSheet('full')}>` … `<Sheet state={sheet} onStateChange={setSheet}>`.
@@ -64,7 +63,7 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
64
63
  was.current = live;
65
64
  if (!rose)
66
65
  return;
67
- const cancel = focusWhenActive(ref.current, { scope: ':scope > .sheet__body', fallback: () => (from === 'away' ? handleRef.current : null) });
66
+ const cancel = focusWhenActive(ref.current, { scope: ':scope > .body', fallback: () => (from === 'away' ? handleRef.current : null) });
68
67
  return () => { if (!cancel())
69
68
  was.current = from; }; // cut short (StrictMode's trial run, or a new state mid-slide): the rise still counts from where it began
70
69
  }, [live]);
@@ -79,11 +78,11 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
79
78
  const from = Math.max(list.indexOf(current), atLeast ? list.indexOf(atLeast) : -1, 0);
80
79
  const rect = el.getBoundingClientRect();
81
80
  const restTop = rect.top - slid(el);
82
- const bodyTop = (el.querySelector(':scope > .sheet__body') ?? el).getBoundingClientRect().top - rect.top;
81
+ const bodyTop = (el.querySelector(':scope > .body') ?? el).getBoundingClientRect().top - rect.top;
83
82
  const box = el.parentElement.getBoundingClientRect(); // what clips it: the Step or the SheetStage
84
83
  const edge = Math.min(box.bottom, restTop + rect.height); // the sheet's own bottom edge at rest — above the box's on a stage that gave the keyboard its room
85
84
  const cut = Math.min(edge, visibleBottom());
86
- const button = el.querySelector(':scope > .card__foot')?.firstElementChild; // the foot's button stands on that edge at every state: the control has to clear its strip too
85
+ const button = el.querySelector(':scope > .foot')?.firstElementChild; // the foot's button stands on that edge at every state: the control has to clear its strip too
87
86
  const need = control.getBoundingClientRect().height + (button ? edge - button.getBoundingClientRect().top : 0);
88
87
  let to = list[list.length - 1];
89
88
  for (let i = from; i < list.length; i++)
@@ -121,7 +120,7 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
121
120
  onFocus?.(e);
122
121
  if (current !== 'peek' || e.target.closest('.sheet__handle'))
123
122
  return;
124
- const body = ref.current?.querySelector(':scope > .sheet__body');
123
+ const body = ref.current?.querySelector(':scope > .body');
125
124
  if (body)
126
125
  body.scrollTop = 0; // the browser's own reveal scrolled it against the peek strip: start over
127
126
  reveal(e.target, 'focus', home.current);
@@ -27,9 +27,9 @@ export interface SheetStackProps extends ComponentProps<'div'> {
27
27
  /**
28
28
  * SheetStack — the MENU: sheets stacked like paper at the middle of the home screen, every one a main section.
29
29
  * Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
30
- * Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a CardHead
31
- * with CHILDREN, in this order: `<StackBack />` (tap mode), `<Text level="h2" caps>Shopping</Text>`, an `<IconRow>` of Icons —
32
- * never the `heading` prop (a stack head is back · h2 · tabs on ONE line — StackSheet builds it). Put it last in a `Screen fill`, it bleeds
30
+ * Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a Head
31
+ * with CHILDREN, in this order: `<StackBack />` (tap mode), the title (`<Words title="Shopping" />`, or a bare `<Text level="h2" caps>`), an `<IconRow>` of Icons —
32
+ * a stack head is back · title · tabs on ONE line (StackSheet builds it). Put it last in a `Screen fill`, it bleeds
33
33
  * off the bottom. `closed`: nothing open on arrival — every head peeks at the bottom and the base above shows `base` (a home screen
34
34
  * with a picture over the menu); the `base` slot is rendered after the sheets, so it never takes a sheet's slot. An open sheet fills everything under the top bar,
35
35
  * or with `rise="flood"` the whole screen, covering the bar. This is the sheet STACK; a single bottom sheet is `Sheet`.
@@ -18,7 +18,7 @@ function measureRests(stack) {
18
18
  return {
19
19
  height: box.height,
20
20
  strips: cards.map((c, i) => {
21
- const head = c.querySelector(':scope > .card__head')?.getBoundingClientRect();
21
+ const head = c.querySelector(':scope > .head')?.getBoundingClientRect();
22
22
  const own = head ? head.bottom - box.top + (head.top - box.top - tops[i]) : tops[i];
23
23
  return { top: tops[i], end: i < cards.length - 1 ? tops[i + 1] : own };
24
24
  }),
@@ -57,9 +57,9 @@ function stamp(child, place) {
57
57
  /**
58
58
  * SheetStack — the MENU: sheets stacked like paper at the middle of the home screen, every one a main section.
59
59
  * Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
60
- * Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a CardHead
61
- * with CHILDREN, in this order: `<StackBack />` (tap mode), `<Text level="h2" caps>Shopping</Text>`, an `<IconRow>` of Icons —
62
- * never the `heading` prop (a stack head is back · h2 · tabs on ONE line — StackSheet builds it). Put it last in a `Screen fill`, it bleeds
60
+ * Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a Head
61
+ * with CHILDREN, in this order: `<StackBack />` (tap mode), the title (`<Words title="Shopping" />`, or a bare `<Text level="h2" caps>`), an `<IconRow>` of Icons —
62
+ * a stack head is back · title · tabs on ONE line (StackSheet builds it). Put it last in a `Screen fill`, it bleeds
63
63
  * off the bottom. `closed`: nothing open on arrival — every head peeks at the bottom and the base above shows `base` (a home screen
64
64
  * with a picture over the menu); the `base` slot is rendered after the sheets, so it never takes a sheet's slot. An open sheet fills everything under the top bar,
65
65
  * or with `rise="flood"` the whole screen, covering the bar. This is the sheet STACK; a single bottom sheet is `Sheet`.
@@ -29,7 +29,7 @@ export interface SheetStepsProps extends ComponentProps<'div'> {
29
29
  * SheetSteps — one task taken in steps INSIDE one sheet (phone · code · PIN · PIN again · face · notices). The done steps pile up
30
30
  * on top, each one line (title · summary · ✓); the current step is open under them, its form at the form's own spacing; the next
31
31
  * steps wait below as their titles. A thin rule over every step. The last step's own action sends, or a `finish` stands under the
32
- * steps on the last one. Goes in a `SheetBody`.
32
+ * steps on the last one. Goes in a Sheet's `Body`.
33
33
  * The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
34
34
  */
35
35
  export declare function SheetSteps({ at, reached, defaultAt, onAtChange, reversible, numbered, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }: SheetStepsProps): import("react").JSX.Element;
@@ -19,7 +19,7 @@ const StepCtx = createContext(null);
19
19
  * SheetSteps — one task taken in steps INSIDE one sheet (phone · code · PIN · PIN again · face · notices). The done steps pile up
20
20
  * on top, each one line (title · summary · ✓); the current step is open under them, its form at the form's own spacing; the next
21
21
  * steps wait below as their titles. A thin rule over every step. The last step's own action sends, or a `finish` stands under the
22
- * steps on the last one. Goes in a `SheetBody`.
22
+ * steps on the last one. Goes in a Sheet's `Body`.
23
23
  * The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
24
24
  */
25
25
  export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible = false, numbered = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
@@ -115,7 +115,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
115
115
  if (!typing(e.target) || performance.now() < moving.current)
116
116
  return; // the old step's field losing the cursor to the new one
117
117
  glideNow();
118
- const body = ref.current?.closest('.sheet__body');
118
+ const body = ref.current?.closest('.body');
119
119
  setTimeout(() => { if (!typing(document.activeElement) && body)
120
120
  body.scrollTo({ top: 0 }); }, 0); // Tab to the next field keeps it up
121
121
  };
@@ -6,6 +6,6 @@ export interface StackBackProps extends ComponentProps<'button'> {
6
6
  * default: at its start, chevron left */
7
7
  end?: boolean;
8
8
  }
9
- /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
9
+ /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's Head. Hidden until the sheet is open.
10
10
  * A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
11
11
  export declare function StackBack({ label, end, className, ...rest }: StackBackProps): import("react").JSX.Element;
@@ -3,7 +3,7 @@ import { cx } from '../cx.js';
3
3
  import { Icon } from '../type/Icon.js';
4
4
  import { IconBtn } from '../actions/IconBtn.js';
5
5
  import { word } from '../words.js';
6
- /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
6
+ /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's Head. Hidden until the sheet is open.
7
7
  * A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
8
8
  export function StackBack({ label = word('back'), end, className, ...rest }) {
9
9
  return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', end && 'sheet-stack__back--end', className), ...rest, children: _jsx(Icon, { name: end ? 'chevron-down' : 'chevron-left' }) });
@@ -4,11 +4,11 @@ import { useControlled, watchSize } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  import { Card } from '../cards/Card.js';
6
6
  import { IconBtn } from '../actions/IconBtn.js';
7
- import { CardHead } from '../cards/CardHead.js';
8
- import { CardFoot } from '../cards/CardFoot.js';
9
- import { SheetBody } from './SheetBody.js';
7
+ import { Head } from '../parts/Head.js';
8
+ import { Words } from '../parts/Words.js';
9
+ import { Foot } from '../parts/Foot.js';
10
+ import { Body } from '../parts/Body.js';
10
11
  import { StackBack } from './StackBack.js';
11
- import { Text } from '../type/Text.js';
12
12
  import { Icon } from '../type/Icon.js';
13
13
  import { Badge } from '../numbers/Badge.js';
14
14
  import { word } from '../words.js';
@@ -24,15 +24,15 @@ export function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defau
24
24
  const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
25
25
  // a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
26
26
  // is measured here (the tabs may grow with the text size) and handed to the CSS as --_over / --_run, the Sheet's measurer
27
- const titleRef = useRef(null);
27
+ const headRef = useRef(null);
28
28
  const [long, setLong] = useState(false);
29
29
  useEffect(() => {
30
- const h = titleRef.current, run = h?.firstElementChild;
30
+ const h = headRef.current?.querySelector(':scope > .words__title'), run = h?.firstElementChild;
31
31
  if (!h || !run)
32
32
  return;
33
33
  const measure = () => setLong(runOver(h, run));
34
34
  measure();
35
35
  return watchSize([h, run], measure);
36
36
  }, [title]);
37
- return (_jsxs(Card, { stack: stack, tone: tone, paper: paper, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: cx('stack-sheet__title', long && 'is-long'), children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon, backAt === 'end' && _jsx(StackBack, { end: true, label: backLabel })] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
37
+ return (_jsxs(Card, { stack: stack, tone: tone, paper: paper, className: cx('stack-sheet', className), ...rest, children: [_jsxs(Head, { ref: headRef, long: long, children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Words, { title: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon, backAt === 'end' && _jsx(StackBack, { end: true, label: backLabel })] }), _jsx(Body, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(Foot, { children: foot })] }));
38
38
  }
@@ -23,7 +23,7 @@ export const stopsOf = (el) => {
23
23
  22-base type scale). Removed when the head fits the floor again, so the CSS number is the one in force. */
24
24
  export function usePeekStrip(ref) {
25
25
  useEffect(() => {
26
- const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
26
+ const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .head');
27
27
  if (!el || !box || !head)
28
28
  return;
29
29
  const publish = () => {
@@ -53,7 +53,7 @@ export function runOver(line, words = line) {
53
53
  /* the Sheet's title: the app's own heading, measured whole (sheet.css runs it with text-indent) */
54
54
  export function useTitleRun(ref, onLong) {
55
55
  useEffect(() => {
56
- const el = ref.current, title = el?.querySelector(':scope > .card__head > :is(h1, h2, h3):first-child, :scope > .sheet__head .sheet__title');
56
+ const el = ref.current, title = el?.querySelector(':scope > .head > :is(.words__title, :is(h1, h2, h3):first-child)');
57
57
  if (!el || !title)
58
58
  return;
59
59
  let was = false;
@@ -75,7 +75,7 @@ export function useTitleRun(ref, onLong) {
75
75
  sheet measures its foot's rows and publishes --sheet-foot-h; the strip, its ground and the body's stop all read it. */
76
76
  export function useFootRoom(ref) {
77
77
  useEffect(() => {
78
- const el = ref.current, foot = el?.querySelector(':scope > .card__foot');
78
+ const el = ref.current, foot = el?.querySelector(':scope > .foot');
79
79
  if (!el || !foot) {
80
80
  el?.style.removeProperty('--sheet-foot-h');
81
81
  return;