@usableapp/cardds 0.7.107 → 0.7.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/css/card.css +3 -0
- package/css/choice.css +26 -1
- package/css/journey.css +6 -2
- package/css/lists.css +3 -0
- package/css/palette.css +1 -1
- package/dist/cardds.css +39 -4
- package/dist/cards/CardParts.d.ts +2 -1
- package/dist/cards/CardParts.js +2 -1
- package/dist/catalog.json +1 -1
- package/dist/choice/Calendar.d.ts +12 -3
- package/dist/choice/Calendar.js +17 -5
- package/dist/choice/CalendarWeeks.d.ts +10 -2
- package/dist/choice/CalendarWeeks.js +18 -4
- package/dist/choice/HourGrid.d.ts +36 -0
- package/dist/choice/HourGrid.js +21 -0
- package/dist/choice/calendarDay.d.ts +7 -2
- package/dist/choice/calendarDay.js +6 -3
- package/dist/choice/span.d.ts +34 -0
- package/dist/choice/span.js +80 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/journey/JourneyBadge.d.ts +2 -1
- package/dist/lists/Kv.d.ts +4 -2
- package/dist/lists/Kv.js +3 -3
- package/docs/reference.md +1 -1
- package/package.json +1 -1
- package/scripts/codemod-parts.mjs +107 -28
|
@@ -1,9 +1,13 @@
|
|
|
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 (
|
|
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 (Lh 2026-10-21, the inner parts): this
|
|
2
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.
|
|
3
|
+
// on its screens after the release; the previews were migrated by it. It also moves the `Row` props 0.7.106 retired (`title`, `meta`, `lead`, `leadIcon`,
|
|
4
|
+
// `body`, `end`, `endValue`, `endCaption`) into the line's children, which the 0.7.107 version left; a screen migrated with that version is checked again.
|
|
4
5
|
// 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
|
|
6
|
+
// npx cardds codemod-parts --check <files…> exit 1 naming every tag, prop, import and type that still writes a retired name
|
|
6
7
|
// (in this repo also `node scripts/codemod-parts.mjs …`; it reads `typescript` from the project that runs it)
|
|
8
|
+
// A part a tag now needs is imported once, on the file's value import (`import type { … }` only has its type names renamed, `CardFootProps` → `FootProps`).
|
|
9
|
+
// A Card's `foot={cond ? x : undefined}` (or `: null`, or `cond && x`) keeps its condition, `{cond ? <Foot>{x}</Foot> : null}`, so no empty foot is drawn;
|
|
10
|
+
// a `foot={value}` is written `<Foot>{value}</Foot>` as it is: the script cannot know whether `value` may be undefined, and the consumer reads that line.
|
|
7
11
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
8
12
|
import { fileURLToPath } from 'node:url';
|
|
9
13
|
import ts from 'typescript';
|
|
@@ -21,15 +25,28 @@ export const RENAMES = {
|
|
|
21
25
|
CardFoot: { to: 'Foot' },
|
|
22
26
|
};
|
|
23
27
|
|
|
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
|
|
28
|
+
/** 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;
|
|
29
|
+
* a `Row` keeps its own too, and the props that drew its line (ROW_PROPS) go */
|
|
25
30
|
export const RESHAPES = {
|
|
26
31
|
CardHead: { to: 'Head', words: { heading: 'title', caption: 'meta', level: 'level' }, end: 'end' },
|
|
27
32
|
SheetHead: { to: 'Head', adds: 'centre', words: { title: 'title', sub: 'meta' }, lead: 'lead', end: 'trail' },
|
|
28
33
|
Card: { words: { heading: 'title', caption: 'meta', level: 'level' }, end: 'end', head: true },
|
|
34
|
+
Row: { line: true },
|
|
29
35
|
};
|
|
30
36
|
/** a Card's foot props, written as its last child, a `Foot`: `foot` its children, `footLink` its `link`, `footRound` its `round` */
|
|
31
37
|
const FOOT_PROPS = ['foot', 'footLink', 'footRound'];
|
|
32
38
|
const HEAD_PROPS = { Card: ['heading', 'caption', 'end', 'level'], CardHead: ['heading', 'caption', 'end', 'level'], SheetHead: ['lead', 'title', 'sub', 'trail'] };
|
|
39
|
+
/** a Row's props of 0.7.105, each written as a child in this order (the 0.7.106 table): `lead` and `end` stand bare, the rest become a Lead, a Words, an End */
|
|
40
|
+
const ROW_PROPS = {
|
|
41
|
+
lead: 'its lead as the first child', leadIcon: '<Lead icon /> first', title: '<Words title meta /> after the lead', meta: '<Words title meta /> after the lead',
|
|
42
|
+
body: '<Words title meta>…</Words> after the lead', endValue: '<End value caption /> after the words', endCaption: '<End value caption /> after the words', end: 'its end as the last child',
|
|
43
|
+
};
|
|
44
|
+
/** the new tag a retired tag is written as, or null */
|
|
45
|
+
const retiredTo = (name) => (name in RENAMES ? RENAMES[name].to : RESHAPES[name]?.to ?? null);
|
|
46
|
+
/** a retired tag's props type → the part's (`CardFootProps` → `FootProps`) */
|
|
47
|
+
const TYPE_RENAMES = Object.fromEntries(Object.keys({ ...RENAMES, ...RESHAPES }).filter(retiredTo).map((n) => [`${n}Props`, `${retiredTo(n)}Props`]));
|
|
48
|
+
/** the parts a reshape writes, imported where the file's tags now need them */
|
|
49
|
+
const PARTS = ['Head', 'Words', 'Foot', 'Lead', 'End'];
|
|
33
50
|
const LINE = 140;
|
|
34
51
|
|
|
35
52
|
const parse = (source, file) => ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
@@ -50,6 +67,7 @@ const fits = (rule, el) => {
|
|
|
50
67
|
const line = (sf, node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
51
68
|
const openingOf = (n) => (ts.isJsxElement(n) ? n.openingElement : n);
|
|
52
69
|
const headProps = (opening) => (HEAD_PROPS[tagName(opening)] ?? []).filter((p) => findAttr(opening, p));
|
|
70
|
+
const rowProps = (opening) => Object.keys(ROW_PROPS).filter((p) => findAttr(opening, p));
|
|
53
71
|
|
|
54
72
|
/** 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
73
|
export function check(source, file = 'x.tsx') {
|
|
@@ -57,12 +75,14 @@ export function check(source, file = 'x.tsx') {
|
|
|
57
75
|
const out = [];
|
|
58
76
|
const walk = (n) => {
|
|
59
77
|
const name = tagName(n);
|
|
60
|
-
if (name && name
|
|
61
|
-
if (name && (name === 'CardHead' || name === 'SheetHead') && !ts.isJsxClosingElement(n)) out.push({ line: line(sf, n), name, write: 'Head' });
|
|
78
|
+
if (name && retiredTo(name) && !ts.isJsxClosingElement(n)) out.push({ line: line(sf, n), name, write: retiredTo(name) });
|
|
62
79
|
if (name === 'Card' && (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n))) {
|
|
63
80
|
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
81
|
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
82
|
}
|
|
83
|
+
if (name === 'Row' && (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n))) {
|
|
84
|
+
for (const p of rowProps(n)) out.push({ line: line(sf, n), name: `Row ${p}`, write: ROW_PROPS[p] });
|
|
85
|
+
}
|
|
66
86
|
ts.forEachChild(n, walk);
|
|
67
87
|
};
|
|
68
88
|
walk(sf);
|
|
@@ -76,8 +96,8 @@ export function check(source, file = 'x.tsx') {
|
|
|
76
96
|
if (imp.moduleSpecifier.text !== 'cardds' || !nb || !ts.isNamedImports(nb)) continue;
|
|
77
97
|
for (const e of nb.elements) {
|
|
78
98
|
const name = (e.propertyName ?? e.name).text;
|
|
79
|
-
|
|
80
|
-
if (
|
|
99
|
+
const to = retiredTo(name) ?? TYPE_RENAMES[name];
|
|
100
|
+
if (to) out.push({ line: line(sf, e), name: e.name.text, write: to });
|
|
81
101
|
}
|
|
82
102
|
}
|
|
83
103
|
return out.sort((a, b) => a.line - b.line);
|
|
@@ -85,14 +105,18 @@ export function check(source, file = 'x.tsx') {
|
|
|
85
105
|
|
|
86
106
|
const lineIndent = (source, pos) => /^[ \t]*/.exec(source.slice(source.lastIndexOf('\n', pos - 1) + 1))[0];
|
|
87
107
|
const valueOf = (attr, sf) => (attr.initializer ? attr.initializer.getText(sf) : '{true}');
|
|
88
|
-
/** a prop's value
|
|
89
|
-
function
|
|
108
|
+
/** a prop's value, the expression or string it holds, or null when it has none (`<Card foot>`) */
|
|
109
|
+
function exprOf(attr) {
|
|
90
110
|
const init = attr.initializer;
|
|
91
111
|
if (!init) return null;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
return ts.isJsxExpression(init) ? init.expression ?? null : init;
|
|
113
|
+
}
|
|
114
|
+
/** an expression as a child: a JSX element stands as it is, anything else in braces */
|
|
115
|
+
const asChild = (expr, sf) => (ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) || ts.isJsxFragment(expr) ? expr.getText(sf) : `{${expr.getText(sf)}}`);
|
|
116
|
+
/** a prop's value as a child, or null */
|
|
117
|
+
function childOf(attr, sf) {
|
|
118
|
+
const expr = exprOf(attr);
|
|
119
|
+
return expr ? asChild(expr, sf) : null;
|
|
96
120
|
}
|
|
97
121
|
/** `parts` laid on one line when they fit, else one to a line under `indent` */
|
|
98
122
|
function lay(open, parts, close, indent) {
|
|
@@ -120,6 +144,7 @@ function reshape(source, sf, el) {
|
|
|
120
144
|
const opening = openingOf(el), name = tagName(opening);
|
|
121
145
|
const rule = RESHAPES[name];
|
|
122
146
|
if (!rule) return null;
|
|
147
|
+
if (rule.line) return rowOf(source, sf, el);
|
|
123
148
|
const used = headProps(opening);
|
|
124
149
|
if (name === 'CardHead' && !used.length && ts.isJsxSelfClosingElement(el) === false) {
|
|
125
150
|
return { rename: true };
|
|
@@ -159,12 +184,19 @@ function moved(node, sf, text, to) {
|
|
|
159
184
|
return text.replace(/\n( *)/g, (m, sp) => `\n${' '.repeat(Math.max(0, sp.length + by))}`);
|
|
160
185
|
}
|
|
161
186
|
/** 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(
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
function footChildren(expr, sf, to) {
|
|
188
|
+
if (!expr) return [];
|
|
189
|
+
if (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));
|
|
190
|
+
return [ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr) ? moved(expr, sf, asChild(expr, sf), to) : asChild(expr, sf)];
|
|
191
|
+
}
|
|
192
|
+
/** a `foot` written under a condition, `cond ? x : undefined`, `cond ? x : null` or `cond && x`: the condition's text and `x`; a plain value has no `when` */
|
|
193
|
+
function footCondition(expr, sf) {
|
|
194
|
+
if (!expr) return { shown: null };
|
|
195
|
+
if (ts.isConditionalExpression(expr) && (expr.whenFalse.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(expr.whenFalse) && expr.whenFalse.text === 'undefined'))) {
|
|
196
|
+
return { when: (foot) => `{${expr.condition.getText(sf)} ? ${foot} : null}`, shown: expr.whenTrue };
|
|
197
|
+
}
|
|
198
|
+
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return { when: (foot) => `{${expr.left.getText(sf)} && ${foot}}`, shown: expr.right };
|
|
199
|
+
return { shown: expr };
|
|
168
200
|
}
|
|
169
201
|
/** 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
202
|
function footOf(source, sf, el) {
|
|
@@ -178,14 +210,42 @@ function footOf(source, sf, el) {
|
|
|
178
210
|
if (!fa && !la) return { pos: opening.getStart(sf), end: opening.end, text: self ? rest.replace(/>$/, ' />') : rest };
|
|
179
211
|
const round = !ra ? '' : hasAttr(opening, 'footRound') ? ' round' : ` round=${valueOf(ra, sf)}`;
|
|
180
212
|
const attrs = `${round}${la ? ` link=${valueOf(la, sf)}` : ''}`;
|
|
181
|
-
const
|
|
182
|
-
const
|
|
213
|
+
const { when, shown } = footCondition(fa ? exprOf(fa) : null, sf);
|
|
214
|
+
const kids = footChildren(shown, sf, indent.length + 4);
|
|
215
|
+
const drawn = kids.length ? lay(`<Foot${attrs}>`, kids, '</Foot>', `${indent} `) : `<Foot${attrs} />`;
|
|
216
|
+
const foot = when ? when(drawn) : drawn;
|
|
183
217
|
if (self) return { pos: el.getStart(sf), end: el.end, text: `${rest}\n${indent} ${foot}\n${indent}</Card>` };
|
|
184
218
|
const inner = source.slice(opening.end, el.closingElement.getStart(sf));
|
|
185
219
|
if (!inner.includes('\n')) return { pos: el.getStart(sf), end: el.end, text: lay(rest, [inner.trim(), foot].filter(Boolean), '</Card>', indent) };
|
|
186
220
|
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
221
|
}
|
|
188
222
|
|
|
223
|
+
/** a Row that still draws its line with props, as an edit that drops them and writes the line's children in order: lead · words · its own children · end */
|
|
224
|
+
function rowOf(source, sf, el) {
|
|
225
|
+
const opening = openingOf(el);
|
|
226
|
+
const used = rowProps(opening);
|
|
227
|
+
if (!used.length) return null;
|
|
228
|
+
const indent = lineIndent(source, opening.getStart(sf));
|
|
229
|
+
const at = (p) => findAttr(opening, p);
|
|
230
|
+
const attrs = (pairs) => pairs.filter(([from]) => at(from)).map(([from, to]) => ` ${to}=${valueOf(at(from), sf)}`).join('');
|
|
231
|
+
const words = attrs([['title', 'title'], ['meta', 'meta']]), body = at('body') ? childOf(at('body'), sf) : null;
|
|
232
|
+
const ends = attrs([['endValue', 'value'], ['endCaption', 'caption']]);
|
|
233
|
+
const front = [
|
|
234
|
+
at('leadIcon') && `<Lead icon=${valueOf(at('leadIcon'), sf)} />`,
|
|
235
|
+
at('lead') && childOf(at('lead'), sf),
|
|
236
|
+
(words || body) && (body ? `<Words${words}>${body}</Words>` : `<Words${words} />`),
|
|
237
|
+
].filter(Boolean);
|
|
238
|
+
const back = [ends && `<End${ends} />`, at('end') && childOf(at('end'), sf)].filter(Boolean);
|
|
239
|
+
const rest = without(source, opening, used, sf).replace(/\s*\/>$/, '>').replace(/\s*\n\s*>$/, '>').replace(/^<Row\s+>$/, '<Row>');
|
|
240
|
+
if (ts.isJsxSelfClosingElement(el)) return { pos: el.getStart(sf), end: el.end, text: lay(rest, [...front, ...back], '</Row>', indent) };
|
|
241
|
+
const inner = source.slice(opening.end, el.closingElement.getStart(sf));
|
|
242
|
+
if (!inner.includes('\n')) return { pos: el.getStart(sf), end: el.end, text: lay(rest, [...front, inner.trim(), ...back], '</Row>', indent) };
|
|
243
|
+
return { edits: [
|
|
244
|
+
{ pos: opening.getStart(sf), end: opening.end, text: front.length ? `${rest}\n${indent} ${front.join(`\n${indent} `)}` : rest },
|
|
245
|
+
{ pos: el.closingElement.getStart(sf), end: el.closingElement.getStart(sf), text: back.map((b) => ` ${b}\n${indent}`).join('') },
|
|
246
|
+
] };
|
|
247
|
+
}
|
|
248
|
+
|
|
189
249
|
/** the file rewritten: `source` after, `tags` rewritten, `imports` rewritten, and `left`, the tags no rule fits (`{ line, name, why }`) */
|
|
190
250
|
export function rewrite(source, file = 'x.tsx') {
|
|
191
251
|
let tags = 0, imports = 0;
|
|
@@ -272,34 +332,53 @@ function importList(names, wrapped) {
|
|
|
272
332
|
return `{\n${rows.map((r) => ` ${r},`).join('\n')}\n}`;
|
|
273
333
|
}
|
|
274
334
|
|
|
275
|
-
/** the `cardds`
|
|
335
|
+
/** the `cardds` imports after the tags changed: a retired name replaced by its new one in place, a part added to the value import where a tag now needs it,
|
|
336
|
+
* a name no tag writes any more dropped; an `import type` only has its retired type names renamed, and a file with no value import gets one after it */
|
|
276
337
|
function fixImports(source, file) {
|
|
277
338
|
const sf = parse(source, file);
|
|
278
339
|
const used = new Set();
|
|
279
340
|
const walk = (n) => { const name = tagName(n); if (name && !ts.isJsxClosingElement(n)) used.add(name); ts.forEachChild(n, walk); };
|
|
280
341
|
walk(sf);
|
|
281
342
|
const edits = [];
|
|
282
|
-
let imports = 0;
|
|
343
|
+
let imports = 0, values = 0, last = null;
|
|
344
|
+
const types = new Map();
|
|
345
|
+
const typeSpec = (e, name) => {
|
|
346
|
+
if (e.propertyName) return e.getText(sf);
|
|
347
|
+
if (TYPE_RENAMES[name]) types.set(name, TYPE_RENAMES[name]);
|
|
348
|
+
return `${e.isTypeOnly ? 'type ' : ''}${TYPE_RENAMES[name] ?? name}`;
|
|
349
|
+
};
|
|
283
350
|
for (const imp of sf.statements.filter(ts.isImportDeclaration)) {
|
|
284
351
|
const nb = imp.importClause?.namedBindings;
|
|
285
352
|
if (imp.moduleSpecifier.text !== 'cardds' || !nb || !ts.isNamedImports(nb)) continue;
|
|
353
|
+
last = imp;
|
|
354
|
+
const typeOnly = !!imp.importClause.isTypeOnly;
|
|
286
355
|
const names = [];
|
|
287
356
|
const add = (t) => { if (!names.includes(t)) names.push(t); };
|
|
288
357
|
for (const e of nb.elements) {
|
|
289
358
|
const name = (e.propertyName ?? e.name).text;
|
|
290
|
-
|
|
359
|
+
if (typeOnly || e.isTypeOnly) { add(typeSpec(e, name)); continue; }
|
|
360
|
+
const retired = !e.propertyName && retiredTo(name) && !used.has(name);
|
|
291
361
|
if (!retired) { add(e.getText(sf)); continue; }
|
|
292
|
-
const to = name
|
|
362
|
+
const to = retiredTo(name);
|
|
293
363
|
add(to);
|
|
294
364
|
if (to === 'Head' && used.has('Words')) add('Words');
|
|
295
365
|
}
|
|
296
|
-
for (const part of
|
|
366
|
+
if (!typeOnly) { values++; for (const part of PARTS) if (used.has(part) && !names.includes(part)) names.push(part); }
|
|
297
367
|
if (names.join(', ') === nb.elements.map((e) => e.getText(sf)).join(', ')) continue;
|
|
298
368
|
imports++;
|
|
299
369
|
edits.push({ pos: nb.getStart(sf), end: nb.end, text: importList(names, nb.getText(sf).includes('\n')) });
|
|
300
370
|
}
|
|
371
|
+
if (types.size) {
|
|
372
|
+
const rename = (n) => { if (ts.isIdentifier(n) && types.has(n.text)) edits.push({ pos: n.getStart(sf), end: n.end, text: types.get(n.text) }); ts.forEachChild(n, rename); };
|
|
373
|
+
for (const st of sf.statements) if (!ts.isImportDeclaration(st)) rename(st);
|
|
374
|
+
}
|
|
375
|
+
const needed = PARTS.filter((part) => used.has(part));
|
|
376
|
+
if (last && !values && needed.length) {
|
|
377
|
+
imports++;
|
|
378
|
+
edits.push({ pos: last.end, end: last.end, text: `\nimport ${importList(needed, false)} from ${last.moduleSpecifier.getText(sf)}${source[last.end - 1] === ';' ? ';' : ''}` });
|
|
379
|
+
}
|
|
301
380
|
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: [
|
|
381
|
+
return { source: apply(source, edits), imports, clash: [...PARTS, 'Body'].filter((n) => used.has(n) && declared.has(n)) };
|
|
303
382
|
}
|
|
304
383
|
|
|
305
384
|
/** the command: rewrite each file in place, or with `--check` name what still writes a retired name; the exit code */
|