@usableapp/cardds 0.7.105 → 0.7.107

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 (100) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/bin/cardds.mjs +6 -2
  3. package/cardds.css +1 -0
  4. package/css/actions.css +16 -9
  5. package/css/base.css +6 -9
  6. package/css/card.css +21 -173
  7. package/css/finish.css +4 -4
  8. package/css/journey.css +4 -1
  9. package/css/layover.css +4 -14
  10. package/css/lists.css +3 -18
  11. package/css/media.css +1 -7
  12. package/css/parts.css +380 -0
  13. package/css/sheet.css +21 -140
  14. package/css/stack.css +15 -26
  15. package/css/stamp.css +1 -1
  16. package/css/step.css +3 -3
  17. package/css/theme.template.css +1 -1
  18. package/css/tokens.css +5 -4
  19. package/dist/actions/BarNote.d.ts +2 -2
  20. package/dist/actions/BarNote.js +2 -2
  21. package/dist/actions/Btn.d.ts +2 -2
  22. package/dist/actions/Btn.js +1 -1
  23. package/dist/actions/BtnRow.d.ts +1 -1
  24. package/dist/actions/BtnRow.js +1 -1
  25. package/dist/actions/Chip.d.ts +7 -3
  26. package/dist/actions/Chip.js +5 -5
  27. package/dist/actions/IconBtn.d.ts +1 -1
  28. package/dist/actions/IconBtn.js +1 -1
  29. package/dist/actions/IconBtnGroup.d.ts +1 -1
  30. package/dist/cardds.css +486 -410
  31. package/dist/cards/Card.d.ts +6 -22
  32. package/dist/cards/Card.js +11 -22
  33. package/dist/cards/CardFeed.d.ts +1 -1
  34. package/dist/cards/CardFeed.js +5 -5
  35. package/dist/cards/CardParts.d.ts +0 -12
  36. package/dist/cards/CardParts.js +0 -12
  37. package/dist/cards/cardGo.js +1 -1
  38. package/dist/cards/cardGrounds.js +2 -2
  39. package/dist/catalog.json +1 -1
  40. package/dist/forms/Field.d.ts +1 -1
  41. package/dist/forms/Field.js +1 -1
  42. package/dist/index.d.ts +6 -5
  43. package/dist/index.js +6 -5
  44. package/dist/lists/Row.d.ts +7 -22
  45. package/dist/lists/Row.js +6 -4
  46. package/dist/media/MapArea.d.ts +7 -3
  47. package/dist/media/MapArea.js +4 -3
  48. package/dist/media/Tile.d.ts +3 -7
  49. package/dist/media/Tile.js +5 -5
  50. package/dist/numbers/Spinner.d.ts +1 -1
  51. package/dist/numbers/Spinner.js +1 -1
  52. package/dist/parts/Body.d.ts +7 -0
  53. package/dist/parts/Body.js +11 -0
  54. package/dist/parts/End.d.ts +10 -0
  55. package/dist/parts/End.js +7 -0
  56. package/dist/parts/Foot.d.ts +17 -0
  57. package/dist/parts/Foot.js +11 -0
  58. package/dist/parts/Head.d.ts +15 -0
  59. package/dist/parts/Head.js +34 -0
  60. package/dist/parts/Lead.d.ts +10 -0
  61. package/dist/parts/Lead.js +9 -0
  62. package/dist/parts/Words.d.ts +13 -0
  63. package/dist/parts/Words.js +20 -0
  64. package/dist/parts/headCtx.d.ts +7 -0
  65. package/dist/parts/headCtx.js +9 -0
  66. package/dist/scaffold/EmptyState.d.ts +1 -1
  67. package/dist/scaffold/EmptyState.js +1 -1
  68. package/dist/scaffold/Screen.js +1 -1
  69. package/dist/sheets/Modal.d.ts +3 -7
  70. package/dist/sheets/Modal.js +3 -7
  71. package/dist/sheets/Sheet.d.ts +2 -2
  72. package/dist/sheets/Sheet.js +8 -9
  73. package/dist/sheets/SheetStack.d.ts +3 -3
  74. package/dist/sheets/SheetStack.js +4 -4
  75. package/dist/sheets/SheetSteps.d.ts +1 -1
  76. package/dist/sheets/SheetSteps.js +2 -2
  77. package/dist/sheets/StackBack.d.ts +1 -1
  78. package/dist/sheets/StackBack.js +1 -1
  79. package/dist/sheets/StackSheet.js +7 -7
  80. package/dist/sheets/sheetMeasure.js +3 -3
  81. package/dist/sheets/sheetStepsPile.js +2 -2
  82. package/dist/sheets/stackCard.js +6 -6
  83. package/dist/templates/CallSheet.js +6 -5
  84. package/dist/templates/Walkthrough.js +3 -2
  85. package/docs/reference.md +75 -36
  86. package/lib/catalog.mjs +2 -0
  87. package/lib/mcp.mjs +1 -1
  88. package/lint/rules.mjs +22 -8
  89. package/package.json +2 -1
  90. package/scripts/codemod-parts.mjs +324 -0
  91. package/dist/cards/CardFoot.d.ts +0 -15
  92. package/dist/cards/CardFoot.js +0 -9
  93. package/dist/cards/CardHead.d.ts +0 -21
  94. package/dist/cards/CardHead.js +0 -39
  95. package/dist/cards/Stat.d.ts +0 -19
  96. package/dist/cards/Stat.js +0 -14
  97. package/dist/sheets/SheetBody.d.ts +0 -5
  98. package/dist/sheets/SheetBody.js +0 -9
  99. package/dist/sheets/SheetHead.d.ts +0 -15
  100. package/dist/sheets/SheetHead.js +0 -6
