@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
package/lib/mcp.mjs CHANGED
@@ -42,7 +42,7 @@ const TOOLS = [
42
42
  },
43
43
  {
44
44
  name: 'get_component',
45
- description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), what it composes and what composes it (composes · composedBy: the catalog names its source renders or calls, and the inverse), its knobs (the CSS custom properties a theme sets to retune it, each with its default, what it sets and the selector that reads it — joined by the classes the component emits, so a knob named for an old name is listed under the component that has it now), and its variations, each a working story with its code. Call it before you use a component you have not used yet, and copy a variation rather than guessing a prop. A component used everywhere (Btn, Text) comes with a few of its stories and the ids of the rest (pass all for their code). Names are case-insensitive; a miss returns the closest names.',
45
+ description: 'One component in full: what it is, its props (a prop marked slot takes children: ReactNode), what it composes and what composes it (composes · composedBy: the catalog names its source renders or calls, and the inverse), its parts (parts · fits: the parts of a line a whole is built of as children, and the wholes a part fits in), its knobs (the CSS custom properties a theme sets to retune it, each with its default, what it sets and the selector that reads it — joined by the classes the component emits, so a knob named for an old name is listed under the component that has it now), and its variations, each a working story with its code. Call it before you use a component you have not used yet, and copy a variation rather than guessing a prop. A component used everywhere (Btn, Text) comes with a few of its stories and the ids of the rest (pass all for their code). Names are case-insensitive; a miss returns the closest names.',
46
46
  inputSchema: { type: 'object', properties: { name: str('component name, e.g. Menu'), html: bool('also return each variation\'s rendered HTML — only to see the markup it makes'), all: bool('include the code of every story that uses the component') }, required: ['name'] },
47
47
  run: (cat, a) => catalog.get(cat, a.name, { html: a.html, all: a.all }),
48
48
  },
package/lint/rules.mjs CHANGED
@@ -168,7 +168,7 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
168
168
  for (const m of text.matchAll(/<Screen\b/g)) {
169
169
  const end = text.indexOf('</Screen>', m.index);
170
170
  const screen = text.slice(m.index, end < 0 ? text.length : end);
171
- if (/<Sheet\b/.test(screen) && /<ActionBar\b/.test(screen)) add('screen.bar-with-sheet', 'warn', m.index, 'a Sheet and an ActionBar on one Screen — a screen with a sheet finishes on the sheet\'s CardFoot, no bar.');
171
+ if (/<Sheet\b/.test(screen) && /<ActionBar\b/.test(screen)) add('screen.bar-with-sheet', 'warn', m.index, 'a Sheet and an ActionBar on one Screen — a screen with a sheet finishes on the sheet\'s Foot, no bar.');
172
172
  }
173
173
 
174
174
  // 4 · focus: cardds places the cursor when these parts come up — an `autoFocus` or a `.focus(` beside them fights it
@@ -218,7 +218,7 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
218
218
  // 8 · a label presses the FIRST control inside it: a button before the row's Check takes the press meant for the row
