@assure-one/design-system 1.34.0 → 1.36.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.
Files changed (35) hide show
  1. package/README.md +53 -0
  2. package/codemods/README.md +225 -10
  3. package/codemods/lib/jsx-edit.mjs +63 -4
  4. package/codemods/lib/registry.mjs +7 -0
  5. package/codemods/transforms/cm-04-button-variant-intent.mjs +223 -0
  6. package/codemods/transforms/cm-05-button-icon-slots.mjs +117 -0
  7. package/codemods/transforms/cm-06-tone-to-intent.mjs +190 -0
  8. package/codemods/transforms/cm-07-input-size.mjs +104 -0
  9. package/codemods/transforms/cm-08-search-select-to-combobox.mjs +236 -0
  10. package/codemods/transforms/cm-10-date-picker-value-change.mjs +416 -0
  11. package/codemods/transforms/cm-14-hidden-mirrors.mjs +2 -2
  12. package/codemods/transforms/cm-19-progress-explicit-intent.mjs +192 -0
  13. package/dist/css/components.css +1 -1
  14. package/dist/css/legacy-aliases.css +5 -0
  15. package/dist/css/tokens.css +5 -0
  16. package/dist/design-system-provider-cPUklDJv.d.ts +220 -0
  17. package/dist/icons/index.d.ts +3 -0
  18. package/dist/icons/index.js +1833 -0
  19. package/dist/icons/index.js.map +1 -0
  20. package/dist/index-CQwzTm0v.d.ts +509 -0
  21. package/dist/index.d.ts +2967 -823
  22. package/dist/index.js +7644 -3062
  23. package/dist/index.js.map +1 -1
  24. package/dist/next/index.d.ts +20 -0
  25. package/dist/next/index.js +16 -0
  26. package/dist/next/index.js.map +1 -0
  27. package/dist/styles.css +1 -1
  28. package/dist/testing/index.cjs +133 -11
  29. package/dist/testing/index.d.cts +98 -2
  30. package/dist/testing/index.d.ts +98 -2
  31. package/dist/testing/index.js +132 -12
  32. package/docs/components.md +601 -0
  33. package/docs/components.registry.json +1586 -0
  34. package/docs/for-ai-agents.md +153 -0
  35. package/package.json +21 -3