@@ -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;
@@ -13,7 +13,7 @@ export function usePile(ref, moving) {
13
13
  above the open step, and half of the one before — older ones go up out of sight, one row at a time, so what was done never seems to vanish (Lh
14
14
  2026-09-24: moving the pile further startled; with two rows or fewer nothing moves) */
15
15
  const settle = () => {
16
- const list = ref.current, body = list?.closest('.sheet__body'), now = list?.querySelector('.sheet-step--now');
16
+ const list = ref.current, body = list?.closest('.body'), now = list?.querySelector('.sheet-step--now');
17
17
  if (!list || !body || !now)
18
18
  return;
19
19
  const above = [];
@@ -43,7 +43,7 @@ export function usePile(ref, moving) {
43
43
  For a short while after a field in the steps gains or loses the focus, every such jump is played as a quick slide (FLIP: the steps
44
44
  are set back by the jump, then eased to rest) so the eye can follow the pile moving out of the way and back. */
45
45
  useEffect(() => {
46
- const el = ref.current, body = el?.closest('.sheet__body');
46
+ const el = ref.current, body = el?.closest('.body');
47
47
  if (!el || !body)
48
48
  return;
49
49
  last.current = body.scrollTop;
@@ -38,13 +38,13 @@ const readFrame = (el) => { el.getBoundingClientRect(); };
38
38
  const along = (dir, startY, y) => (dir === 'down' ? Math.max(0, y - startY) : Math.max(0, startY - y));
39
39
  /** the head the sheet owns: the card's own child. A card standing in the sheet's body has a head too, and it is that card's */
40
40
  export const ownHead = (card, target) => {
41
- const head = target.closest('.card__head');
41
+ const head = target.closest('.head');
42
42
  return head?.parentElement === card ? head : null;
43
43
  };
44
44
  const pullsHead = (target, head) => {
45
45
  if (!(target instanceof Element))
46
46
  return false;
47
- return target === head || !!target.closest('.sheet-stack__back') || !!target.closest('.card__head > :is(h1, h2, h3)');
47
+ return target === head || !!target.closest('.sheet-stack__back') || !!target.closest('.head > :is(h1, h2, h3)');
48
48
  };
49
49
  /* once a finger is down the DOCUMENT hears it, not the head or the body it started on: the finger leaves them while it drags.
50
50
  One set of move / up / cancel at a time; `stop` takes the set off, and is safe to call with none on */
@@ -160,7 +160,7 @@ export function useStackCard(ref, place, swallow) {
160
160
  useEffect(() => {
161
161
  if (!place?.open || !ref.current)
162
162
  return;
163
- const cancel = focusWhenActive(ref.current, { scope: ':scope > .sheet__body' });
163
+ const cancel = focusWhenActive(ref.current, { scope: ':scope > .body' });
164
164
  return () => { cancel(); };
165
165
  }, [place?.open, ref]);
166
166
  /* open: a pull down on the head folds it; a pull down in the content, already at the top, asks for a reload */
@@ -170,8 +170,8 @@ export function useStackCard(ref, place, swallow) {
170
170
  if (!card || !here?.open)
171
171
  return;
172
172
  const stack = card.closest('.sheet-stack');
173
- const head = card.querySelector(':scope > .card__head');
174
- const scroller = card.querySelector(':scope > .sheet__body') ?? card;
173
+ const head = card.querySelector(':scope > .head');
174
+ const scroller = card.querySelector(':scope > .body') ?? card;
175
175
  const doc = docPointer();
176
176
  const primary = (e) => e.isPrimary && e.button === 0;
177
177
  const onHeadDown = (e) => {
@@ -298,7 +298,7 @@ export function useStackCard(ref, place, swallow) {
298
298
  if (!card || !here || here.open || here.before || here.after)
299
299
  return;
300
300
  const stack = card.closest('.sheet-stack');
301
- const head = card.querySelector(':scope > .card__head');
301
+ const head = card.querySelector(':scope > .head');
302
302
  if (!head || !stack)
303
303
  return;
304
304
  const doc = docPointer();
@@ -5,9 +5,10 @@ import { Screen } from '../scaffold/Screen.js';
5
5
  import { Step } from '../sheets/Step.js';
6
6
  import { Page, CtaPack } from '../scaffold/Page.js';
7
7
  import { Sheet } from '../sheets/Sheet.js';
8
- import { SheetBody } from '../sheets/SheetBody.js';
9
- import { CardHead } from '../cards/CardHead.js';
10
- import { CardFoot } from '../cards/CardFoot.js';
8
+ import { Body } from '../parts/Body.js';
9
+ import { Head } from '../parts/Head.js';
10
+ import { Words } from '../parts/Words.js';
11
+ import { Foot } from '../parts/Foot.js';
11
12
  import { Text } from '../type/Text.js';
12
13
  import { Btn } from '../actions/Btn.js';
13
14
  const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
@@ -29,9 +30,9 @@ export function CallSheet({ media, heading, body, action, sheet, ways, open: ope
29
30
  if (was !== null)
30
31
  setLeaving(was);
31
32
  }
32
- return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => w.action == null ? null : (_jsx(Btn, { primary: i === first, quiet: i !== first, "aria-expanded": on === i, onClick: () => { setAt('full'); set(i); }, children: w.action }, i))) })] }), list.map(({ sheet: sh }, i) => (_jsxs(Sheet, { state: on === i ? at : 'away', onStateChange: (s) => { if (s === 'away')
33
+ return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(Head, { children: _jsx(Words, { title: heading, level: "h1" }) }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => w.action == null ? null : (_jsx(Btn, { primary: i === first, quiet: i !== first, "aria-expanded": on === i, onClick: () => { setAt('full'); set(i); }, children: w.action }, i))) })] }), list.map(({ sheet: sh }, i) => (_jsxs(Sheet, { state: on === i ? at : 'away', onStateChange: (s) => { if (s === 'away')
33
34
  set(null);
34
35
  else
35
36
  setAt(s); }, onSettled: (s) => { if (s === 'away' && leaving === i)
36
- setLeaving(null); }, states: states, handle: sh.handle ?? handle, children: [_jsx(CardHead, { heading: sh.title }), _jsx(SheetBody, { children: on === i || leaving === i ? (typeof sh.body === 'function' ? sh.body() : sh.body) : null }), (sh.foot != null || sh.footLink != null) && _jsx(CardFoot, { link: sh.footLink, children: sh.foot })] }, i)))] }), children] }));
37
+ setLeaving(null); }, states: states, handle: sh.handle ?? handle, children: [_jsx(Head, { children: _jsx(Words, { title: sh.title }) }), _jsx(Body, { children: on === i || leaving === i ? (typeof sh.body === 'function' ? sh.body() : sh.body) : null }), (sh.foot != null || sh.footLink != null) && _jsx(Foot, { link: sh.footLink, children: sh.foot })] }, i)))] }), children] }));
37
38
  }