219
219
  for (const m of text.matchAll(/<Row\b/g)) {
220
220
  const tag = tagAt(text, m.index);
221
- if (/\sas\s*=\s*\{?\s*["']label["']/.test(tag) && /<(?:IconBtn|Btn|Link)\b/.test(tag) && !/\shtmlFor\s*=/.test(tag))
221
+ if (/\sas\s*=\s*\{?\s*["']label["']/.test(tag) && /<(?:IconBtn|Btn|Link)\b/.test(elementAt(text, m.index)) && !/\shtmlFor\s*=/.test(tag))
222
222
  add('row-label-button', 'warn', m.index, '<Row as="label"> holding a button and no htmlFor: a label presses the first control inside it, so a touch on the row\'s words presses that button, not its Check. Give the Check an id and the row htmlFor={id}.');
223
223
  }
224
224
 
@@ -246,16 +246,21 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
246
246
  // a form is never on the ground (Lh 2026-10-18, timebank's ask steps on a bare Page): the control's nearest container written in this file decides.
247
247
  // None in the file (a step component's fragment) is not judged here — the file that places it is
248
248
  const places = [
249
- ...span(/<(?:Sheet|SheetBody|SheetStep|StackSheet|SheetStack|CallSheet|Modal|ModalAsk)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })), // a modal is a surface over the screen too
249
+ // a Sheet's or a Modal's own children are its rows: content in it stands in its Body (a Modal's answers in its ModalAsk; the one control of a
250
+ // head, a Dropdown or a Toggle, in its Head). A Body is the sheet's when nothing round it in this file says otherwise; in a Card it is the card's (field-in-card)
251
+ ...span(/<(?:Sheet|Modal)\b(?![\w.])/g).map((p) => ({ ...p, ground: false, rows: true })),
252
+ ...span(/<(?:Body|SheetStep|StackSheet|SheetStack|CallSheet|ModalAsk)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })), // a modal is a surface over the screen too
250
253
  ...span(/<(?:ActionBarTier|FilterRow|TopBar|AppBar)\b(?![\w.])/g).map((p) => ({ ...p, ground: false })), // a bar's control: a Slider in a control tier, a Dropdown in a filter row or a top bar's actions, a Toggle in an app bar
251
254
  ...span(/<(?:Page|Pages|Step|Screen)\b(?![\w.])/g).map((p) => ({ ...p, ground: true })),
252
255
  ...cards.map((p) => ({ ...p, ground: false })), // a card round it is field-in-card's finding
253
256
  ].sort((a, b) => a.at - b.at);
254
257
  // true on a sheet, false on the ground, null when this file cannot tell. A component of this file is where it is placed: the ask steps' <Page>,
255
- // in a function rendered inside a <SheetBody> further up the file, slides inside that sheet
258
+ // in a function rendered inside a <Body> further up the file, slides inside that sheet
256
259
  const componentAt = (at) => [...text.slice(0, at).matchAll(/(?:function\s+|const\s+)([A-Z]\w*)/g)].at(-1)?.[1];
260
+ const placeAt = (at) => places.filter((p) => within(at, p)).at(-1);
261
+ const heads = span(/<Head\b(?![\w.])/g);
257
262
  const onSheet = (at, depth = 0) => {
258
- const place = places.filter((p) => within(at, p)).at(-1);
263
+ const place = placeAt(at);
259
264
  if (place && !place.ground) return true;
260
265
  const name = depth < 5 && componentAt(at);
261
266
  if (name) for (const u of text.matchAll(new RegExp(`(?<![\\w$)\\].])<${name}\\b(?![\\w.])`, 'g'))) if (onSheet(u.index, depth + 1)) return true;
@@ -263,16 +268,18 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
263
268
  };
264
269
  for (const c of text.matchAll(/<(Field|Composer|ComposerArea|Pin|Dropdown|Slider|Toggle|Check|ChipPick|Calendar|CalendarWeeks|DayStrip|Day|Picker|Mood|AvatarPick|FileBtn|AddRow|MapPick|Tile)\b(?![\w.])/g)) {
265
270
  const kind = c[1] === 'Tile' ? tagAt(text, c.index).match(/\s(pick|upload)\b(?!\s*=\s*\{\s*false)/)?.[1] : null;
266
- if (c[1] === 'Tile' && !kind) continue; // a plain or stat Tile is read, not filled
271
+ if (c[1] === 'Tile' && !kind) continue; // a plain Tile is read, not filled
267
272
  const card = innermost(c.index);
268
273
  if (!card) {
269
- if (onSheet(c.index) === false) add('field-off-sheet', 'error', c.index, `<${kind ? `Tile ${kind}` : c[1]}> on the ground: a form is never on the ground, it sits only on a sheet (Lh 2026-10-18). A Page, Pages, Step or Screen is the base, not a sheet. Put the control on a Sheet's <SheetBody>, in a <SheetStep>, or on a stack sheet — a form over a screen is a <Sheet> in a <SheetStage over dim> (Home/AskSheet).`);
274
+ const place = placeAt(c.index);
275
+ if (place?.rows && !heads.some((h) => within(c.index, h) && h.at > place.at)) { add('field-off-sheet', 'error', c.index, `<${kind ? `Tile ${kind}` : c[1]}> straight in a <${text.slice(place.at + 1).match(/^\w+/)[0]}>: a sheet's and a modal's children are its rows, a Head, a Body and a Foot, and the content stands in the Body (Lh 2026-10-21). Put the control in its <Body>.`); continue; }
276
+ if (onSheet(c.index) === false) add('field-off-sheet', 'error', c.index, `<${kind ? `Tile ${kind}` : c[1]}> on the ground: a form is never on the ground, it sits only on a sheet (Lh 2026-10-18). A Page, Pages, Step or Screen is the base, not a sheet. Put the control in a Sheet's <Body>, in a <SheetStep>, or on a stack sheet — a form over a screen is a <Sheet> in a <SheetStage over dim> (Home/AskSheet).`);
270
277
  continue;
271
278
  }
272
279
  const stack = stacks.filter((s) => card.at >= s.body && card.at < s.end).at(-1);
273
280
  if (stack && !cards.some((o) => o !== card && within(card.at, o) && o.at >= stack.body)) continue;
274
281
  const name = kind ? `Tile ${kind}` : c[1];
275
- add('field-in-card', 'error', c.index, `<${name}> inside a <Card>: the sheet is the paper the member fills in (Lh 2026-10-18) — a card is for reading and pressing buttons. Put the ${name} straight on a Sheet's <SheetBody>, in a <SheetStep>, or on a stack sheet, with no card round it.`);
282
+ add('field-in-card', 'error', c.index, `<${name}> inside a <Card>: the sheet is the paper the member fills in (Lh 2026-10-18) — a card is for reading and pressing buttons. Put the ${name} straight in a Sheet's <Body>, in a <SheetStep>, or on a stack sheet, with no card round it.`);
276
283
  }
277
284
 
278
285
  // 11 · a worded pager bar: back and next never carry the same signal — the forward Btn (the last) is `primary`, the back one is not
@@ -294,6 +301,13 @@ export function checkSource(source, file, { classes = carddsClasses() } = {}) {
294
301
  });
295
302
  }
296
303
 
304
+ // 12 · MapArea bleed is read only with inline (the map in flow): alone it does nothing, and the map stays in its inset
305
+ for (const m of text.matchAll(/<MapArea\b(?![\w.])/g)) {
306
+ const tag = tagAt(text, m.index);
307
+ if (/\sbleed\b(?!\s*=\s*\{\s*false)/.test(tag) && !/\sinline\b(?!\s*=\s*\{\s*false)/.test(tag))
308
+ add('map-bleed-without-inline', 'warn', m.index, '<MapArea bleed> without inline: bleed is read only with inline (the map in flow), so the map stays in its inset. Add inline, or drop bleed.');
309
+ }
310
+
297
311
  return findings.sort((a, b) => a.line - b.line);
298
312
  }
299
313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.105",
3
+ "version": "0.7.107",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
@@ -59,6 +59,7 @@
59
59
  "lint",
60
60
  "lib",
61
61
  "bin",
62
+ "scripts/codemod-parts.mjs",
62
63
  "css",
63
64
  "fonts",
64
65
  "cardds.css",
@@ -0,0 +1,324 @@
1
+ // The parts of a line, and the head, the body and the foot of a card, a sheet, a stack sheet and a modal replace the parts each whole had of its own (docs/parts-plan.md, Lh 2026-10-21): this
2
+ // rewrites a file's JSX and its `cardds` imports from the retired names to the new, through the TypeScript AST, never by the text of a name. A consumer runs it
3
+ // on its screens after the release; the previews were migrated by it. Later PRs of the program add their rows to RENAMES and RESHAPES.
4
+ // npx cardds codemod-parts <files…> rewrite each file in place; a tag it cannot rewrite is printed and left
5
+ // npx cardds codemod-parts --check <files…> exit 1 naming every tag and import that still writes a retired name
6
+ // (in this repo also `node scripts/codemod-parts.mjs …`; it reads `typescript` from the project that runs it)
7
+ import { readFileSync, writeFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import ts from 'typescript';
10
+
11
+ /** old tag → new tag; `inside` when the rewrite holds only under that parent element (a `CardBody` is the words of a `Card row`) */
12
+ export const RENAMES = {
13
+ RowLead: { to: 'Lead' },
14
+ RowBody: { to: 'Words' },
15
+ RowEnd: { to: 'End' },
16
+ CardThumb: { to: 'Lead' },
17
+ CardEnd: { to: 'End' },
18
+ CardBody: { to: 'Words', inside: { tag: 'Card', attr: 'row' } },
19
+ SheetBody: { to: 'Body' },
20
+ ModalBody: { to: 'Body' },
21
+ CardFoot: { to: 'Foot' },
22
+ };
23
+
24
+ /** a tag whose props became children: the props the tag drew its head with, and what each is now. `Card` keeps its own props, so only the head's go */
25
+ export const RESHAPES = {
26
+ CardHead: { to: 'Head', words: { heading: 'title', caption: 'meta', level: 'level' }, end: 'end' },
27
+ SheetHead: { to: 'Head', adds: 'centre', words: { title: 'title', sub: 'meta' }, lead: 'lead', end: 'trail' },
28
+ Card: { words: { heading: 'title', caption: 'meta', level: 'level' }, end: 'end', head: true },
29
+ };
30
+ /** a Card's foot props, written as its last child, a `Foot`: `foot` its children, `footLink` its `link`, `footRound` its `round` */
31
+ const FOOT_PROPS = ['foot', 'footLink', 'footRound'];
32
+ const HEAD_PROPS = { Card: ['heading', 'caption', 'end', 'level'], CardHead: ['heading', 'caption', 'end', 'level'], SheetHead: ['lead', 'title', 'sub', 'trail'] };
33
+ const LINE = 140;
34
+
35
+ const parse = (source, file) => ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
36
+ const tagName = (el) => (ts.isJsxOpeningElement(el) || ts.isJsxSelfClosingElement(el) || ts.isJsxClosingElement(el)) && ts.isIdentifier(el.tagName) ? el.tagName.text : null;
37
+ const attrName = (a) => (ts.isJsxAttribute(a) && ts.isIdentifier(a.name) ? a.name.text : null);
38
+ const findAttr = (opening, name) => opening.attributes.properties.find((a) => attrName(a) === name);
39
+ const hasAttr = (opening, name) => opening.attributes.properties.some((a) => attrName(a) === name && (!a.initializer || (ts.isJsxExpression(a.initializer) && a.initializer.expression?.kind === ts.SyntaxKind.TrueKeyword)));
40
+ /** the element this tag stands directly in, or null at the top or inside an attribute */
41
+ const parentElement = (el) => {
42
+ const self = ts.isJsxSelfClosingElement(el) ? el : el.parent;
43
+ return ts.isJsxElement(self.parent) ? self.parent.openingElement : null;
44
+ };
45
+ const fits = (rule, el) => {
46
+ if (!rule.inside) return true;
47
+ const parent = parentElement(el);
48
+ return !!parent && tagName(parent) === rule.inside.tag && hasAttr(parent, rule.inside.attr);
49
+ };
50
+ const line = (sf, node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
51
+ const openingOf = (n) => (ts.isJsxElement(n) ? n.openingElement : n);
52
+ const headProps = (opening) => (HEAD_PROPS[tagName(opening)] ?? []).filter((p) => findAttr(opening, p));
53
+
54
+ /** every retired name the file still writes, as a JSX tag, a `cardds` import or a prop that became a child: `{ line, name, write }`, in file order */
55
+ export function check(source, file = 'x.tsx') {
56
+ const sf = parse(source, file);
57
+ const out = [];
58
+ const walk = (n) => {
59
+ const name = tagName(n);
60
+ if (name && name in RENAMES && !ts.isJsxClosingElement(n)) out.push({ line: line(sf, n), name, write: RENAMES[name].to });
61
+ if (name && (name === 'CardHead' || name === 'SheetHead') && !ts.isJsxClosingElement(n)) out.push({ line: line(sf, n), name, write: 'Head' });
62
+ if (name === 'Card' && (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n))) {
63
+ for (const p of headProps(n)) out.push({ line: line(sf, n), name: `Card ${p}`, write: '<Head><Words title meta /></Head> as its first child' });
64
+ for (const p of FOOT_PROPS.filter((f) => findAttr(n, f))) out.push({ line: line(sf, n), name: `Card ${p}`, write: '<Foot round link>…</Foot> as its last child' });
65
+ }
66
+ ts.forEachChild(n, walk);
67
+ };
68
+ walk(sf);
69
+ for (const st of sf.statements) {
70
+ if (!st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
71
+ const named = ts.isFunctionDeclaration(st) ? [st.name] : ts.isVariableStatement(st) ? st.declarationList.declarations.map((d) => d.name) : [];
72
+ for (const id of named) if (id && ts.isIdentifier(id) && STORY.test(id.text)) out.push({ line: line(sf, id), name: id.text, write: storyName(id.text) });
73
+ }
74
+ for (const imp of sf.statements.filter(ts.isImportDeclaration)) {
75
+ const nb = imp.importClause?.namedBindings;
76
+ if (imp.moduleSpecifier.text !== 'cardds' || !nb || !ts.isNamedImports(nb)) continue;
77
+ for (const e of nb.elements) {
78
+ const name = (e.propertyName ?? e.name).text;
79
+ if (name in RENAMES) out.push({ line: line(sf, e), name: e.name.text, write: RENAMES[name].to });
80
+ if (name === 'CardHead' || name === 'SheetHead') out.push({ line: line(sf, e), name: e.name.text, write: 'Head' });
81
+ }
82
+ }
83
+ return out.sort((a, b) => a.line - b.line);
84
+ }
85
+
86
+ const lineIndent = (source, pos) => /^[ \t]*/.exec(source.slice(source.lastIndexOf('\n', pos - 1) + 1))[0];
87
+ const valueOf = (attr, sf) => (attr.initializer ? attr.initializer.getText(sf) : '{true}');
88
+ /** a prop's value as a child: a JSX element stands as it is, anything else in braces */
89
+ function childOf(attr, sf) {
90
+ const init = attr.initializer;
91
+ if (!init) return null;
92
+ const expr = ts.isJsxExpression(init) ? init.expression : init;
93
+ if (!expr) return null;
94
+ const jsx = ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) || ts.isJsxFragment(expr);
95
+ return jsx ? expr.getText(sf) : `{${expr.getText(sf)}}`;
96
+ }
97
+ /** `parts` laid on one line when they fit, else one to a line under `indent` */
98
+ function lay(open, parts, close, indent) {
99
+ const one = `${open}${parts.join('')}${close}`;
100
+ if (one.length + indent.length <= LINE && !one.includes('\n')) return one;
101
+ return `${open}\n${parts.map((p) => `${indent} ${p}`).join('\n')}\n${indent}${close}`;
102
+ }
103
+ /** the words the head's props draw, as one `<Words … />`, or null when none was given */
104
+ function wordsOf(opening, rule, sf) {
105
+ const props = Object.entries(rule.words).map(([from, to]) => [to, findAttr(opening, from)]).filter(([, a]) => a);
106
+ if (!props.some(([to]) => to === 'title' || to === 'meta')) return null;
107
+ return `<Words ${props.map(([to, a]) => `${to}=${valueOf(a, sf)}`).join(' ')} />`;
108
+ }
109
+ /** the opening tag's text without the props that became children (each removed with the space before it) */
110
+ function without(source, opening, names, sf) {
111
+ const cuts = names.map((n) => findAttr(opening, n)).filter(Boolean).map((a) => [a.getFullStart(), a.end]).sort((a, b) => b[0] - a[0]);
112
+ let text = source.slice(opening.getStart(sf), opening.end);
113
+ const base = opening.getStart(sf);
114
+ for (const [from, to] of cuts) text = text.slice(0, from - base) + text.slice(to - base);
115
+ return text;
116
+ }
117
+
118
+ /** the innermost head-shaped element that still writes the old props, as an edit: `{ pos, end, text }`, or null */
119
+ function reshape(source, sf, el) {
120
+ const opening = openingOf(el), name = tagName(opening);
121
+ const rule = RESHAPES[name];
122
+ if (!rule) return null;
123
+ const used = headProps(opening);
124
+ if (name === 'CardHead' && !used.length && ts.isJsxSelfClosingElement(el) === false) {
125
+ return { rename: true };
126
+ }
127
+ if (rule.head && !used.length) return footOf(source, sf, el);
128
+ const indent = lineIndent(source, opening.getStart(sf));
129
+ const words = wordsOf(opening, rule, sf);
130
+ const endAttr = findAttr(opening, rule.end);
131
+ const end = endAttr ? childOf(endAttr, sf) : null;
132
+ const lead = rule.lead && findAttr(opening, rule.lead) ? childOf(findAttr(opening, rule.lead), sf) : null;
133
+ const moved = Object.keys(rule.words).concat(rule.end, rule.lead ?? []);
134
+ const rest = (without(source, opening, moved, sf)).replace(/\s*\/>$/, '>').replace(/\s*\n\s*>$/, '>').replace(/^<(\w+)\s+>$/, '<$1>').replace(/^<(\w+)/, (m, tag) => `<${rule.to ?? tag}${rule.adds ? ` ${rule.adds}` : ''}`);
135
+ if (rule.head && !words && !end) {
136
+ const was = source.slice(opening.getStart(sf), opening.end);
137
+ return { pos: opening.getStart(sf), end: opening.end, text: ts.isJsxSelfClosingElement(el) ? was.replace(new RegExp(`\\s*\\b(${moved.join('|')})=(\\{[^}]*\\}|"[^"]*")`, 'g'), '') : rest };
138
+ }
139
+ if (rule.head) {
140
+ const head = lay('<Head>', [words, end].filter(Boolean), '</Head>', `${indent} `);
141
+ if (ts.isJsxSelfClosingElement(el)) return { pos: el.getStart(sf), end: el.end, text: `${rest}\n${indent} ${head}\n${indent}</Card>` };
142
+ return { pos: opening.getStart(sf), end: opening.end, text: `${rest}\n${indent} ${head}` };
143
+ }
144
+ if (ts.isJsxSelfClosingElement(el)) {
145
+ const open = rest;
146
+ return { pos: el.getStart(sf), end: el.end, text: lay(open, [lead, words, end].filter(Boolean), '</Head>', indent) };
147
+ }
148
+ const front = [lead, words].filter(Boolean);
149
+ const inner = source.slice(opening.end, el.closingElement.getStart(sf));
150
+ if (!inner.includes('\n')) return { pos: el.getStart(sf), end: el.end, text: lay(rest, [...front, inner.trim(), end].filter(Boolean), '</Head>', indent) };
151
+ const edits = [{ pos: opening.getStart(sf), end: opening.end, text: front.length ? `${rest}\n${indent} ${front.join(`\n${indent} `)}` : rest }];
152
+ edits.push({ pos: el.closingElement.getStart(sf), end: el.closingElement.end, text: `${end ? ` ${end}\n${indent}` : ''}</Head>` });
153
+ return { edits };
154
+ }
155
+
156
+ /** a node's text with its lines after the first moved as far as its first one moves, to stand at column `to` */
157
+ function moved(node, sf, text, to) {
158
+ const by = to - sf.getLineAndCharacterOfPosition(node.getStart(sf)).character;
159
+ return text.replace(/\n( *)/g, (m, sp) => `\n${' '.repeat(Math.max(0, sp.length + by))}`);
160
+ }
161
+ /** what a Card's `foot` held, as the Foot's children standing at column `to`: a fragment's children each as it is, anything else as one child */
162
+ function footChildren(attr, sf, to) {
163
+ const expr = attr.initializer && ts.isJsxExpression(attr.initializer) ? attr.initializer.expression : null;
164
+ if (expr && ts.isJsxFragment(expr)) return expr.children.filter((c) => !(ts.isJsxText(c) && !c.getText(sf).trim())).map((c) => moved(c, sf, c.getText(sf).trim(), to));
165
+ const child = childOf(attr, sf);
166
+ if (!child) return [];
167
+ return [expr && (ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr)) ? moved(expr, sf, child, to) : child];
168
+ }
169
+ /** a Card that still writes its foot with props, as an edit that drops them and writes a `<Foot>` as its last child (`footRound` alone drew no foot: it only goes) */
170
+ function footOf(source, sf, el) {
171
+ const opening = openingOf(el);
172
+ const used = FOOT_PROPS.filter((p) => findAttr(opening, p));
173
+ if (!used.length) return null;
174
+ const indent = lineIndent(source, opening.getStart(sf));
175
+ const self = ts.isJsxSelfClosingElement(el);
176
+ const rest = without(source, opening, used, sf).replace(/\s*\/>$/, '>').replace(/\s*\n\s*>$/, '>');
177
+ const fa = findAttr(opening, 'foot'), la = findAttr(opening, 'footLink'), ra = findAttr(opening, 'footRound');
178
+ if (!fa && !la) return { pos: opening.getStart(sf), end: opening.end, text: self ? rest.replace(/>$/, ' />') : rest };
179
+ const round = !ra ? '' : hasAttr(opening, 'footRound') ? ' round' : ` round=${valueOf(ra, sf)}`;
180
+ const attrs = `${round}${la ? ` link=${valueOf(la, sf)}` : ''}`;
181
+ const kids = fa ? footChildren(fa, sf, indent.length + 4) : [];
182
+ const foot = kids.length ? lay(`<Foot${attrs}>`, kids, '</Foot>', `${indent} `) : `<Foot${attrs} />`;
183
+ if (self) return { pos: el.getStart(sf), end: el.end, text: `${rest}\n${indent} ${foot}\n${indent}</Card>` };
184
+ const inner = source.slice(opening.end, el.closingElement.getStart(sf));
185
+ if (!inner.includes('\n')) return { pos: el.getStart(sf), end: el.end, text: lay(rest, [inner.trim(), foot].filter(Boolean), '</Card>', indent) };
186
+ return { edits: [{ pos: opening.getStart(sf), end: opening.end, text: rest }, { pos: el.closingElement.getStart(sf), end: el.closingElement.getStart(sf), text: ` ${foot}\n${indent}` }] };
187
+ }
188
+
189
+ /** the file rewritten: `source` after, `tags` rewritten, `imports` rewritten, and `left`, the tags no rule fits (`{ line, name, why }`) */
190
+ export function rewrite(source, file = 'x.tsx') {
191
+ let tags = 0, imports = 0;
192
+ const left = [];
193
+ let text = source;
194
+ for (let pass = 0; pass < 12; pass++) {
195
+ const r = once(text, file);
196
+ if (r.source === text) { left.splice(0, left.length, ...r.left); break; }
197
+ tags += r.tags;
198
+ text = r.source;
199
+ if (pass === 11) throw new Error(`codemod-parts: ${file} still changing after 12 passes`);
200
+ }
201
+ const fin = fixImports(text, file);
202
+ for (const name of fin.clash) left.push({ line: 1, name, why: `the file declares its own ${name}, which the import would shadow: rename it` });
203
+ const stories = renameStories(fin.source, file);
204
+ return { source: stories.source, tags, imports: fin.imports, stories: stories.count, left };
205
+ }
206
+
207
+ /** one pass: the renames of RENAMES, then the innermost reshape that overlaps no other edit */
208
+ function once(source, file) {
209
+ const sf = parse(source, file);
210
+ const edits = [];
211
+ const left = [];
212
+ const walk = (n) => {
213
+ const name = tagName(n);
214
+ if (name && name in RENAMES) {
215
+ const rule = RENAMES[name];
216
+ const opening = ts.isJsxClosingElement(n) ? n.parent.openingElement : n;
217
+ if (fits(rule, opening)) edits.push({ pos: n.tagName.getStart(sf), end: n.tagName.end, text: rule.to });
218
+ else if (!ts.isJsxClosingElement(n)) left.push({ line: line(sf, n), name, why: `not in a <${rule.inside.tag} ${rule.inside.attr}>` });
219
+ }
220
+ ts.forEachChild(n, walk);
221
+ };
222
+ walk(sf);
223
+ if (edits.length) return { source: apply(source, edits), tags: edits.length, left };
224
+ const found = [];
225
+ const reshapes = (n) => {
226
+ if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n)) {
227
+ const r = reshape(source, sf, n);
228
+ if (r) found.push({ node: n, r });
229
+ }
230
+ ts.forEachChild(n, reshapes);
231
+ };
232
+ reshapes(sf);
233
+ const inner = found.filter(({ node }) => !found.some((o) => o.node !== node && o.node.getStart(sf) >= node.getStart(sf) && o.node.end <= node.end));
234
+ const out = [];
235
+ for (const { node, r } of inner) {
236
+ if (r.rename) { const el = node; out.push({ pos: el.openingElement.tagName.getStart(sf), end: el.openingElement.tagName.end, text: 'Head' }, { pos: el.closingElement.tagName.getStart(sf), end: el.closingElement.tagName.end, text: 'Head' }); }
237
+ else out.push(...(r.edits ?? [r]));
238
+ }
239
+ return { source: apply(source, out), tags: inner.length, left };
240
+ }
241
+
242
+ const apply = (source, edits) => {
243
+ let out = source;
244
+ for (const e of edits.sort((a, b) => b.pos - a.pos)) out = out.slice(0, e.pos) + e.text + out.slice(e.end);
245
+ return out;
246
+ };
247
+
248
+ /** a story named for a part it showed (`CardHeadTop`, `SheetHeadLeadTrail`, `SheetBodyScrolls`, `CardFootRound`) is named for the one part (`HeadTop`,
249
+ * `HeadLeadTrail`, `BodyScrolls`, `FootRound`), wherever the file says it */
250
+ const STORY = /^(CardHead|SheetHead|SheetBody|CardFoot)(?=[A-Z])/;
251
+ const storyName = (name) => name.replace(STORY, (m) => ({ SheetBody: 'Body', CardFoot: 'Foot' })[m] ?? 'Head');
252
+ function renameStories(source, file) {
253
+ const sf = parse(source, file);
254
+ const names = new Set();
255
+ for (const st of sf.statements) {
256
+ if (!st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
257
+ if (ts.isFunctionDeclaration(st) && st.name && STORY.test(st.name.text)) names.add(st.name.text);
258
+ if (ts.isVariableStatement(st)) for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name) && STORY.test(d.name.text)) names.add(d.name.text);
259
+ }
260
+ if (!names.size) return { source, count: 0 };
261
+ const edits = [];
262
+ const walk = (n) => { if (ts.isIdentifier(n) && names.has(n.text)) edits.push({ pos: n.getStart(sf), end: n.end, text: storyName(n.text) }); ts.forEachChild(n, walk); };
263
+ walk(sf);
264
+ return { source: apply(source, edits), count: names.size };
265
+ }
266
+
267
+ /** the names between the braces, wrapped under 140 columns when the import was written over lines */
268
+ function importList(names, wrapped) {
269
+ if (!wrapped) return `{ ${names.join(', ')} }`;
270
+ const rows = [''];
271
+ for (const n of names) { const row = rows[rows.length - 1]; if (row && row.length + n.length > LINE - 6) rows.push(n); else rows[rows.length - 1] = row ? `${row}, ${n}` : n; }
272
+ return `{\n${rows.map((r) => ` ${r},`).join('\n')}\n}`;
273
+ }
274
+
275
+ /** the `cardds` import after the tags changed: a retired name replaced by its new one in place, `Head` and `Words` added where a tag now needs them, a name no tag writes any more dropped */
276
+ function fixImports(source, file) {
277
+ const sf = parse(source, file);
278
+ const used = new Set();
279
+ const walk = (n) => { const name = tagName(n); if (name && !ts.isJsxClosingElement(n)) used.add(name); ts.forEachChild(n, walk); };
280
+ walk(sf);
281
+ const edits = [];
282
+ let imports = 0;
283
+ for (const imp of sf.statements.filter(ts.isImportDeclaration)) {
284
+ const nb = imp.importClause?.namedBindings;
285
+ if (imp.moduleSpecifier.text !== 'cardds' || !nb || !ts.isNamedImports(nb)) continue;
286
+ const names = [];
287
+ const add = (t) => { if (!names.includes(t)) names.push(t); };
288
+ for (const e of nb.elements) {
289
+ const name = (e.propertyName ?? e.name).text;
290
+ const retired = !e.propertyName && (name in RENAMES || name === 'CardHead' || name === 'SheetHead') && !used.has(name);
291
+ if (!retired) { add(e.getText(sf)); continue; }
292
+ const to = name in RENAMES ? RENAMES[name].to : 'Head';
293
+ add(to);
294
+ if (to === 'Head' && used.has('Words')) add('Words');
295
+ }
296
+ for (const part of ['Head', 'Words', 'Foot']) if (used.has(part) && !names.includes(part)) names.push(part);
297
+ if (names.join(', ') === nb.elements.map((e) => e.getText(sf)).join(', ')) continue;
298
+ imports++;
299
+ edits.push({ pos: nb.getStart(sf), end: nb.end, text: importList(names, nb.getText(sf).includes('\n')) });
300
+ }
301
+ const declared = new Set(sf.statements.flatMap((st) => (ts.isFunctionDeclaration(st) || ts.isClassDeclaration(st)) && st.name ? [st.name.text] : ts.isVariableStatement(st) ? st.declarationList.declarations.flatMap((d) => (ts.isIdentifier(d.name) ? [d.name.text] : [])) : []));
302
+ return { source: apply(source, edits), imports, clash: ['Head', 'Words', 'Body', 'Foot'].filter((n) => used.has(n) && declared.has(n)) };
303
+ }
304
+
305
+ /** the command: rewrite each file in place, or with `--check` name what still writes a retired name; the exit code */
306
+ export function main(args) {
307
+ const checking = args.includes('--check');
308
+ const files = args.filter((a) => a !== '--check');
309
+ if (!files.length) { console.error('usage: npx cardds codemod-parts [--check] <files…>'); return 2; }
310
+ let failed = false;
311
+ for (const file of files) {
312
+ const source = readFileSync(file, 'utf8');
313
+ if (checking) {
314
+ for (const { line, name, write } of check(source, file)) { failed = true; console.log(`${file}:${line}: ${name} is retired, write ${write}`); }
315
+ continue;
316
+ }
317
+ const { source: out, tags, imports, stories, left } = rewrite(source, file);
318
+ if (out !== source) { writeFileSync(file, out); console.log(`${file}: ${tags} tag${tags === 1 ? '' : 's'}, ${imports} import${imports === 1 ? '' : 's'}${stories ? `, ${stories} stor${stories === 1 ? 'y' : 'ies'}` : ''}`); }
319
+ for (const { line, name, why } of left) { failed = true; console.log(`${file}:${line}: <${name}> left as it is, ${why}`); }
320
+ }
321
+ return failed ? 1 : 0;
322
+ }
323
+
324
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exit(main(process.argv.slice(2)));
@@ -1,15 +0,0 @@
1
- import type { ComponentProps, ReactNode } from 'react';
2
- export interface CardFootProps 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' — `--card-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
- /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
12
- * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
13
- * should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one `primary` with a word, the others
14
- * with icons): one centred group, a fixed gap between them. */
15
- export declare function CardFoot({ link, round, className, children, ...rest }: CardFootProps): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /** CardFoot — the card's action buttons on one line: one takes the width, up to four share it evenly. Btns only in the row; a small
4
- * link goes in `link`, under it. A `Btn form` keeps its own size here: as wide as its words, centred — for a foot whose one button
5
- * should not fill the card. With `round` it is the foot of a few round `IconBtn`s (`size="xl"`, one `primary` with a word, the others
6
- * with icons): one centred group, a fixed gap between them. */
7
- export function CardFoot({ link, round, className, children, ...rest }) {
8
- return (_jsxs("div", { className: cx('card__foot', link != null && 'card__foot--link', round && 'card__foot--round', className), ...rest, children: [children, link != null && _jsx("div", { className: "card__foot-link", children: link })] }));
9
- }
@@ -1,21 +0,0 @@
1
- import { type ComponentProps, type ReactNode } from 'react';
2
- export interface CardHeadProps extends ComponentProps<'div'> {
3
- /** the heading (a string becomes an h2 at level `t-h2`) — wraps freely, as many lines as the words need */
4
- heading?: ReactNode;
5
- /** the muted line under the heading (a date, a subtitle) */
6
- caption?: ReactNode;
7
- /** the contextual thing at the far right: a chip, an IconBtn (more), a Link, a small Ring, an Avatar, a Badge (a count in a stack head) */
8
- end?: ReactNode;
9
- /** top-align a genuinely tall multi-line title with its control */
10
- top?: boolean;
11
- /** the heading level to render the string heading with (h2 default; h1 for the one-feature page) */
12
- level?: 'h1' | 'h2' | 'display';
13
- /** the sheet measured this title as wider than its line, so the heading runs sideways */
14
- long?: boolean;
15
- }
16
- /**
17
- * CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
18
- * Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
19
- * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
20
- */
21
- export declare function CardHead({ heading, caption, end, top, level, long, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
@@ -1,39 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } 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
- const onTitleKey = (e) => {
7
- if (e.key !== 'Enter' && e.key !== ' ')
8
- return;
9
- e.preventDefault();
10
- e.currentTarget.click();
11
- };
12
- /** the stack's title is the keyboard's way in. The heading renders the tab stop and, when the sheet measured it long, `.is-long`. Nothing sets either on the node afterwards */
13
- function markTitle(node, tab, long) {
14
- if (!isValidElement(node))
15
- return node;
16
- const el = node;
17
- const level = el.type === Text ? el.props.level : null;
18
- const intrinsic = typeof el.type === 'string' && /^h[1-3]$/.test(el.type);
19
- if (level !== 'h1' && level !== 'h2' && level !== 'display' && !intrinsic)
20
- return node;
21
- return cloneElement(el, {
22
- ...(long ? { className: cx(el.props.className, 'is-long') } : {}),
23
- ...(tab != null ? {
24
- tabIndex: tab,
25
- onKeyDown: (e) => { el.props.onKeyDown?.(e); if (!e.defaultPrevented)
26
- onTitleKey(e); },
27
- } : {}),
28
- });
29
- }
30
- /**
31
- * CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
32
- * Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
33
- * (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
34
- */
35
- export function CardHead({ heading, caption, end, top, level = 'h2', long, className, children, ...rest }) {
36
- const tab = useContext(StackHeadCtx);
37
- const H = level === 'h1' ? 'h1' : 'h2';
38
- return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: cx(`t-${level}`, long && 'is-long'), tabIndex: tab ?? undefined, onKeyDown: tab != null ? onTitleKey : undefined, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), tab == null && !long ? children : Children.map(children, (c) => markTitle(c, tab, !!long)), end] }));
39
- }
@@ -1,19 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- export interface StatProps extends ComponentProps<'div'> {
3
- /** takes the remaining row space beside a Ring */
4
- grow?: boolean;
5
- /** centred: the one number of a sheet or a stage, under a centred Avatar */
6
- centre?: boolean;
7
- }
8
- /** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
9
- export declare function Stat({ grow, centre, className, ...rest }: StatProps): import("react").JSX.Element;
10
- export interface StatRowProps extends ComponentProps<'div'> {
11
- }
12
- /** StatRow — label left, Delta (or caption) right, inside a Stat. */
13
- export declare function StatRow({ className, ...rest }: StatRowProps): import("react").JSX.Element;
14
- export interface DeltaProps extends ComponentProps<'span'> {
15
- /** a drop: painted in the signal colour */
16
- neg?: boolean;
17
- }
18
- /** Delta — the small "+7.5%" pill beside a stat label. */
19
- export declare function Delta({ neg, className, ...rest }: DeltaProps): import("react").JSX.Element;
@@ -1,14 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
4
- export function Stat({ grow, centre, className, ...rest }) {
5
- return _jsx("div", { className: cx('stat', grow && 'grow', centre && 'stat--centre', className), ...rest });
6
- }
7
- /** StatRow — label left, Delta (or caption) right, inside a Stat. */
8
- export function StatRow({ className, ...rest }) {
9
- return _jsx("div", { className: cx('stat__row', className), ...rest });
10
- }
11
- /** Delta — the small "+7.5%" pill beside a stat label. */
12
- export function Delta({ neg, className, ...rest }) {
13
- return _jsx("span", { className: cx('delta', neg && 'delta--neg', className), ...rest });
14
- }
@@ -1,5 +0,0 @@
1
- import { type ComponentProps } from 'react';
2
- export interface SheetBodyProps extends ComponentProps<'div'> {
3
- }
4
- /** SheetBody — everything under a Sheet's head: the one thing that scrolls; its scroll padding keeps a focused control above the cut. Content never goes straight into the Sheet. */
5
- export declare function SheetBody({ className, ...rest }: SheetBodyProps): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useContext } from 'react';
3
- import { cx } from '../cx.js';
4
- import { StackPullCtx } from './stackCard.js';
5
- /** SheetBody — everything under a Sheet's head: the one thing that scrolls; its scroll padding keeps a focused control above the cut. Content never goes straight into the Sheet. */
6
- export function SheetBody({ className, ...rest }) {
7
- const pulling = useContext(StackPullCtx);
8
- return _jsx("div", { className: cx('sheet__body', pulling && 'is-refreshing', className), ...rest });
9
- }
@@ -1,15 +0,0 @@
1
- import type { ComponentProps, ReactNode } from 'react';
2
- export interface SheetHeadProps extends Omit<ComponentProps<'div'>, 'title'> {
3
- /** the round button on the left (an IconBtn sm) */
4
- lead?: ReactNode;
5
- /** the centred title */
6
- title: ReactNode;
7
- /** the caption under the title */
8
- sub?: ReactNode;
9
- /** the round button on the right */
10
- trail?: ReactNode;
11
- /** the sheet measured this title as wider than its line, so it runs sideways */
12
- long?: boolean;
13
- }
14
- /** SheetHead — the stage sheet's three-column head: round button · centred title + sub · round button. A step sheet uses a CardHead instead. */
15
- export declare function SheetHead({ lead, title, sub, trail, long, className, ...rest }: SheetHeadProps): import("react").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cx } from '../cx.js';
3
- /** SheetHead — the stage sheet's three-column head: round button · centred title + sub · round button. A step sheet uses a CardHead instead. */
4
- export function SheetHead({ lead, title, sub, trail, long, className, ...rest }) {
5
- return (_jsxs("div", { className: cx('sheet__head', className), ...rest, children: [lead ?? _jsx("span", {}), _jsxs("div", { children: [_jsx("h2", { className: cx('sheet__title', long && 'is-long'), children: title }), sub != null && _jsx("span", { className: "sheet__sub", children: sub })] }), trail ?? _jsx("span", {})] }));
6
- }