@usableapp/cardds 0.7.106 → 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.
- package/CHANGELOG.md +97 -0
- package/bin/cardds.mjs +5 -3
- package/cardds.css +1 -0
- package/css/actions.css +9 -9
- package/css/base.css +6 -9
- package/css/card.css +16 -186
- package/css/finish.css +4 -4
- package/css/journey.css +1 -1
- package/css/layover.css +4 -14
- package/css/lists.css +3 -18
- package/css/media.css +1 -7
- package/css/parts.css +380 -0
- package/css/sheet.css +12 -139
- package/css/stack.css +15 -26
- package/css/stamp.css +1 -1
- package/css/step.css +3 -3
- package/css/theme.template.css +1 -1
- package/css/tokens.css +4 -4
- package/dist/actions/BarNote.d.ts +2 -2
- package/dist/actions/BarNote.js +2 -2
- package/dist/actions/Btn.d.ts +2 -2
- package/dist/actions/Btn.js +1 -1
- package/dist/actions/BtnRow.d.ts +1 -1
- package/dist/actions/BtnRow.js +1 -1
- package/dist/actions/IconBtn.d.ts +1 -1
- package/dist/actions/IconBtn.js +1 -1
- package/dist/cardds.css +461 -422
- package/dist/cards/Card.d.ts +6 -22
- package/dist/cards/Card.js +11 -22
- package/dist/cards/CardFeed.d.ts +1 -1
- package/dist/cards/CardFeed.js +5 -5
- package/dist/cards/CardParts.d.ts +0 -12
- package/dist/cards/CardParts.js +0 -12
- package/dist/cards/cardGo.js +1 -1
- package/dist/cards/cardGrounds.js +2 -2
- package/dist/catalog.json +1 -1
- package/dist/forms/Field.d.ts +1 -1
- package/dist/forms/Field.js +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -5
- package/dist/lists/Row.d.ts +2 -28
- package/dist/lists/Row.js +2 -17
- package/dist/media/MapArea.d.ts +2 -2
- package/dist/media/MapArea.js +1 -1
- package/dist/media/Tile.d.ts +3 -7
- package/dist/media/Tile.js +5 -5
- package/dist/parts/Body.d.ts +7 -0
- package/dist/parts/Body.js +11 -0
- package/dist/parts/End.d.ts +10 -0
- package/dist/parts/End.js +7 -0
- package/dist/parts/Foot.d.ts +17 -0
- package/dist/parts/Foot.js +11 -0
- package/dist/parts/Head.d.ts +15 -0
- package/dist/parts/Head.js +34 -0
- package/dist/parts/Lead.d.ts +10 -0
- package/dist/parts/Lead.js +9 -0
- package/dist/parts/Words.d.ts +13 -0
- package/dist/parts/Words.js +20 -0
- package/dist/parts/headCtx.d.ts +7 -0
- package/dist/parts/headCtx.js +9 -0
- package/dist/scaffold/EmptyState.d.ts +1 -1
- package/dist/scaffold/EmptyState.js +1 -1
- package/dist/scaffold/Screen.js +1 -1
- package/dist/sheets/Modal.d.ts +3 -7
- package/dist/sheets/Modal.js +3 -7
- package/dist/sheets/Sheet.d.ts +2 -2
- package/dist/sheets/Sheet.js +8 -9
- package/dist/sheets/SheetStack.d.ts +3 -3
- package/dist/sheets/SheetStack.js +4 -4
- package/dist/sheets/SheetSteps.d.ts +1 -1
- package/dist/sheets/SheetSteps.js +2 -2
- package/dist/sheets/StackBack.d.ts +1 -1
- package/dist/sheets/StackBack.js +1 -1
- package/dist/sheets/StackSheet.js +7 -7
- package/dist/sheets/sheetMeasure.js +3 -3
- package/dist/sheets/sheetStepsPile.js +2 -2
- package/dist/sheets/stackCard.js +6 -6
- package/dist/templates/CallSheet.js +6 -5
- package/dist/templates/Walkthrough.js +3 -2
- package/docs/reference.md +75 -36
- package/lib/catalog.mjs +1 -1
- package/lib/mcp.mjs +1 -1
- package/lint/rules.mjs +14 -7
- package/package.json +2 -1
- package/scripts/codemod-parts.mjs +324 -0
- package/dist/cards/CardFoot.d.ts +0 -15
- package/dist/cards/CardFoot.js +0 -9
- package/dist/cards/CardHead.d.ts +0 -21
- package/dist/cards/CardHead.js +0 -39
- package/dist/cards/Stat.d.ts +0 -19
- package/dist/cards/Stat.js +0 -14
- package/dist/sheets/SheetBody.d.ts +0 -5
- package/dist/sheets/SheetBody.js +0 -9
- package/dist/sheets/SheetHead.d.ts +0 -15
- package/dist/sheets/SheetHead.js +0 -6
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
|
|
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
|
|
@@ -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
|
-
|
|
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 <
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
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)));
|
package/dist/cards/CardFoot.d.ts
DELETED
|
@@ -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;
|
package/dist/cards/CardFoot.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cards/CardHead.d.ts
DELETED
|
@@ -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;
|
package/dist/cards/CardHead.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cards/Stat.d.ts
DELETED
|
@@ -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;
|
package/dist/cards/Stat.js
DELETED
|
@@ -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;
|
package/dist/sheets/SheetBody.js
DELETED
|
@@ -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;
|
package/dist/sheets/SheetHead.js
DELETED
|
@@ -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
|
-
}
|