@@ -7,7 +7,8 @@ import { Page } from '../scaffold/Page.js';
7
7
  import { Pages, slideTo } from '../scaffold/Centre.js';
8
8
  import { Pager } from '../scaffold/Pager.js';
9
9
  import { ActionBar, ActionBarTier } from '../scaffold/ActionBar.js';
10
- import { CardHead } from '../cards/CardHead.js';
10
+ import { Head } from '../parts/Head.js';
11
+ import { Words } from '../parts/Words.js';
11
12
  import { Text } from '../type/Text.js';
12
13
  import { IconBtn } from '../actions/IconBtn.js';
13
14
  import { word } from '../words.js';
@@ -51,5 +52,5 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
51
52
  const pager = (_jsxs(_Fragment, { children: [_jsx(IconBtn, { icon: "arrow-left", label: backLabel, disabled: at === 0, onClick: () => go(at - 1) }), _jsx(IconBtn, { invert: true, icon: "arrow-right", label: nextLabel, disabled: at === count - 1, onClick: () => go(at + 1) })] }));
52
53
  const dots = count > 1 ? _jsx(Pager, { at: at + 1, of: count }) : null;
53
54
  const bottom = dots || controls.length || ctaLink != null ? (_jsxs(ActionBar, { variant: "tiers", children: [dots, controls.map((c, i) => _jsx(ActionBarTier, { children: c }, i)), _jsx(ActionBarTier, { pager: true, children: custom ? row : pager }), ctaLink != null && _jsx(ActionBarTier, { className: "action-bar__tier--link", children: ctaLink })] })) : (_jsx(ActionBar, { variant: "pager", children: custom ? row : pager }));
