@assure-one/design-system 1.36.0 → 1.38.0

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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * CM-13 — `Tooltip delayMs` → `delayDuration` (class A; plan §29 seq 2,
3
+ * registry `C-TOOLTIP-DELAY`).
4
+ *
5
+ * W5-12 made `delayMs` a deprecated alias of Radix's `delayDuration` on the
6
+ * design-system `Tooltip` (`TooltipProps` in `src/primitives/tooltip.tsx`):
7
+ * both name the same milliseconds and `delayDuration` wins when both are
8
+ * given, so the rename is behaviour-neutral and a pure rename — only the
9
+ * attribute's name is replaced; a literal or a dynamic `delayMs={x}` keeps
10
+ * its value and its formatting.
11
+ *
12
+ * | rule | registry | what happens |
13
+ * | --------------- | --------------- | ----------------------------------- |
14
+ * | `delay-renamed` | C-TOOLTIP-DELAY | `delayMs={…}` → `delayDuration={…}` |
15
+ *
16
+ * ## What it leaves alone, and lists under "could not be transformed"
17
+ *
18
+ * - An element that passes **both** `delayDuration` and `delayMs`:
19
+ * `delayDuration` already wins in the component, and a rename would make a
20
+ * duplicate attribute. A human deletes `delayMs`.
21
+ * - An element with a spread (`<Tooltip delayMs={300} {...props}>`): the
22
+ * spread may carry `delayDuration`, and which one wins depends on attribute
23
+ * order — renaming the literal could change it.
24
+ *
25
+ * Elements already on `delayDuration` are not touched (idempotency). Local
26
+ * components called Tooltip are not touched; test files are skipped, as by
27
+ * the scanner that measures C-TOOLTIP-DELAY.
28
+ */
29
+ import { analyseForms } from "../lib/forms.mjs";
30
+ import { applyEdits, openingOf, renameAttribute } from "../lib/jsx-edit.mjs";
31
+
32
+ export const meta = {
33
+ id: "CM-13",
34
+ title: "Tooltip: delayMs → delayDuration",
35
+ class: "A",
36
+ oneShot: false,
37
+ requires: { codemods: [], dsVersion: null },
38
+ parses: ["code"],
39
+ includeTests: false,
40
+ usesTypeScript: true,
41
+ usesPostcss: false,
42
+ registryIds: ["C-TOOLTIP-DELAY"],
43
+ };
44
+
45
+ /** The design-system component that accepts the deprecated `delayMs` (W5-12). */
46
+ export const DELAYED_COMPONENTS = new Set(["Tooltip"]);
47
+
48
+ export const LEGACY_PROP = "delayMs";
49
+ export const PROP = "delayDuration";
50
+
51
+ export function transform(file, { ts }) {
52
+ const facts = analyseForms(ts, file.source, file.rel);
53
+ const findings = [];
54
+ const notTransformed = [];
55
+ const edits = [];
56
+
57
+ for (const el of facts.elements) {
58
+ if (!el.isDs || !DELAYED_COMPONENTS.has(el.base) || el.component !== el.base) continue;
59
+ if (!el.props.has(LEGACY_PROP)) continue;
60
+
61
+ if (el.props.has(PROP)) {
62
+ notTransformed.push({
63
+ line: el.line,
64
+ reason: "both-names",
65
+ detail: `<${el.tag}> passes \`${LEGACY_PROP}\` and \`${PROP}\` — \`${PROP}\` already wins; delete \`${LEGACY_PROP}\` by hand`,
66
+ });
67
+ continue;
68
+ }
69
+ if (el.spread) {
70
+ notTransformed.push({
71
+ line: el.line,
72
+ reason: "spread-props",
73
+ detail: `<${el.tag} {…}> — the spread may carry \`${PROP}\`; rename by hand once you know it does not`,
74
+ });
75
+ continue;
76
+ }
77
+
78
+ const edit = renameAttribute(ts, facts.sf, openingOf(ts, el.node), LEGACY_PROP, PROP);
79
+ if (!edit) continue;
80
+ edits.push(edit);
81
+ findings.push({
82
+ line: el.props.get(LEGACY_PROP).line,
83
+ registryId: "C-TOOLTIP-DELAY",
84
+ rule: "delay-renamed",
85
+ match: `<${el.tag} ${LEGACY_PROP}>`,
86
+ component: el.component,
87
+ action: "applied",
88
+ gate: null,
89
+ detail: { from: LEGACY_PROP, to: PROP, value: el.props.get(LEGACY_PROP).text },
90
+ });
91
+ }
92
+
93
+ return {
94
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
95
+ findings,
96
+ notTransformed,
97
+ parseErrors: facts.parseErrors,
98
+ };
99
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * CM-18 — `DataTable*` presentational parts → `Table*` (class A; plan §29
3
+ * seq 12, registry `C-DT-PARTS`, lands with W6-03).
4
+ *
5
+ * W6-03 made the `DataTable*` table parts thin aliases of the `Table*` parts:
6
+ * one styling source, the same props. Where a part is a 1:1 copy the rename
7
+ * is mechanical and this codemod applies it — tag, closing tag and the named
8
+ * import:
9
+ *
10
+ * | from | to | note |
11
+ * | ----------------- | ------------- | ----------------------------------------------------------- |
12
+ * | `DataTableHead` | `TableHeader` | `<thead>` |
13
+ * | `DataTableBody` | `TableBody` | `<tbody>` |
14
+ * | `DataTableRow` | `TableRow` | `selected` is the same prop |
15
+ * | `DataTableCell` | `TableCell` | same props (`align`, `numeric`, `muted`, `truncate`, `width`)|
16
+ * | `DataTableHeader` | `TableHead` | plain, or controlled `sortable sort onSortChange` — `sortable` is dropped because `TableHead` is sortable whenever `sort` is passed |
17
+ *
18
+ * What changes for the rendered DOM: the `data-slot` attributes read
19
+ * `table-*` instead of `data-table-*`, and `TableHead` does not stamp
20
+ * `scope="col"` (a `<th>` inside `<thead>` is a column header by default).
21
+ * Classes, geometry and colours are the same — the `DataTable*` parts render
22
+ * through the `Table*` parts since W6-03.
23
+ *
24
+ * ## What it leaves alone, and lists under "could not be transformed"
25
+ *
26
+ * - `DataTableHeader sortable` **without** a controlled `sort` (uncontrolled
27
+ * sorting, `defaultSort`): `TableHead` has no uncontrolled sort state — the
28
+ * consumer wires `sort` from `useDataTable` or its own state.
29
+ * - `DataTableHeader sortable={expr}`: whether the header is sortable is not
30
+ * visible here.
31
+ * - `DataTableHeader {...props}`: the spread may carry `sortable`.
32
+ *
33
+ * Not touched at all (not parallel copies of a `Table*` part, they stay):
34
+ * `DataTable` (the card frame), `DataTableToolbar`, `DataTableSearch`,
35
+ * `DataTableSpacer`, `DataTableResultsCount`, `DataTableCellName` /
36
+ * `DataTableCellMono` / `DataTableCellId` / `DataTableCellDue`,
37
+ * `DataTableCheckbox`, `DataTablePagination`, and the `DataTable*Props` types.
38
+ * An aliased import (`DataTableCell as Cell`) and a namespace import are left
39
+ * alone too. The import specifier is rewritten only when no reference to the
40
+ * old name remains in the file (a `typeof DataTableCell` keeps it, and the
41
+ * new name is added next to it). Local components with the same names are
42
+ * not touched; test files are skipped, as by the scanner that measures
43
+ * C-DT-PARTS.
44
+ */
45
+ import { analyseForms } from "../lib/forms.mjs";
46
+ import { applyEdits, openingOf, removeAttribute } from "../lib/jsx-edit.mjs";
47
+
48
+ export const meta = {
49
+ id: "CM-18",
50
+ title: "DataTable* presentational parts → Table* (pure rename where the part is a 1:1 copy)",
51
+ class: "A",
52
+ oneShot: false,
53
+ requires: { codemods: [], dsVersion: null },
54
+ parses: ["code"],
55
+ includeTests: false,
56
+ usesTypeScript: true,
57
+ usesPostcss: false,
58
+ registryIds: ["C-DT-PARTS"],
59
+ };
60
+
61
+ /** `DataTable*` part → the `Table*` part it aliases (src/composites/data-table.tsx, W6-03). */
62
+ export const PART_MAP = {
63
+ DataTableHead: "TableHeader",
64
+ DataTableBody: "TableBody",
65
+ DataTableRow: "TableRow",
66
+ DataTableCell: "TableCell",
67
+ DataTableHeader: "TableHead",
68
+ };
69
+
70
+ export const RULES = {
71
+ "part-renamed": { action: "applied", severity: "low" },
72
+ };
73
+
74
+ const DS_PACKAGE = "@assure-one/design-system";
75
+
76
+ /**
77
+ * Identifier references to `name` outside its import specifier and outside
78
+ * JSX tag names — `typeof DataTableCell`, `as={DataTableCell}`, a plain
79
+ * value use. While one exists the old import must stay.
80
+ */
81
+ function hasOtherReferences(ts, sf, name) {
82
+ let found = false;
83
+ const visit = (node) => {
84
+ if (found) return;
85
+ if (ts.isIdentifier(node) && node.text === name) {
86
+ const parent = node.parent;
87
+ const isImport = parent && ts.isImportSpecifier(parent);
88
+ const isTag =
89
+ parent &&
90
+ (ts.isJsxOpeningElement(parent) ||
91
+ ts.isJsxSelfClosingElement(parent) ||
92
+ ts.isJsxClosingElement(parent)) &&
93
+ parent.tagName === node;
94
+ if (!isImport && !isTag) found = true;
95
+ return;
96
+ }
97
+ ts.forEachChild(node, visit);
98
+ };
99
+ visit(sf);
100
+ return found;
101
+ }
102
+
103
+ export function transform(file, { ts }) {
104
+ const facts = analyseForms(ts, file.source, file.rel);
105
+ const sf = facts.sf;
106
+ const findings = [];
107
+ const notTransformed = [];
108
+ const edits = [];
109
+ /** old local name → new name, for the elements that were renamed */
110
+ const renamed = new Map();
111
+ /** old local names that still have an element on them */
112
+ const remaining = new Set();
113
+
114
+ for (const el of facts.elements) {
115
+ if (!el.isDs || !PART_MAP[el.base] || el.component !== el.base) continue;
116
+ // An aliased import (`DataTableCell as Cell`): the local name is not the part's.
117
+ if (el.tag !== el.base) {
118
+ remaining.add(el.tag);
119
+ continue;
120
+ }
121
+ const to = PART_MAP[el.base];
122
+ const opening = openingOf(ts, el.node);
123
+ const inner = [];
124
+ let sortableDropped = false;
125
+
126
+ if (el.base === "DataTableHeader") {
127
+ const sortable = el.props.get("sortable");
128
+ if (el.spread) {
129
+ notTransformed.push({
130
+ line: el.line,
131
+ reason: "spread-props",
132
+ detail: `<${el.tag} {…}> — the spread may carry \`sortable\`; rename to \`TableHead\` by hand once you know it does not`,
133
+ });
134
+ remaining.add(el.tag);
135
+ continue;
136
+ }
137
+ if (sortable) {
138
+ if (sortable.text !== "true") {
139
+ notTransformed.push({
140
+ line: el.line,
141
+ reason: "dynamic-sortable",
142
+ detail: `<${el.tag} sortable={…}> — whether the header sorts is not visible here; \`TableHead\` sorts whenever \`sort\` is passed`,
143
+ });
144
+ remaining.add(el.tag);
145
+ continue;
146
+ }
147
+ if (!el.props.has("sort") || el.props.has("defaultSort")) {
148
+ notTransformed.push({
149
+ line: el.line,
150
+ reason: "uncontrolled-sort",
151
+ detail: `<${el.tag} sortable> without a controlled \`sort\` — \`TableHead\` has no uncontrolled sort state; wire \`sort\` + \`onSortChange\` (from \`useDataTable\`) first`,
152
+ });
153
+ remaining.add(el.tag);
154
+ continue;
155
+ }
156
+ const drop = removeAttribute(ts, sf, opening, "sortable");
157
+ if (drop) {
158
+ inner.push(drop);
159
+ sortableDropped = true;
160
+ }
161
+ } else if (el.props.has("defaultSort")) {
162
+ notTransformed.push({
163
+ line: el.line,
164
+ reason: "uncontrolled-sort",
165
+ detail: `<${el.tag} defaultSort> — \`TableHead\` has no uncontrolled sort state`,
166
+ });
167
+ remaining.add(el.tag);
168
+ continue;
169
+ }
170
+ }
171
+
172
+ inner.push({ pos: opening.tagName.getStart(sf), end: opening.tagName.getEnd(), text: to });
173
+ if (ts.isJsxElement(el.node)) {
174
+ const closing = el.node.closingElement.tagName;
175
+ inner.push({ pos: closing.getStart(sf), end: closing.getEnd(), text: to });
176
+ }
177
+ edits.push(...inner);
178
+ renamed.set(el.base, to);
179
+ findings.push({
180
+ line: el.line,
181
+ registryId: "C-DT-PARTS",
182
+ rule: "part-renamed",
183
+ match: `<${el.tag}>`,
184
+ component: el.component,
185
+ action: "applied",
186
+ gate: null,
187
+ severity: RULES["part-renamed"].severity,
188
+ detail: { from: el.base, to, sortableDropped },
189
+ });
190
+ }
191
+
192
+ // The import: each renamed name becomes its `Table*` part when nothing else
193
+ // in the file still refers to it; otherwise the new name is added next to it.
194
+ if (renamed.size) {
195
+ for (const stmt of sf.statements) {
196
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
197
+ const spec = stmt.moduleSpecifier.text;
198
+ if (spec !== DS_PACKAGE && !spec.startsWith(`${DS_PACKAGE}/`)) continue;
199
+ const bindings = stmt.importClause?.namedBindings;
200
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
201
+ const present = new Set(bindings.elements.map((e) => e.name.text));
202
+ const list = bindings.elements;
203
+ for (const [from, to] of renamed) {
204
+ const index = list.findIndex((e) => !e.propertyName && e.name.text === from);
205
+ if (index < 0) continue;
206
+ const element = list[index];
207
+ const keep = remaining.has(from) || hasOtherReferences(ts, sf, from);
208
+ if (keep) {
209
+ if (!present.has(to)) {
210
+ edits.push({ pos: element.getEnd(), text: `, ${to}` });
211
+ present.add(to);
212
+ }
213
+ continue;
214
+ }
215
+ if (present.has(to)) {
216
+ // `TableCell` is already imported: the old specifier goes.
217
+ const fromPos = index === 0 ? element.getStart(sf) : list[index - 1].getEnd();
218
+ const toPos = index === 0 && list.length > 1 ? list[1].getStart(sf) : element.getEnd();
219
+ edits.push({ pos: fromPos, end: toPos, text: "" });
220
+ } else {
221
+ edits.push({ pos: element.getStart(sf), end: element.getEnd(), text: to });
222
+ present.add(to);
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ return {
229
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
230
+ findings,
231
+ notTransformed,
232
+ parseErrors: facts.parseErrors,
233
+ };
234
+ }