@@ -0,0 +1,223 @@
1
+ /**
2
+ * CM-04 — Button family: legacy single-axis `variant` → the `variant` +
3
+ * `intent` pair (class R; plan §29 seq 5, registry `C-BTN-VAR`, `C-BTN-ACCENT`).
4
+ *
5
+ * W3-02 gave `Button`, `LinkButton` and `SubmitButton` the two axes of
6
+ * ADR-007 — `variant` is the emphasis, `intent` the meaning — and kept the
7
+ * single-axis words as deprecated aliases, each a fixed pair
8
+ * (`LEGACY_BUTTON_VARIANTS` in `src/primitives/behavior/button-base.ts`,
9
+ * ADR-007 §4). Every alias renders the exact classes it always did, so the
10
+ * rewrite is pixel-neutral:
11
+ *
12
+ * | legacy word | becomes | `intent` written? |
13
+ * | ------------- | ------------------------------------ | ---------------------------------------- |
14
+ * | `primary` | `variant="solid"` | no — `brand` is what `solid` defaults to |
15
+ * | `secondary` | `variant="soft"` | no — `neutral` is `soft`'s default |
16
+ * | `destructive` | `variant="solid" intent="danger"` | yes |
17
+ * | `success` | `variant="solid" intent="success"` | yes |
18
+ * | `accent` | left alone, reported | its suite-action colour has no pair (D13) |
19
+ * | `dashed` | left alone, reported | its dashed border has no pair (2.x alias) |
20
+ *
21
+ * `intent` is omitted where it equals what the new emphasis resolves on its
22
+ * own (`DEFAULT_BUTTON_INTENT`), so the output is the smallest call that
23
+ * renders today's pixels. `ghost`, `outline` and `link` are emphases already
24
+ * and are not touched.
25
+ *
26
+ * ## What it reports
27
+ *
28
+ * | rule | action | severity | why |
29
+ * | ----------------------- | ------- | -------- | ---------------------------------------------------------------------------- |
30
+ * | `variant-split` | applied | low | the alias became its pair (or its emphasis alone, when the intent is the default) |
31
+ * | `destructive-as-danger` | applied | medium | `destructive` → `solid` + `danger`: the word moves to D4's default (`danger` canonical, ADR-007 §5); a human confirms |
32
+ * | `legacy-look` | review | medium | `accent` (D13) and `dashed` have a look no pair renders; they stay until decided / 2.x |
33
+ * | `dynamic-intent` | review | medium | a legacy word next to `intent={expr}` whose alias intent is not the new emphasis's default: when the expression is `undefined` the alias renders its own intent, the new word its default |
34
+ * | `dynamic-variant` | review | high | `variant={expr}`: the value is not visible here; map it by hand |
35
+ * | `spread-props` | review | medium | `{...props}` may carry `variant` or `intent`; rewriting the literal could change which wins |
36
+ * | `unknown-variant` | review | high | a word the Button never accepted |
37
+ *
38
+ * A legacy word next to a **literal** `intent` (`variant="destructive"
39
+ * intent="success"`) is rewritten to its emphasis only: the intent already
40
+ * wins on both sides of the rewrite (ADR-007 §4, `resolveButtonStyle`).
41
+ * `intent="destructive"` is not touched — that is CM-17, gated by D4.
42
+ *
43
+ * Elements that already speak the new words are not touched (idempotency).
44
+ * Local components with the same names are not touched; test files are
45
+ * skipped, as by the scanner that measures C-BTN-VAR.
46
+ */
47
+ import { analyseForms } from "../lib/forms.mjs";
48
+ import { applyEdits, insertAttribute, openingOf, replaceAttribute } from "../lib/jsx-edit.mjs";
49
+
50
+ export const meta = {
51
+ id: "CM-04",
52
+ title: "Button family: legacy variant → variant + intent",
53
+ class: "R",
54
+ oneShot: false,
55
+ requires: { codemods: [], dsVersion: null },
56
+ parses: ["code"],
57
+ includeTests: false,
58
+ usesTypeScript: true,
59
+ usesPostcss: false,
60
+ registryIds: ["C-BTN-VAR", "C-BTN-ACCENT"],
61
+ };
62
+
63
+ /** The design-system components that read `buttonVariants` (W3-02). */
64
+ export const BUTTONS = new Set(["Button", "LinkButton", "SubmitButton"]);
65
+
66
+ /**
67
+ * The legacy alias map — `LEGACY_BUTTON_VARIANTS` of
68
+ * `src/primitives/behavior/button-base.ts`, row for row (ADR-007 §4). A row
69
+ * with a `look` is a legacy-only look: no pair renders it, so the word stays.
70
+ * `tests/codemods/cm-04.test.mjs` checks this table against the source.
71
+ */
72
+ export const LEGACY_VARIANTS = {
73
+ primary: { variant: "solid", intent: "brand" },
74
+ secondary: { variant: "soft", intent: "neutral" },
75
+ destructive: { variant: "solid", intent: "danger" },
76
+ success: { variant: "solid", intent: "success" },
77
+ accent: { variant: "solid", intent: "brand", look: "accent" },
78
+ dashed: { variant: "outline", intent: "neutral", look: "dashed" },
79
+ };
80
+
81
+ /**
82
+ * The intent each emphasis renders when only `variant` is passed —
83
+ * `DEFAULT_BUTTON_INTENT` of button-base.ts (ADR-007 §3). Where the alias
84
+ * intent equals this, `intent` is omitted from the output.
85
+ */
86
+ export const DEFAULT_INTENT = {
87
+ solid: "brand",
88
+ soft: "neutral",
89
+ outline: "neutral",
90
+ ghost: "neutral",
91
+ link: "brand",
92
+ };
93
+
94
+ /** The emphases: already vocabulary, never rewritten. */
95
+ export const EMPHASES = new Set(Object.keys(DEFAULT_INTENT));
96
+
97
+ /** The registry contract a legacy word belongs to. */
98
+ export const contractOf = (word) => (word === "accent" ? "C-BTN-ACCENT" : "C-BTN-VAR");
99
+
100
+ /**
101
+ * What a literal legacy `variant` becomes, or null when the word is not a
102
+ * legacy alias: `{ rule, action, severity, variant, intent }`, `intent` being
103
+ * `null` when it is the new emphasis's default and is left out.
104
+ */
105
+ export function classifyVariant(word) {
106
+ const pair = LEGACY_VARIANTS[word];
107
+ if (!pair) return null;
108
+ if (pair.look) return { rule: "legacy-look", action: "review", severity: "medium" };
109
+ const intent = pair.intent === DEFAULT_INTENT[pair.variant] ? null : pair.intent;
110
+ if (word === "destructive") {
111
+ return {
112
+ rule: "destructive-as-danger",
113
+ action: "applied",
114
+ severity: "medium",
115
+ variant: pair.variant,
116
+ intent,
117
+ };
118
+ }
119
+ return {
120
+ rule: "variant-split",
121
+ action: "applied",
122
+ severity: "low",
123
+ variant: pair.variant,
124
+ intent,
125
+ };
126
+ }
127
+
128
+ export function transform(file, { ts }) {
129
+ const facts = analyseForms(ts, file.source, file.rel);
130
+ const findings = [];
131
+ const edits = [];
132
+
133
+ for (const el of facts.elements) {
134
+ if (!el.isDs || !BUTTONS.has(el.base) || el.component !== el.base) continue;
135
+ const variant = el.props.get("variant");
136
+ if (!variant) continue;
137
+ const word = variant.expression || variant.literals.length !== 1 ? null : variant.literals[0];
138
+ if (word !== null && EMPHASES.has(word)) continue;
139
+
140
+ const intentAttr = el.props.get("intent") ?? null;
141
+ const base = {
142
+ line: el.line,
143
+ registryId: contractOf(word),
144
+ match: `<${el.tag} variant=${variant.text ?? ""}>`,
145
+ component: el.component,
146
+ gate: null,
147
+ detail: {
148
+ variant: word,
149
+ dynamic: variant.expression,
150
+ intent: intentAttr
151
+ ? intentAttr.expression
152
+ ? null
153
+ : (intentAttr.literals[0] ?? null)
154
+ : undefined,
155
+ },
156
+ };
157
+ const review = (rule, severity) => findings.push({ ...base, rule, severity, action: "review" });
158
+
159
+ if (word === null) {
160
+ review("dynamic-variant", "high");
161
+ continue;
162
+ }
163
+ if (el.spread) {
164
+ review("spread-props", "medium");
165
+ continue;
166
+ }
167
+ const outcome = classifyVariant(word);
168
+ if (!outcome) {
169
+ review("unknown-variant", "high");
170
+ continue;
171
+ }
172
+ if (outcome.action === "review") {
173
+ review(outcome.rule, outcome.severity);
174
+ continue;
175
+ }
176
+ // A dynamic `intent` may be `undefined` at runtime, in which case the
177
+ // alias renders its own intent and the new emphasis its default: only
178
+ // safe when the two agree.
179
+ if (intentAttr && intentAttr.expression && outcome.intent !== null) {
180
+ review("dynamic-intent", "medium");
181
+ continue;
182
+ }
183
+
184
+ const opening = openingOf(ts, el.node);
185
+ const replaced = replaceAttribute(
186
+ ts,
187
+ facts.sf,
188
+ opening,
189
+ "variant",
190
+ `variant="${outcome.variant}"`,
191
+ );
192
+ if (!replaced) {
193
+ review("dynamic-variant", "high");
194
+ continue;
195
+ }
196
+ edits.push(replaced);
197
+ const writesIntent = outcome.intent !== null && !intentAttr;
198
+ if (writesIntent) {
199
+ edits.push(
200
+ insertAttribute(ts, facts.sf, opening, `intent="${outcome.intent}"`, { after: "variant" }),
201
+ );
202
+ }
203
+ findings.push({
204
+ ...base,
205
+ rule: outcome.rule,
206
+ severity: outcome.severity,
207
+ action: "applied",
208
+ detail: {
209
+ ...base.detail,
210
+ variant: word,
211
+ emphasis: outcome.variant,
212
+ intent: writesIntent ? outcome.intent : base.detail.intent,
213
+ },
214
+ });
215
+ }
216
+
217
+ return {
218
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
219
+ findings,
220
+ notTransformed: [],
221
+ parseErrors: facts.parseErrors,
222
+ };
223
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * CM-05 — Button family: `iconLeft` / `iconRight` → `iconStart` / `iconEnd`
3
+ * (class A; plan §29 seq 1, registry `C-BTN-ICONPROPS`).
4
+ *
5
+ * W3-07 named the two icon slots of `Button`, `LinkButton` and `SubmitButton`
6
+ * logically — `iconStart` is the left in LTR and the right in RTL — and kept
7
+ * `iconLeft` / `iconRight` as deprecated aliases of the same slots
8
+ * (`ButtonIconProps`, `resolveButtonIcons` in
9
+ * `src/primitives/behavior/button-base.ts`). Each alias is the same slot under
10
+ * another name, so the rename is a pure rename: only the attribute's name is
11
+ * replaced and its value — a one-line icon or a multi-line element — keeps its
12
+ * text and its formatting.
13
+ *
14
+ * | rule | registry | what happens |
15
+ * | -------------- | -------------- | ----------------------------------------- |
16
+ * | `icon-renamed` | C-BTN-ICONPROPS | `iconLeft` → `iconStart`, `iconRight` → `iconEnd` |
17
+ *
18
+ * ## What it leaves alone, and lists under "could not be transformed"
19
+ *
20
+ * - An element that passes **both names of one side** (`iconStart` and
21
+ * `iconLeft`): the component throws on that in development — two names,
22
+ * one slot — and a rename would turn the error into a duplicate attribute.
23
+ * Neither side of that element is touched; a human deletes one.
24
+ * - An element with a spread (`<Button iconLeft={…} {...props}>`): the spread
25
+ * may carry either name for the same slot, and which one wins depends on
26
+ * the order of the attributes — renaming the literal could change it.
27
+ *
28
+ * Elements that already use the new names are not touched (idempotency).
29
+ * Local components with the same names are not touched; test files are
30
+ * skipped, as by the scanner that measures C-BTN-ICONPROPS.
31
+ */
32
+ import { analyseForms } from "../lib/forms.mjs";
33
+ import { applyEdits, openingOf, renameAttribute } from "../lib/jsx-edit.mjs";
34
+
35
+ export const meta = {
36
+ id: "CM-05",
37
+ title: "Button family: iconLeft/iconRight → iconStart/iconEnd",
38
+ class: "A",
39
+ oneShot: false,
40
+ requires: { codemods: [], dsVersion: null },
41
+ parses: ["code"],
42
+ includeTests: false,
43
+ usesTypeScript: true,
44
+ usesPostcss: false,
45
+ registryIds: ["C-BTN-ICONPROPS"],
46
+ };
47
+
48
+ /** The design-system components with the two icon slots (W3-07). */
49
+ export const ICON_BUTTONS = new Set(["Button", "LinkButton", "SubmitButton"]);
50
+
51
+ /**
52
+ * Deprecated alias → the slot it names (`ButtonIconProps` of button-base.ts).
53
+ * `tests/codemods/cm-05.test.mjs` checks it against the source.
54
+ */
55
+ export const ICON_SLOTS = {
56
+ iconLeft: "iconStart",
57
+ iconRight: "iconEnd",
58
+ };
59
+
60
+ export function transform(file, { ts }) {
61
+ const facts = analyseForms(ts, file.source, file.rel);
62
+ const findings = [];
63
+ const notTransformed = [];
64
+ const edits = [];
65
+
66
+ for (const el of facts.elements) {
67
+ if (!el.isDs || !ICON_BUTTONS.has(el.base) || el.component !== el.base) continue;
68
+ const legacy = Object.keys(ICON_SLOTS).filter((name) => el.props.has(name));
69
+ if (!legacy.length) continue;
70
+
71
+ const clashes = legacy.filter((name) => el.props.has(ICON_SLOTS[name]));
72
+ if (clashes.length) {
73
+ notTransformed.push({
74
+ line: el.line,
75
+ reason: "both-names",
76
+ detail: `<${el.tag}> passes ${clashes
77
+ .map((name) => `\`${name}\` and \`${ICON_SLOTS[name]}\``)
78
+ .join(" and ")} — one slot, two names; delete the deprecated one by hand`,
79
+ });
80
+ continue;
81
+ }
82
+ if (el.spread) {
83
+ notTransformed.push({
84
+ line: el.line,
85
+ reason: "spread-props",
86
+ detail: `<${el.tag} {…}> — the spread may carry \`${legacy
87
+ .map((name) => ICON_SLOTS[name])
88
+ .join("\` or \`")}\` for the same slot; rename by hand once you know it does not`,
89
+ });
90
+ continue;
91
+ }
92
+
93
+ const opening = openingOf(ts, el.node);
94
+ for (const name of legacy) {
95
+ const edit = renameAttribute(ts, facts.sf, opening, name, ICON_SLOTS[name]);
96
+ if (!edit) continue;
97
+ edits.push(edit);
98
+ findings.push({
99
+ line: el.props.get(name).line,
100
+ registryId: "C-BTN-ICONPROPS",
101
+ rule: "icon-renamed",
102
+ match: `<${el.tag} ${name}>`,
103
+ component: el.component,
104
+ action: "applied",
105
+ gate: null,
106
+ detail: { from: name, to: ICON_SLOTS[name] },
107
+ });
108
+ }
109
+ }
110
+
111
+ return {
112
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
113
+ findings,
114
+ notTransformed,
115
+ parseErrors: facts.parseErrors,
116
+ };
117
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * CM-06 — `tone=` → `intent=` on StatusDot, IconTile, Spinner, SegmentedProgress
3
+ * and SuiteProgress (class R; plan §29 seq 6, registry `C-TONE`).
4
+ *
5
+ * W3-21 gave the five components the shared `intent` axis of ADR-007 and kept
6
+ * `tone` as a deprecated alias. Every legacy word resolves to one intent and
7
+ * renders the same classes, so the rename is pixel-neutral; the value map is
8
+ * the components' own alias tables (`STATUS_DOT_TONE_TO_INTENT`, …):
9
+ *
10
+ * | component | `tone` → `intent` |
11
+ * | ----------------- | -------------------------------------------------------------------------- |
12
+ * | StatusDot | `pro` → `brand`; `danger` `warning` `info` `success` `neutral` unchanged |
13
+ * | IconTile | `pro` → `brand`; status words unchanged; `audit` `books` `tax` → review |
14
+ * | Spinner | `muted` → `neutral`, `accent` → `info`, `destructive` → `danger`, `current` → removed (the default); `success` `warning` unchanged |
15
+ * | SegmentedProgress | `default` → `brand`, `destructive` → `danger`; `success` `warning` `info` unchanged; `tax` `audit` `accounting` → review |
16
+ * | SuiteProgress | `pro` → `brand`; `audit` `books` `tax` → review |
17
+ *
18
+ * ## What it reports
19
+ *
20
+ * | rule | action | severity | why |
21
+ * | --------------- | ------- | -------- | ---------------------------------------------------------------------------- |
22
+ * | `tone-renamed` | applied | low | same word on the new axis (or `default` → `brand`, `destructive` → `danger`) |
23
+ * | `pro-as-brand` | applied | medium | `pro` is the brand colour — unless the call site meant "generic purple" (Portal's firm-branded context); a human confirms |
24
+ * | `tone-removed` | applied | low | `tone="current"` on Spinner is the default; the attribute goes |
25
+ * | `product-tone` | review | medium | `audit` `books` `tax` `accounting` are product hues, never intents (ADR-007 §2): they stay on `tone` until the products own a service → colour map |
26
+ * | `dynamic-tone` | review | high | `tone={expr}`: the value is not visible here; map it by hand |
27
+ * | `spread-props` | review | medium | `{...props}` may carry `tone` too; renaming the literal could change which one wins |
28
+ * | `has-intent` | review | low | both axes present; `intent` already wins, delete `tone` by hand |
29
+ * | `unknown-tone` | review | high | a word the component never accepted |
30
+ *
31
+ * Elements that already speak `intent` and have no `tone` are not touched
32
+ * (idempotency). Local components with the same names are not touched; test
33
+ * files are skipped, as by the scanner that measures C-TONE.
34
+ */
35
+ import { analyseForms } from "../lib/forms.mjs";
36
+ import { applyEdits, openingOf, removeAttribute, replaceAttribute } from "../lib/jsx-edit.mjs";
37
+
38
+ export const meta = {
39
+ id: "CM-06",
40
+ title: "tone → intent on StatusDot, IconTile, Spinner, SegmentedProgress and SuiteProgress",
41
+ class: "R",
42
+ oneShot: false,
43
+ requires: { codemods: [], dsVersion: null },
44
+ parses: ["code"],
45
+ includeTests: false,
46
+ usesTypeScript: true,
47
+ usesPostcss: false,
48
+ registryIds: ["C-TONE"],
49
+ };
50
+
51
+ /** Marker: the legacy word has no intent and stays on `tone` (a product hue). */
52
+ export const PRODUCT = Symbol("product-tone");
53
+ /** Marker: the legacy word is the default and the attribute is simply removed. */
54
+ export const REMOVE = Symbol("remove");
55
+
56
+ /**
57
+ * `tone` → `intent` per component (mirrors the alias tables in
58
+ * src/primitives/{status-dot,icon-tile,spinner,segmented-progress,suite-progress}.tsx).
59
+ */
60
+ export const TONE_MAP = {
61
+ StatusDot: {
62
+ danger: "danger",
63
+ warning: "warning",
64
+ info: "info",
65
+ success: "success",
66
+ pro: "brand",
67
+ neutral: "neutral",
68
+ },
69
+ IconTile: {
70
+ pro: "brand",
71
+ neutral: "neutral",
72
+ info: "info",
73
+ success: "success",
74
+ warning: "warning",
75
+ danger: "danger",
76
+ audit: PRODUCT,
77
+ books: PRODUCT,
78
+ tax: PRODUCT,
79
+ },
80
+ Spinner: {
81
+ current: REMOVE,
82
+ muted: "neutral",
83
+ accent: "info",
84
+ success: "success",
85
+ warning: "warning",
86
+ destructive: "danger",
87
+ },
88
+ SegmentedProgress: {
89
+ default: "brand",
90
+ success: "success",
91
+ warning: "warning",
92
+ destructive: "danger",
93
+ info: "info",
94
+ tax: PRODUCT,
95
+ audit: PRODUCT,
96
+ accounting: PRODUCT,
97
+ },
98
+ SuiteProgress: {
99
+ pro: "brand",
100
+ audit: PRODUCT,
101
+ books: PRODUCT,
102
+ tax: PRODUCT,
103
+ },
104
+ };
105
+
106
+ export const TONED_COMPONENTS = new Set(Object.keys(TONE_MAP));
107
+
108
+ /** The rule a literal tone falls under, or null when the word is unknown. */
109
+ export function classifyTone(component, tone) {
110
+ const target = TONE_MAP[component]?.[tone];
111
+ if (target === undefined) return null;
112
+ if (target === PRODUCT) return { rule: "product-tone", action: "review", severity: "medium" };
113
+ if (target === REMOVE)
114
+ return { rule: "tone-removed", action: "applied", severity: "low", intent: null };
115
+ if (tone === "pro")
116
+ return { rule: "pro-as-brand", action: "applied", severity: "medium", intent: target };
117
+ return { rule: "tone-renamed", action: "applied", severity: "low", intent: target };
118
+ }
119
+
120
+ export function transform(file, { ts }) {
121
+ const facts = analyseForms(ts, file.source, file.rel);
122
+ const findings = [];
123
+ const edits = [];
124
+
125
+ for (const el of facts.elements) {
126
+ if (!el.isDs || !TONED_COMPONENTS.has(el.base) || el.component !== el.base) continue;
127
+ const tone = el.props.get("tone");
128
+ if (!tone) continue;
129
+
130
+ const base = {
131
+ line: el.line,
132
+ registryId: "C-TONE",
133
+ match: `<${el.tag} tone=${tone.text ?? ""}>`,
134
+ component: el.component,
135
+ gate: null,
136
+ detail: {
137
+ tone: tone.expression ? null : (tone.literals[0] ?? null),
138
+ dynamic: tone.expression,
139
+ },
140
+ };
141
+ const review = (rule, severity) => findings.push({ ...base, rule, severity, action: "review" });
142
+
143
+ if (tone.expression || tone.literals.length !== 1) {
144
+ review("dynamic-tone", "high");
145
+ continue;
146
+ }
147
+ if (el.spread) {
148
+ review("spread-props", "medium");
149
+ continue;
150
+ }
151
+ if (el.props.has("intent")) {
152
+ review("has-intent", "low");
153
+ continue;
154
+ }
155
+ const word = tone.literals[0];
156
+ const outcome = classifyTone(el.base, word);
157
+ if (!outcome) {
158
+ review("unknown-tone", "high");
159
+ continue;
160
+ }
161
+ if (outcome.action === "review") {
162
+ review(outcome.rule, outcome.severity);
163
+ continue;
164
+ }
165
+ const opening = openingOf(ts, el.node);
166
+ const edit =
167
+ outcome.intent === null
168
+ ? removeAttribute(ts, facts.sf, opening, "tone")
169
+ : replaceAttribute(ts, facts.sf, opening, "tone", `intent="${outcome.intent}"`);
170
+ if (!edit) {
171
+ review("dynamic-tone", "high");
172
+ continue;
173
+ }
174
+ edits.push(edit);
175
+ findings.push({
176
+ ...base,
177
+ rule: outcome.rule,
178
+ severity: outcome.severity,
179
+ action: "applied",
180
+ detail: { ...base.detail, intent: outcome.intent },
181
+ });
182
+ }
183
+
184
+ return {
185
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
186
+ findings,
187
+ notTransformed: [],
188
+ parseErrors: facts.parseErrors,
189
+ };
190
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * CM-07 — `inputSize` → `size` on `Input`, `Textarea` and `SearchInput`
3
+ * (class A; plan §29 seq 3, registry `C-INPUT-SIZE`).
4
+ *
5
+ * W4-05/06/07 put the three text controls on the control scale: `size` is the
6
+ * prop, `inputSize` the deprecated spelling of the same value
7
+ * (`InputVariantsOptions`, `TextareaProps`, `SearchInputProps`). Every legacy
8
+ * word renders the classes it always did and `size` wins when both are
9
+ * given, so the rename is pixel-neutral and a pure rename: only the
10
+ * attribute's name is replaced — a literal or a dynamic `inputSize={x}` keeps
11
+ * its value and its formatting.
12
+ *
13
+ * | rule | registry | what happens |
14
+ * | -------------- | ------------ | ------------------------------------- |
15
+ * | `size-renamed` | C-INPUT-SIZE | `inputSize={…}` → `size={…}` |
16
+ *
17
+ * ## What it leaves alone, and lists under "could not be transformed"
18
+ *
19
+ * - An element that passes **both** `size` and `inputSize`: `size` already
20
+ * wins in the component, and a rename would make a duplicate attribute.
21
+ * A human deletes `inputSize`.
22
+ * - An element with a spread (`<Input inputSize="sm" {...props}>`): the
23
+ * spread may carry `size`, and which one wins depends on attribute order —
24
+ * renaming the literal could change it.
25
+ *
26
+ * Elements already on `size` are not touched (idempotency). Local components
27
+ * with the same names are not touched; test files are skipped, as by the
28
+ * scanner that measures C-INPUT-SIZE.
29
+ */
30
+ import { analyseForms } from "../lib/forms.mjs";
31
+ import { applyEdits, openingOf, renameAttribute } from "../lib/jsx-edit.mjs";
32
+
33
+ export const meta = {
34
+ id: "CM-07",
35
+ title: "Input, Textarea, SearchInput: inputSize → size",
36
+ class: "A",
37
+ oneShot: false,
38
+ requires: { codemods: [], dsVersion: null },
39
+ parses: ["code"],
40
+ includeTests: false,
41
+ usesTypeScript: true,
42
+ usesPostcss: false,
43
+ registryIds: ["C-INPUT-SIZE"],
44
+ };
45
+
46
+ /**
47
+ * The design-system components that accept the deprecated `inputSize`
48
+ * (W4-05/06/07). `tests/codemods/cm-07.test.mjs` checks the set against the
49
+ * sources that declare `inputSize?:`.
50
+ */
51
+ export const SIZED_CONTROLS = new Set(["Input", "Textarea", "SearchInput"]);
52
+
53
+ export const LEGACY_PROP = "inputSize";
54
+ export const PROP = "size";
55
+
56
+ export function transform(file, { ts }) {
57
+ const facts = analyseForms(ts, file.source, file.rel);
58
+ const findings = [];
59
+ const notTransformed = [];
60
+ const edits = [];
61
+
62
+ for (const el of facts.elements) {
63
+ if (!el.isDs || !SIZED_CONTROLS.has(el.base) || el.component !== el.base) continue;
64
+ if (!el.props.has(LEGACY_PROP)) continue;
65
+
66
+ if (el.props.has(PROP)) {
67
+ notTransformed.push({
68
+ line: el.line,
69
+ reason: "both-names",
70
+ detail: `<${el.tag}> passes \`${LEGACY_PROP}\` and \`${PROP}\` — \`${PROP}\` already wins; delete \`${LEGACY_PROP}\` by hand`,
71
+ });
72
+ continue;
73
+ }
74
+ if (el.spread) {
75
+ notTransformed.push({
76
+ line: el.line,
77
+ reason: "spread-props",
78
+ detail: `<${el.tag} {…}> — the spread may carry \`${PROP}\`; rename by hand once you know it does not`,
79
+ });
80
+ continue;
81
+ }
82
+
83
+ const edit = renameAttribute(ts, facts.sf, openingOf(ts, el.node), LEGACY_PROP, PROP);
84
+ if (!edit) continue;
85
+ edits.push(edit);
86
+ findings.push({
87
+ line: el.props.get(LEGACY_PROP).line,
88
+ registryId: "C-INPUT-SIZE",
89
+ rule: "size-renamed",
90
+ match: `<${el.tag} ${LEGACY_PROP}>`,
91
+ component: el.component,
92
+ action: "applied",
93
+ gate: null,
94
+ detail: { from: LEGACY_PROP, to: PROP, value: el.props.get(LEGACY_PROP).text },
95
+ });
96
+ }
97
+
98
+ return {
99
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
100
+ findings,
101
+ notTransformed,
102
+ parseErrors: facts.parseErrors,
103
+ };
104
+ }