54
- return (_jsxs(Screen, { fill: true, bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { ...swipe, children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
55
+ return (_jsxs(Screen, { fill: true, bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { ...swipe, children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(Head, { children: _jsx(Words, { title: p.heading, level: "h1" }) }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
55
56
  }
package/docs/reference.md CHANGED
@@ -42,7 +42,7 @@ npm test # Playwright: the tokens and the step/sheet/centre geometry
42
42
  ```
43
43
 
44
44
  ```tsx
45
- import { Screen, TopBar, Step, Page, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
45
+ import { Screen, TopBar, Step, Page, Sheet, Head, Words, Body, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
46
46
  import '@usableapp/cardds/dist/cardds.css';
47
47
  import './theme.css'; // the project's own look, after the system's — see "A project's theme"
48
48
  ```
@@ -95,7 +95,7 @@ npm i @usableapp/cardds
95
95
  ```
96
96
 
97
97
  ```tsx
98
- import { Card, CardHead, Btn } from '@usableapp/cardds';
98
+ import { Card, Head, Words, Btn } from '@usableapp/cardds';
99
99
  import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the package
100
100
  import './theme.css'; // the project's look, after cardds.css
101
101
  ```
@@ -177,7 +177,7 @@ A scaffolded tsx may import `./_story` or `./_*Art`: story glue, not in the pack
177
177
  ### React adapter — how a component is written
178
178
 
179
179
  `src/<group>/<Name>.tsx`, one export per class family; sub-parts are their own
180
- components (`CardHead`, `KvRow`, `TrackStep`, `SheetBody`). A component
180
+ components (`Head`, `KvRow`, `TrackStep`, `Body`). A component
181
181
  chooses classes from props (`tone`, `state`, `primary`, `sm`…) and lays the
182
182
  contract's children in order. A component that has STATE is controlled like an input
183
183
  (`state` / `open` / `value` / `flipped` / `selected` + its `on…`, or a `default…` twin), keeps its own behaviour
@@ -188,6 +188,37 @@ wrapper follows. **Claude Design** builds with these components through
188
188
  stories, the conventions the design agent reads) — a new component needs a
189
189
  preview story there.
190
190
 
191
+ ### Outer and inner — a paper holds rows
192
+
193
+ An outer is the paper and how it behaves: a `Card`, a `Sheet`, a stack sheet (a `Card` in a `SheetStack`), a `Modal`.
194
+ Inside every outer stand the same rows, top to bottom: a `Head` (what this is, and the one thing beside it), a `Body`
195
+ (what the member reads or fills in, the part of a sheet that scrolls) and a `Foot` (what the member can do next).
196
+ A line inside a head or a row is made of `Lead` · `Words` · `End`, the same parts a `Row` and a `Card row` are made of.
197
+ To change the behaviour, change the outer and keep what is inside: the same children read the same in a card and a sheet.
198
+ An outer governs an inner only through the inner's knobs (`--head-*`, `--body-*`, `--foot-*`, `--words-*`) or a rule in
199
+ `css/parts.css` that names both (`.sheet > .foot`), never through a second component (Lh 2026-10-21).
200
+
201
+ ```tsx
202
+ <Card>
203
+ <Head><Words title="Sunday market" meta="Saturday · 07:00" /><Chip>3 new</Chip></Head>
204
+ <Text level="body">Neighbours trade what they grew.</Text>
205
+ <Foot><Btn primary>Join</Btn></Foot>
206
+ </Card>
207
+
208
+ <Sheet defaultState="half">
209
+ <Head><Words title="Sunday market" meta="Saturday · 07:00" /><Chip>3 new</Chip></Head>
210
+ <Body><Text level="body">Neighbours trade what they grew.</Text></Body>
211
+ <Foot><Btn primary block>Join</Btn></Foot>
212
+ </Sheet>
213
+ ```
214
+
215
+ A card's content stands straight in it, between the head and the foot; a sheet's and a modal's content stands in its
216
+ `Body`, the one part that scrolls (`cardds check` names a control written straight in a `Sheet` or a `Modal`). `ModalAsk`
217
+ is one piece, the question and its answers. The templates take data and draw the parts themselves (`StackSheet title tabs foot`,
218
+ `Home sheets`, `CallSheet`, `Walkthrough pages`). `cardds get Card` lists the parts a card holds; `cardds get Head` the outers it fits.
219
+ A screen written on the retired names (`CardHead`, `SheetHead`, `SheetBody`, `ModalBody`, `CardFoot`, `RowLead`, `RowBody`, `RowEnd`,
220
+ `CardThumb`, `CardBody`, `CardEnd`, `Card heading caption end level foot footLink footRound`) moves with `npx cardds codemod-parts <files>`.
221
+
191
222
  ## Sheet stack markup
192
223
 
193
224
  The sheet stack (`.sheet-stack`, `stack.css` — the menu, see *Anatomy*) is the
@@ -198,9 +229,9 @@ the card is the material, the sheet is what it is in the stack.
198
229
  ```html
199
230
  <div class="sheet-stack sheet-stack--tap"> <!-- drop --tap for a static stack -->
200
231
  <article class="card card--2">
201
- <div class="card__head">
232
+ <div class="head">
202
233
  <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
203
- <h2 class="t-h2 t-caps">Shopping</h2>
234
+ <h2 class="words__title t-h2 t-caps">Shopping</h2>
204
235
  <span class="icon-row">…</span> <!-- optional trailing icons -->
205
236
  </div>
206
237
  …the sheet's own content…
@@ -279,7 +310,7 @@ of `Screen`:
279
310
  covers what is under it — on purpose, nothing is added to the content to make room. The keyboard lifts the layer (`--kb`).
280
311
  - **The bar is INSIDE the screen** — `bottom`, never a sibling after `</Screen>`, never pinned with a hand-set `position`.
281
312
  A `FloatBar` in `bottom` floats the same way.
282
- - **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `CardFoot`; `Screen` warns once when both
313
+ - **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `Foot`; `Screen` warns once when both
283
314
  are there.
284
315
  - **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
285
316
  `tests/composition.spec.js`) fails a bar after `</Screen>` or a pinned bar, warns on a Sheet + ActionBar.
@@ -297,7 +328,7 @@ whose markup has rules, because the sheet's box is the step itself:
297
328
  <!-- reads: the words only -->
298
329
  <section class="step">
299
330
  <section class="page">
300
- <div class="card__head"><h2 class="t-h2">…</h2></div>
331
+ <div class="head"><h2 class="words__title t-h2">…</h2></div>
301
332
  <p class="t-body">…</p>
302
333
  </section>
303
334
  </section>
@@ -308,12 +339,12 @@ whose markup has rules, because the sheet's box is the step itself:
308
339
  <section class="page">…the words…</section>
309
340
  <article class="card sheet sheet--half">
310
341
  <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
311
- <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
312
- <div class="sheet__body">
342
+ <div class="head"><h2 class="words__title t-h2">…</h2><span class="chip">…</span></div>
343
+ <div class="body">
313
344
  …cardds controls (.field / .pin / .composer / .rows + .check / .segment /
314
345
  .chip-grid), or small content cards (.card--sm)…
315
346
  </div>
316
- <div class="card__foot"><button class="btn btn--primary btn--block">…the one button that finishes the sheet…</button></div>
347
+ <div class="foot"><button class="btn btn--primary btn--block">…the one button that finishes the sheet…</button></div>
317
348
  </article>
318
349
  </section>
319
350
  <!-- no .action-bar: a bar is the base's, never in front of a sheet -->
@@ -332,10 +363,10 @@ whose markup has rules, because the sheet's box is the step itself:
332
363
  - **`.page` first, `.sheet` last.** The block carries the step's words
333
364
  ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
334
365
  Lh: a step reads like a page, not a card in a page). It keeps a card's
335
- rhythm: a `.card__head`, `--card-gap` between things. The sheet is
336
- **handle · head · body**: the handle, then a `.card__head` title row (that
366
+ rhythm: a `.head`, `--card-gap` between things. The sheet is
367
+ **handle · head · body**: the handle, then a `.head` title row (that
337
368
  row is what `peek` shows — heading left, the contextual thing right), then
338
- `.sheet__body` with what the member acts on. The handle and the head are
369
+ a `.body` with what the member acts on. The handle and the head are
339
370
  pinned; the body is the one thing that scrolls, to the card's own edges. The
340
371
  handle is a line, or four dots (`mark="dots"`): both sit in the card's top
341
372
  padding, the same distance from the edge, so the title stays put. `refreshing`
@@ -367,7 +398,7 @@ whose markup has rules, because the sheet's box is the step itself:
367
398
  the button at half and 3q as it does at full.
368
399
  - **Layers: the base, the sheet over it, the top bar** (Lh 2026-09-20). The `.action-bar` belongs to the
369
400
  BASE — the move of a page of words (a pager bar, or one wide button). It is never in front of a sheet. A
370
- screen with a sheet finishes ON the sheet: a `.card__foot`, the sheet's last child — ONE row, the page's
401
+ screen with a sheet finishes ON the sheet: a `.foot`, the sheet's last child — ONE row, the page's
371
402
  conclusion. The foot stands on the screen's bottom edge at half, 3q and full alike (a layer as tall as
372
403
  the sheet, slid up by the share the sheet slid down — `sheet.css`), and is gone at peek. Nothing inside
373
404
  the sheet's body commits on its own (a `Composer` there takes no `send`). This is how cardds's own screens are
@@ -415,7 +446,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
415
446
  <div class="screen screen--fill">
416
447
  <header class="topbar"><h1 class="t-h1">ธนาคารเวลายินดีต้อนรับ</h1></header>
417
448
  <article class="card card--centre">
418
- <div class="card__head"><div><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
449
+ <div class="head"><h2 class="words__title t-h2">ปรับขนาดตัวหนังสือ</h2></div>
419
450
  <p class="t-body">…</p>
420
451
  </article>
421
452
  </div>
@@ -495,19 +526,28 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
495
526
  sheet**. Content normally comes in cards; static data (a blurb, a legal
496
527
  note, help steps) may sit on the base directly.
497
528
  - **A card is head · content · foot.**
498
- - **Head** (`.card__head`): the heading on the left; hints, actions or a
499
- more-menu — the contextual info — at the far right. With nothing at
500
- the far right it runs all the way across. The heading wraps, no
501
- squeeze, up to three lines and is cut there with an ellipsis (Lh
502
- 2026-10-21; `--card-head-title-lines`, default 3). A date
529
+ - **Head** (`.head`, the `Head` part): the first row, what this is and
530
+ the one thing beside it. Its children are, in order, an optional lead
531
+ (a `Lead`, a `StackBack`, a `JourneyBadge`), the `Words` (the title takes
532
+ the room), chips, and an end: hints, actions or a more-menu — the
533
+ contextual info — at the far right (`IconBtn sm`, `Chip`, `Link`, `End`).
534
+ With nothing at the far right the title runs all the way across. The title
535
+ wraps, no squeeze, up to three lines and is cut there with an ellipsis (Lh
536
+ 2026-10-21; `--head-title-lines`, default 3). A date
503
537
  (`JourneyBadge stretch`) beside it is as tall as the title block up to
504
- its own three lines, at the head's top. The head is FLAT markup — heading, then an
505
- optional caption as a span, then the far-right thing; no
506
- wrapper element, no `<p>`: the head wraps, the caption takes the row
507
- below (`order`), the gap tokens do the rest (Lh 2026-09-16).
538
+ its own three lines, at the head's top. The head is FLAT markup — the
539
+ title (`h2.words__title`, `h1` with `level="h1"`), then an optional meta
540
+ as a span (`.words__meta`), then the far-right thing; no
541
+ wrapper element, no `<p>`: `Words` in a `Head` writes its title and its
542
+ meta as the head's own children. The head wraps, the meta takes the row
543
+ below (`order`), the gap tokens do the rest (Lh 2026-09-16). The
544
+ title's type is the knob `--words-title-font` (default the level's own
545
+ face), which a stack sheet, a sheet and a page set for their heads. `top`
546
+ keeps a tall title's control at the top; `centre` is the stage sheet's
547
+ head, a round button · the title over its meta · a round button.
508
548
  - **Content**: whatever the card frames — rows, key-values, a gauge, a
509
549
  paragraph. Frameless, per rule 9.
510
- - **Foot** (`.card__foot`): the card's action buttons on one line. One
550
+ - **Foot** (`.foot`): the card's action buttons on one line. One
511
551
  button takes the width; up to four share it in even parts; from three
512
552
  on they drop their side padding so the labels keep the room. A panel of
513
553
  up to four icon buttons (`IconBtnGroup`) takes the foot the same way; with
@@ -515,10 +555,10 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
515
555
  as `Btn form` does — in a foot, a `Page`, a `Centre` or a `CtaPack` — and
516
556
  with `form labelled` its `label` is SHOWN as words at its left, the two
517
557
  centred as one line (Lh 2026-10-10).
518
- - In React the head and the foot are the card's **slots**:
519
- `<Card heading caption end foot footLink>` with the content as children —
520
- the card writes the order (head first, foot last). A head of its own shape
521
- is a `<CardHead>` child instead; never both (Lh 2026-09-27).
558
+ - In React the head is the card's first child, a `<Head>` (`<Head><Words title meta /><Chip>…</Chip></Head>`),
559
+ and the foot its last, a `<Foot>` (`<Foot link={<Link sm>…</Link>}><Btn primary>…</Btn></Foot>`, `round` for a few
560
+ big round buttons), the content between them. The card finds its `Head` and its `Foot` by their type, so a
561
+ component of your own that wraps one is not found (Lh 2026-10-21: one head, body and foot for a card, a sheet and a stack sheet).
522
562
  - **A card that goes on is pressed whole** (Lh 2026-10-09). A card whose ONLY
523
563
  control is its foot's button, or the end button of a row card, is pressed
524
564
  anywhere: a touch off the button runs that button's own action once, and the
@@ -566,7 +606,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
566
606
  sheet.
567
607
  - **The sheet is the paper the member fills in** (Lh 2026-10-18). Every
568
608
  control the member fills in or picks stands on a sheet: a `Sheet`'s
569
- `SheetBody`, a `SheetStep`, or a stack sheet. That is `Field`, `Composer`
609
+ `Body`, a `SheetStep`, or a stack sheet. That is `Field`, `Composer`
570
610
  and `ComposerArea`, `Pin`, `Dropdown`, `Slider`, `Toggle`, `Check`,
571
611
  `ChipPick`, `Calendar`, `DayStrip` and `Day`, `Picker`, `Mood`,
572
612
  `AvatarPick`, `FileBtn`, `AddRow`, `MapPick`, a `Tile pick` and a
@@ -705,8 +745,7 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
705
745
  its third, 4:5, the gaps one `--screen-pad` both ways (`--stamp-book-fill-gap`), nothing at the row's sides; with `bare`
706
746
  at 375 a stamp is 103.67 × 129.58. A `false` or `null` child holds no place.
707
747
  - A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it stands in the
708
- middle and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
709
- end={<Stamp bare …/>} foot={<Btn primary block>…</Btn>} />` — the words stand at the left and the stamp at the right,
748
+ middle and the words and link under it centre. In a card's head — `<Card foot={<Btn primary block>…</Btn>}><Head><Words title={name} meta={greeting} /><Stamp bare …/></Head></Card>` — the words stand at the left and the stamp at the right,
710
749
  beside both lines; the card's one button presses it whole. In an `AvatarStack` it is a head, the small size
711
750
  (`more="+9"` for the count).
712
751
  - **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
@@ -860,7 +899,7 @@ The gallery's `PostScreens` page has the three finished screens (read, write, ke
860
899
  state — the stack's rule, inherited. The box clips whatever slid below its
861
900
  bottom edge, so a sheet at any height is still a card: rounded top, a
862
901
  straight cut at the box's edge — and its shape is handle · head · body:
863
- the handle and the title row are pinned, `.sheet__body` scrolls beneath
902
+ the handle and the title row are pinned, its `.body` scrolls beneath
864
903
  them, and the body's `scroll-padding-bottom` is the slid-under part (in
865
904
  the sheet's own container units, `--_cover`), so a focused control is
866
905
  revealed above the cut, not under it. `--sheet-half` is a share of the
@@ -889,7 +928,7 @@ question and hunts for its answer. Rules 11–13 serve that:
889
928
  11. **Everything holds to edges.** Every element touches at least two edges —
890
929
  the card's, the column's, or a line made by the thing above it (a row, the
891
930
  avatar + name line). Words to READ are aligned start, values end (the
892
- receipt: items left, prices right; `.card__head`, `.kv`, `.row`); centred
931
+ receipt: items left, prices right; `.head`, `.kv`, `.row`); centred
893
932
  only what is not read — dots, a lone number, an icon, a mark. Empty room is
894
933
  not filled with more content; a new edge is made instead (line the buttons
895
934
  up with the avatar). One exception: a small control with its own words
@@ -908,14 +947,14 @@ question and hunts for its answer. Rules 11–13 serve that:
908
947
  bolder, never a second colour.
909
948
  14. **A frame says "tap me".** Inside a card, only a control draws its own
910
949
  edge (a chip, a field, a pick tile, a small `Card sm` to pick). What is
911
- only READ never gets a frame — no card in a card: a stat tile stands on
950
+ only READ never gets a frame — no card in a card: a KvCell stands on
912
951
  the card's ground, a note is a `Banner` (a light band, a rule above and
913
952
  below, edge to edge), a count is words (`Text caption muted`), never a
914
953
  chip. A chip or tile that looks pressable and is not lies.
915
954
  15. **A component adjusts another only through its knobs.** A component that
916
955
  holds another (a sheet holding a card, a stack holding a sheet) sets the
917
956
  child's knobs (`.sheet { --card-pad: … }`), never the child's own
918
- property (`.sheet .card__head { color: … }`). The child's file is the one
957
+ property (`.sheet .head { color: … }`). The child's file is the one
919
958
  place its look is written, so what a story shows is what a project gets.
920
959
  Missing the knob? Add it in the child's file. No `!important` either, but
921
960
  `[hidden]`'s. A component belongs to the file that writes its bare class
package/lib/catalog.mjs CHANGED
@@ -76,6 +76,8 @@ export function get(cat, name, { html = false, all = false } = {}) {
76
76
  ...(c.params && { params: c.params }),
77
77
  composes: c.composes,
78
78
  composedBy: c.composedBy,
79
+ parts: c.parts ?? [],
80
+ fits: c.fits ?? [],
79
81
  knobs: c.knobs,
80
82
  variations: [...own, ...elsewhere.filter((s) => shown.includes(s))].map(show),
81
83
  ...(elsewhere.length > shown.length && { alsoUsedIn: elsewhere.filter((s) => !shown.includes(s)).map((s) => s.id) }),