@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
package/README.md CHANGED
@@ -8,11 +8,16 @@ The single source of truth for visual design across all Assure products.
8
8
 
9
9
  ## Documentation
10
10
 
11
+ - [**`docs/components.md`**](./docs/components.md) — the component catalogue: every public component by need and category, what it replaces, what not to use it for, deprecated aliases and their codemods. Generated from the API reports; ships in the package (`node_modules/@assure-one/design-system/docs/components.md`).
12
+ - [**`docs/for-ai-agents.md`**](./docs/for-ai-agents.md) — the short rulebook for AI agents and new contributors: never raw HTML, the `Field` recipe, `className` vs `classNames`, the `ds:` prefix, tokens, codemods, testing, what is experimental, how to propose a component.
11
13
  - [**`CHANGELOG.md`**](./CHANGELOG.md) — release-by-release log of what shipped.
12
14
  - [**`CONTRIBUTING.md`**](./CONTRIBUTING.md) — dev loop, release flow, versioning, pitfalls.
13
15
  - [**`CLAUDE.md`**](./CLAUDE.md) — invariants and conventions for AI assistants working in this repo.
14
16
  - [**`docs/integration/css.md`**](./docs/integration/css.md) — how an application loads our CSS: layer order, import sequence, the compatibility preset, removing `@source`, runtime brand theming.
15
17
  - [**`docs/testing.md`**](./docs/testing.md) — rendering the real package in your app's Jest tests (`@assure-one/design-system/testing`, experimental).
18
+ - [**`docs/forms.md`**](./docs/forms.md) — building a form: `Field` + design-system controls + a Server Action + zod, reset and `required`, what each control submits, and testing it with `fillField` / `selectOption` / `pickDate`.
19
+ - [**`docs/design-system/icons.md`**](./docs/design-system/icons.md) — the icon catalogue and its server-safe entry (`@assure-one/design-system/icons`, experimental): import an icon in a Server Component without a client boundary.
20
+ - [**`docs/adr/010-design-system-provider.md`**](./docs/adr/010-design-system-provider.md) — the optional `DesignSystemProvider` (messages, locale, link adapter) and the `/next` entry; the quick start is [below](#the-provider-and-the-next-entry-experimental).
16
21
  - [**`claude-skills/`**](./claude-skills) — drop-in Claude Code skill for consuming projects.
17
22
 
18
23
  ## Install
@@ -83,6 +88,54 @@ canvas, and guarantees WCAG AA for every foreground it emits. The seed shape is
83
88
  gated by decision D7 and may change while that decision is open — see
84
89
  [`docs/integration/css.md`](./docs/integration/css.md#5-runtime-brand-theming).
85
90
 
91
+ ### The provider and the `/next` entry (experimental)
92
+
93
+ Nothing requires a provider: every component renders English strings, formats
94
+ with `en-US` and (for `LinkButton` / `Logo`) routes through `next/link` on its
95
+ own. `DesignSystemProvider` is the one optional, thin, client-side provider
96
+ for what CSS cannot do — the generic strings, the locale, the text direction,
97
+ the portal container, the link and image adapters, shared tooltip defaults and
98
+ (opt-in) the toast region. It renders no DOM wrapper ([ADR-010](./docs/adr/010-design-system-provider.md)).
99
+
100
+ ```tsx
101
+ // app/layout.tsx (Next.js) — the Next adapters are filled in for you
102
+ import { NextDesignSystemProvider } from "@assure-one/design-system/next";
103
+
104
+ export default function RootLayout({ children }) {
105
+ return (
106
+ <html lang="de">
107
+ <body>
108
+ <NextDesignSystemProvider
109
+ locale="de-DE"
110
+ messages={{ select: { clear: "Auswahl löschen" }, combobox: { empty: "Keine Treffer" } }}
111
+ >
112
+ {children}
113
+ </NextDesignSystemProvider>
114
+ </body>
115
+ </html>
116
+ );
117
+ }
118
+ ```
119
+
120
+ - **`messages`** — any subset of the typed catalogue `DsMessages`, by component
121
+ namespace (`select`, `searchInput`, `combobox`, `field`, `fileUpload`; more
122
+ join as the overlays adopt it). Resolution is always *component prop >
123
+ provider > English default*, per key. The full English list is
124
+ `src/foundation/messages/en.json`; parameterised messages are functions
125
+ (`combobox.create: (query) => string`).
126
+ - **`locale`** — a BCP 47 tag, default `"en-US"` on the server *and* the client.
127
+ Pass the locale your app already knows server-side; the design system never
128
+ reads `navigator.language` during render, so server and client HTML agree.
129
+ - **`linkComponent` / `imageComponent`** — what `LinkButton` and `Logo` render
130
+ through. Without them the components keep their static `next/link` /
131
+ `next/image` imports until 2.0, so a Next.js app that renders no provider is
132
+ unchanged. A non-Next app passes its router's link; a Next.js app uses
133
+ `NextDesignSystemProvider` so it is ready for the 2.0 removal of the static
134
+ imports.
135
+ - **`portalContainer`**, **`dir`**, **`tooltip`**, **`toasts`** — documented on
136
+ the props. `toasts` mounts the toast region for `useToast()`; leave it off if
137
+ you already render `ToastProvider`.
138
+
86
139
  ## Develop
87
140
 
88
141
  ```bash
@@ -107,18 +107,170 @@ the list a human has to pick up — spread props, dynamic expressions, conflicti
107
107
 
108
108
  ## Codemods
109
109
 
110
- | Id | Class | What it does |
111
- | ----- | ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
112
- | CM-02 | A | Button explicit default size: `size="md"` on every `Button`, `SubmitButton` and `LinkButton` that passes no `size` (pixel-neutral) |
113
- | CM-12 | R | Button `type="submit"` where the intent is evident (inside `<form>` in the same file, no `onClick`); everything else untypedreport |
114
- | CM-14 | X | hidden-input mirror finder: hidden `<input name>` a consumer added because a design-system control posts nothing |
115
- | CM-15 | X | DOM-selector finder: consumer code that depends on the internal DOM of design-system components, mapped to registry `C-DOM-*` ids |
116
- | CM-16 | X | `globals.css` analyser: `@source` into the package, duplicate preflight, colliding `@theme` keys, unlayered globals, legacy `var()` |
117
- | CM-20 | X | Select sentinel finder: option values standing in for "no value", and the line where each is converted back |
110
+ | Id | Class | What it does |
111
+ | ----- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | CM-05 | A | Button family `iconLeft` / `iconRight` `iconStart` / `iconEnd` (pure rename; both names of one slot, spreads → "could not be transformed") |
113
+ | CM-07 | A | `inputSize` `size` on `Input`, `Textarea`, `SearchInput` (pure rename, literal or dynamic value; both names, spreads"could not be transformed") |
114
+ | CM-02 | A | Button explicit default size: `size="md"` on every `Button`, `SubmitButton` and `LinkButton` that passes no `size` (pixel-neutral) |
115
+ | CM-04 | R | Button family legacy `variant` `variant` + `intent` pair (`primary` `solid`, `destructive` `solid` + `danger`, …); `accent`, `dashed`, dynamic → report |
116
+ | CM-06 | R | `tone` `intent` on StatusDot, IconTile, Spinner, SegmentedProgress, SuiteProgress (`pro` `brand`, listed for review); product hues → report |
117
+ | CM-19 | R | ProgressBar/ProgressRing: `variant` `intent`, and the value colour rule written down as an explicit `intent` where the value is a literal; dynamic values → report |
118
+ | CM-12 | R | Button `type="submit"` where the intent is evident (inside `<form>` in the same file, no `onClick`); everything else untyped → report |
119
+ | CM-10 | R | `DatePicker onChange={e => f(e.target.value)}` → `onValueChange={value => f(value)}` for the recognisable arrow shapes; handlers by reference, other shapes → report |
120
+ | CM-08 | R | `SearchSelect` → `Combobox`: single-mode call sites rewritten (tag, import, `{id,label}` accessors, placeholder → `aria-label`); multi API, extras and unnamed sites → report |
121
+ | CM-14 | X | hidden-input mirror finder: hidden `<input name>` a consumer added because a design-system control posts nothing |
122
+ | CM-15 | X | DOM-selector finder: consumer code that depends on the internal DOM of design-system components, mapped to registry `C-DOM-*` ids |
123
+ | CM-16 | X | `globals.css` analyser: `@source` into the package, duplicate preflight, colliding `@theme` keys, unlayered globals, legacy `var()` |
124
+ | CM-20 | X | Select sentinel finder: option values standing in for "no value", and the line where each is converted back |
118
125
 
119
126
  The remaining ids of plan §29 land with the waves that ship their replacement
120
- APIs (CM-04 and CM-05 need the Button `intent` and `iconStart`/`iconEnd` API of
121
- W3-02 and W3-07).
127
+ APIs.
128
+
129
+ ### CM-05 — Button family `iconLeft` / `iconRight` → `iconStart` / `iconEnd`
130
+
131
+ Class A. W3-07 named the two icon slots of `Button`, `LinkButton` and
132
+ `SubmitButton` logically — `iconStart` is the left in LTR and the right in
133
+ RTL — and kept `iconLeft` / `iconRight` as deprecated aliases of the same
134
+ slots (contract C-BTN-ICONPROPS, removed in 3.0). Each alias is the same slot
135
+ under another name, so this is a pure rename: only the attribute's name is
136
+ replaced, and its value — a one-line icon or a multi-line element — keeps its
137
+ text and formatting. Aliased and namespace imports count.
138
+
139
+ | rule | Contract | What happens |
140
+ | -------------- | --------------- | ------------------------------------------------- |
141
+ | `icon-renamed` | C-BTN-ICONPROPS | `iconLeft` → `iconStart`, `iconRight` → `iconEnd` |
142
+
143
+ Not touched, and listed under "could not be transformed": an element that
144
+ passes **both names of one slot** (`iconStart` and `iconLeft` — the component
145
+ throws on that in development; a rename would make a duplicate attribute;
146
+ delete the deprecated one by hand), and an element with a spread
147
+ (`<Button iconLeft={…} {...props}>` — the spread may carry the other name for
148
+ the same slot, and which one wins depends on attribute order). Not touched and
149
+ not reported: elements already on the new names, local components with the
150
+ same names, test files.
151
+
152
+ ### CM-07 — `inputSize` → `size` on Input, Textarea, SearchInput
153
+
154
+ Class A. W4-05/06/07 put the three text controls on the control scale:
155
+ `size` is the prop, `inputSize` the deprecated spelling of the same value
156
+ (contract C-INPUT-SIZE, removed in 3.0). Every legacy word renders the
157
+ classes it always did and `size` wins when both are given, so the rename is
158
+ pixel-neutral and a pure rename: only the attribute's name is replaced, and
159
+ its value — a literal or a dynamic `inputSize={dense ? "sm" : "md"}` — keeps
160
+ its text and formatting. Aliased and namespace imports count.
161
+
162
+ | rule | Contract | What happens |
163
+ | -------------- | ------------ | ---------------------------- |
164
+ | `size-renamed` | C-INPUT-SIZE | `inputSize={…}` → `size={…}` |
165
+
166
+ Not touched, and listed under "could not be transformed": an element that
167
+ passes **both** `size` and `inputSize` (`size` already wins; a rename would
168
+ make a duplicate attribute — delete `inputSize` by hand), and an element with
169
+ a spread (`<Input inputSize="sm" {...props}>` — the spread may carry `size`,
170
+ and which one wins depends on attribute order). Not touched and not reported:
171
+ elements already on `size`, other controls with a `size` of their own, local
172
+ components with the same names, test files.
173
+ `tests/codemods/cm-07.test.mjs` checks the component set against the sources
174
+ that declare `inputSize?:`.
175
+
176
+ ### CM-04 — Button family legacy `variant` → `variant` + `intent`
177
+
178
+ Class R. W3-02 gave `Button`, `LinkButton` and `SubmitButton` the two axes of
179
+ ADR-007 — `variant` is the emphasis, `intent` the meaning — and kept the
180
+ single-axis words as deprecated aliases (contract C-BTN-VAR, removed in 3.0),
181
+ each a fixed pair (`LEGACY_BUTTON_VARIANTS`, ADR-007 §4). Every alias renders
182
+ the exact classes it always did, so the rewrite is pixel-neutral. `intent` is
183
+ written only where it differs from what the new emphasis resolves on its own
184
+ (`DEFAULT_BUTTON_INTENT`, ADR-007 §3), so the output is the smallest call that
185
+ renders today's pixels; when written it goes right after `variant`, on its own
186
+ line in a one-attribute-per-line element. `tests/codemods/cm-04.test.mjs`
187
+ checks both tables against `src/primitives/behavior/button-base.ts`.
188
+
189
+ | legacy word | becomes | `intent` written? |
190
+ | ------------- | ---------------------------------- | ----------------------------------------- |
191
+ | `primary` | `variant="solid"` | no — `brand` is what `solid` defaults to |
192
+ | `secondary` | `variant="soft"` | no — `neutral` is what `soft` defaults to |
193
+ | `destructive` | `variant="solid" intent="danger"` | yes |
194
+ | `success` | `variant="solid" intent="success"` | yes |
195
+ | `accent` | left alone, reported | its suite-action colour has no pair (D13) |
196
+ | `dashed` | left alone, reported | its dashed border has no pair (2.x alias) |
197
+
198
+ | rule | Contract | What happens |
199
+ | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
200
+ | `variant-split` | C-BTN-VAR | the alias became its pair, or its emphasis alone when the intent is the default (applied, low) |
201
+ | `destructive-as-danger` | C-BTN-VAR | `destructive` → `solid` + `danger` (applied, **medium**): the word moves to D4's default (`danger` canonical, ADR-007 §5); confirm |
202
+ | `legacy-look` | C-BTN-VAR, C-BTN-ACCENT | `dashed` (C-BTN-VAR) and `accent` (C-BTN-ACCENT, D13) have a look no pair renders; they stay (review, medium) |
203
+ | `dynamic-intent` | C-BTN-VAR | a legacy word next to `intent={expr}` whose alias intent is not the new emphasis's default: `undefined` would change the colour (review, medium) |
204
+ | `dynamic-variant` | C-BTN-VAR | `variant={expr}`: map the expression's values by hand (review, high) |
205
+ | `spread-props` | C-BTN-VAR | `{...props}` may carry `variant` or `intent`; rewriting the literal could change which wins (review, medium) |
206
+ | `unknown-variant` | C-BTN-VAR | a word the Button never accepted (review, high) |
207
+
208
+ A legacy word next to a **literal** `intent` (`variant="destructive"
209
+ intent="success"`) is rewritten to its emphasis only: the intent already wins
210
+ on both sides of the rewrite. `intent="destructive"` is not touched — that is
211
+ CM-17, gated by D4. Not touched and not reported: the emphases (`ghost`,
212
+ `outline`, `link`, `solid`, `soft`), elements with no `variant`, local
213
+ components with the same names, test files.
214
+
215
+ ### CM-06 — `tone` → `intent`
216
+
217
+ Class R. W3-21 gave `StatusDot`, `IconTile`, `Spinner`, `SegmentedProgress` and
218
+ `SuiteProgress` the shared `intent` axis of ADR-007 and kept `tone` as a
219
+ deprecated alias (contract C-TONE, removed in 3.0). Every legacy word resolves
220
+ to one intent and renders the same classes, so the rename is pixel-neutral. The
221
+ attribute is rewritten in place — `tone="pro"` → `intent="brand"` — so a
222
+ Prettier-formatted file stays formatted; `tone="current"` on `Spinner` is the
223
+ default and is removed.
224
+
225
+ | component | `tone` → `intent` |
226
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
227
+ | StatusDot | `pro` → `brand`; `danger` `warning` `info` `success` `neutral` unchanged |
228
+ | IconTile | `pro` → `brand`; status words unchanged; `audit` `books` `tax` → review |
229
+ | Spinner | `muted` → `neutral`, `accent` → `info`, `destructive` → `danger`, `current` → removed; `success` `warning` unchanged |
230
+ | SegmentedProgress | `default` → `brand`, `destructive` → `danger`; `success` `warning` `info` unchanged; `tax` `audit` `accounting` → review |
231
+ | SuiteProgress | `pro` → `brand`; `audit` `books` `tax` → review |
232
+
233
+ | rule | Contract | What happens |
234
+ | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
235
+ | `tone-renamed` | C-TONE | the attribute becomes `intent="…"` (applied, low) |
236
+ | `pro-as-brand` | C-TONE | `pro` becomes `brand` (applied, **medium**): confirm the call site meant the brand colour, not "generic purple" |
237
+ | `tone-removed` | C-TONE | `tone="current"` is deleted (applied, low) |
238
+ | `product-tone` | C-TONE | `audit` `books` `tax` `accounting` have no intent (ADR-007 §2) and stay on `tone` until the products own a service → colour map |
239
+ | `dynamic-tone` | C-TONE | `tone={expr}`: map the expression's values by hand (review, high) |
240
+ | `spread-props` | C-TONE | `{...props}` may carry `tone` too; renaming the literal could change which one wins (review) |
241
+ | `has-intent` | C-TONE | both axes present; `intent` already wins, delete `tone` by hand (review, low) |
242
+ | `unknown-tone` | C-TONE | a word the component never accepted (review, high) |
243
+
244
+ Not touched: elements that already speak `intent` (idempotency), local
245
+ components with the same names, test files.
246
+
247
+ ### CM-19 — Progress: explicit `intent`
248
+
249
+ Class R. `ProgressBar` and `ProgressRing` colour themselves **by value** when
250
+ no colour is passed (`< 50%` brand, `≥ 50%` warning, `≥ 80%` success) — a
251
+ business mapping the design system should not own, which 2.0 switches off
252
+ (W9-10, contract C-PROGRESS). W3-21 added `intent`; passing it disables the
253
+ rule. CM-19 makes every call site say what it renders today, so the flip
254
+ changes nothing a consumer did not write down: `variant="<legacy word>"`
255
+ becomes `intent="<colour>"` (`default` → `brand`, `destructive` → `danger`),
256
+ and a meter with no colour and a literal `value` (literal or absent `max`)
257
+ gets the `intent` the rule picks for that value, after `value`. The rename to
258
+ the unified `Progress variant="bar" | "ring"` (target architecture §26) is the
259
+ second half of this codemod and lands with that component.
260
+
261
+ | rule | Contract | What happens |
262
+ | --------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------- |
263
+ | `variant-renamed` | C-PROGRESS | `variant` becomes `intent` (applied, low) |
264
+ | `auto-colour-made-explicit` | C-PROGRESS | the value rule's intent is written down (applied, **medium**): confirm the colour, not the number, was the point |
265
+ | `indeterminate-brand` | C-PROGRESS | `value={null}` gets `intent="brand"` (applied, low) |
266
+ | `dynamic-value` | C-PROGRESS | no colour and `value={expr}` (or `max={expr}`): the colour depends on runtime data → a human (review, high) |
267
+ | `dynamic-variant` | C-PROGRESS | `variant={expr}`: map the values by hand (review, high) |
268
+ | `spread-props` | C-PROGRESS | `{...props}` may carry `variant`, `intent` or `value` (review) |
269
+ | `unknown-variant` | C-PROGRESS | a word the component never accepted (review, high) |
270
+
271
+ `SegmentedProgress` and `SuiteProgress` have no value rule; CM-06 moves their
272
+ `tone`. Not touched: elements that already pass `intent`, local components
273
+ with the same names, test files.
122
274
 
123
275
  ### CM-02 — Button explicit default size
124
276
 
@@ -167,6 +319,43 @@ capitalised `<Form>` does not count: the react-hook-form / shadcn `Form` is a
167
319
  context provider, not a form element. `SubmitButton`, `LinkButton`, a Button
168
320
  with any `type`, a Button with `asChild` and test files are not reported.
169
321
 
322
+ ### CM-10 — DatePicker fake-event `onChange` → `onValueChange`
323
+
324
+ Class R. `DatePicker` never fired a DOM event: its `onChange` receives
325
+ `{ target: { name, value } }` with the ISO string (`""` when cleared), and
326
+ every consumer handler unwraps `.target.value`. W4-18 added the value callback
327
+ of target architecture §25, `onValueChange(iso | null)`, and kept the fake
328
+ event as a deprecated alias that fires after it (contract C-DATE-FAKEEVENT,
329
+ removed in 3.0). CM-10 rewrites the handlers whose shape makes the unwrapping
330
+ visible — an inline arrow whose parameter is read **only** as the value:
331
+
332
+ | handler | becomes |
333
+ | ----------------------------------------- | --------------------------------- |
334
+ | `(e) => f(e.target.value)` (typed or not) | `(value) => f(value)` |
335
+ | `e => setX(e.target.value)` | `value => setX(value)` |
336
+ | `({ target }) => f(target.value)` | `(value) => f(value)` |
337
+ | `({ target: { value } }) => f(value)` | `(value) => f(value)` |
338
+ | `() => refetch()` | body unchanged, attribute renamed |
339
+
340
+ The parameter is named `value` — `v`, then `next`, when the enclosing
341
+ component binds that name already, so nothing is shadowed — and the body
342
+ keeps its formatting. **A cleared picker now passes `null` where `onChange`
343
+ passed `""`**: that is why every rewrite is in the review list (medium), and
344
+ why a handler that reads a member of the value (`e.target.value.slice(0, 4)`)
345
+ is not rewritten — it would throw on clear.
346
+
347
+ | rule | Contract | What happens |
348
+ | ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
349
+ | `fake-event-unwrapped` | C-DATE-FAKEEVENT | one of the shapes above was rewritten (applied, **medium**): confirm `null` on clear is what the state wants |
350
+ | `handler-reference` | C-DATE-FAKEEVENT | the handler is passed by reference (`onChange={handleChange}`, `{field.onChange}`, `{onField("due")}`): its body is elsewhere (review, high) |
351
+ | `handler-shape` | C-DATE-FAKEEVENT | an inline handler that reads more than `.target.value` — `e.target.name`, the whole event, a member of the value — or a `function` expression (review, high) |
352
+ | `has-value-change` | C-DATE-FAKEEVENT | `onValueChange` is already wired; `onChange` still fires after it — delete it by hand (review, low) |
353
+ | `spread-props` | C-DATE-FAKEEVENT | `{...props}` may carry either callback; which one wins depends on attribute order (review, medium) |
354
+
355
+ `DateRangePicker` is not touched: its `onChange` is a value callback already.
356
+ Not touched and not reported: elements that only pass `onValueChange`, local
357
+ components called `DatePicker`, test files.
358
+
170
359
  ### CM-14 — hidden-input mirror finder
171
360
 
172
361
  Report-only, **for ever**: deleting a mirror changes what the server receives,
@@ -225,6 +414,32 @@ Sentinels outside the Select family — grouping keys, tab ids, route segments
225
414
  are out of scope even when the spelling matches, and nothing is followed across
226
415
  files.
227
416
 
417
+ ### CM-08 — `SearchSelect` → `Combobox`
418
+
419
+ Class R. W4-12 made `SearchSelect` a deprecated adapter over `Combobox`
420
+ (contract C-SEARCHSELECT, removed in 3.0). A **single-mode** call site
421
+ (`value` / `onValueChange`, no spread) is rewritten: the tag and the named
422
+ import become `Combobox`, `getOptionValue={(o) => o.id}` and
423
+ `getOptionLabel={(o) => o.label}` are inserted after `options` so the
424
+ `{ id, label }` records need no remapping, and a literal `placeholder` — which
425
+ named the SearchSelect input — is copied into `aria-label`. Everything else is
426
+ reported for a human.
427
+
428
+ | rule | Contract | What happens |
429
+ | ----------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
430
+ | `renamed` | C-SEARCHSELECT | tag, import and accessors rewritten (applied, low) |
431
+ | `clear-null` | C-SEARCHSELECT | the rewritten Combobox reports `null` on clear where SearchSelect passed `""`; check the handler (applied, **medium**) |
432
+ | `option-fields` | C-SEARCHSELECT | `description` / `icon` / `color` on the records are not read through the accessors; move them to `renderOption` if used (applied, low) |
433
+ | `rendering` | C-SEARCHSELECT | per call site (plan §29): SearchSelect opened on focus, below the field at its width, never flipping; Combobox flips to stay in the viewport and opens on click or typing (applied on a rewritten element, review otherwise, low) |
434
+ | `multi-api` | C-SEARCHSELECT | `selected` / `onSelect` / `onRemove` → `multiple` + `value: string[]` + `onValueChange`: the state shape changes (review, high) |
435
+ | `unmapped-prop` | C-SEARCHSELECT | `groupFilter`, `pinnedGroups`, `showGroupCounts`, `onCreateNew`, `createNewLabel`, `descriptionVariant`, `closeOnSelect` have no Combobox prop (review, high) |
436
+ | `accessible-name` | C-SEARCHSELECT | no literal placeholder and no `aria-label`: left alone — a Combobox without a name fires the W4-02 development error (review, high) |
437
+ | `spread-props` | C-SEARCHSELECT | `{...props}` may carry any of the above (review, medium) |
438
+ | `option-type` | C-SEARCHSELECT | `SearchSelectOption` is still imported; the Combobox shape is `Option` (review, low) |
439
+
440
+ Not touched: elements already on `Combobox` (idempotency), local components
441
+ with the same name, test files.
442
+
228
443
  ### CM-15 — DOM-selector finder
229
444
 
230
445
  Report-only. It never modifies a file; the runner throws if it tries. What it
@@ -46,14 +46,73 @@ export function insertAttribute(ts, sf, opening, text, { after = null } = {}) {
46
46
  return { pos: attributesStart, text: `${separator}${text}` };
47
47
  }
48
48
 
49
- /** `source` with every `{ pos, text }` insertion applied. */
50
- export function applyInsertions(source, edits) {
49
+ /** The JSX attribute named `name` on `opening`, or null. */
50
+ export function attributeNamed(ts, opening, name) {
51
+ return (
52
+ opening.attributes.properties.find(
53
+ (attr) => ts.isJsxAttribute(attr) && attr.name.getText() === name,
54
+ ) ?? null
55
+ );
56
+ }
57
+
58
+ /**
59
+ * The edit that replaces the attribute named `name` (its whole text, name and
60
+ * initializer) with `text`, e.g. `tone="pro"` → `intent="brand"`. The
61
+ * attribute keeps its place, so the file's formatting is untouched.
62
+ *
63
+ * @returns {{ pos: number, end: number, text: string } | null}
64
+ */
65
+ export function replaceAttribute(ts, sf, opening, name, text) {
66
+ const attr = attributeNamed(ts, opening, name);
67
+ if (!attr) return null;
68
+ return { pos: attr.getStart(sf), end: attr.getEnd(), text };
69
+ }
70
+
71
+ /**
72
+ * The edit that renames the attribute `name` to `to` and keeps its value,
73
+ * e.g. `iconLeft={<X />}` → `iconStart={<X />}`. Only the name's characters
74
+ * are replaced, so the initializer — however many lines it spans — and the
75
+ * file's formatting are untouched.
76
+ *
77
+ * @returns {{ pos: number, end: number, text: string } | null}
78
+ */
79
+ export function renameAttribute(ts, sf, opening, name, to) {
80
+ const attr = attributeNamed(ts, opening, name);
81
+ if (!attr) return null;
82
+ return { pos: attr.name.getStart(sf), end: attr.name.getEnd(), text: to };
83
+ }
84
+
85
+ /**
86
+ * The edit that removes the attribute named `name` together with the
87
+ * whitespace before it, so `<X a tone="current" b>` becomes `<X a b>` and a
88
+ * one-attribute-per-line element loses the whole line.
89
+ *
90
+ * @returns {{ pos: number, end: number, text: string } | null}
91
+ */
92
+ export function removeAttribute(ts, sf, opening, name) {
93
+ const attrs = opening.attributes.properties;
94
+ const index = attrs.findIndex((attr) => ts.isJsxAttribute(attr) && attr.name.getText() === name);
95
+ if (index < 0) return null;
96
+ const from = index === 0 ? opening.attributes.pos : attrs[index - 1].getEnd();
97
+ return { pos: from, end: attrs[index].getEnd(), text: "" };
98
+ }
99
+
100
+ /**
101
+ * `source` with every edit applied, from the end backwards so positions stay
102
+ * valid. An edit is `{ pos, text }` (an insertion) or `{ pos, end, text }` (a
103
+ * replacement of the range). Edits must not overlap.
104
+ */
105
+ export function applyEdits(source, edits) {
51
106
  let out = source;
52
- for (const edit of [...edits].sort((a, b) => b.pos - a.pos)) {
53
- out = `${out.slice(0, edit.pos)}${edit.text}${out.slice(edit.pos)}`;
107
+ const ordered = [...edits].sort((a, b) => b.pos - a.pos || (b.end ?? b.pos) - (a.end ?? a.pos));
108
+ for (const edit of ordered) {
109
+ out = `${out.slice(0, edit.pos)}${edit.text}${out.slice(edit.end ?? edit.pos)}`;
54
110
  }
55
111
  return out;
56
112
  }
57
113
 
114
+ /** `source` with every `{ pos, text }` insertion applied. */
115
+ export const applyInsertions = applyEdits;
116
+
58
117
  /** The opening element of a JSX element node (a self-closing one is its own). */
59
118
  export const openingOf = (ts, node) => (ts.isJsxElement(node) ? node.openingElement : node);
@@ -3,8 +3,15 @@
3
3
  * codemods listed here can be run by id; the sequence is the plan's.
4
4
  */
5
5
  export const CODEMODS = [
6
+ { id: "CM-05", module: "../transforms/cm-05-button-icon-slots.mjs" },
7
+ { id: "CM-07", module: "../transforms/cm-07-input-size.mjs" },
6
8
  { id: "CM-02", module: "../transforms/cm-02-button-explicit-size.mjs" },
9
+ { id: "CM-04", module: "../transforms/cm-04-button-variant-intent.mjs" },
10
+ { id: "CM-06", module: "../transforms/cm-06-tone-to-intent.mjs" },
11
+ { id: "CM-19", module: "../transforms/cm-19-progress-explicit-intent.mjs" },
7
12
  { id: "CM-12", module: "../transforms/cm-12-button-type-submit.mjs" },
13
+ { id: "CM-10", module: "../transforms/cm-10-date-picker-value-change.mjs" },
14
+ { id: "CM-08", module: "../transforms/cm-08-search-select-to-combobox.mjs" },
8
15
  { id: "CM-14", module: "../transforms/cm-14-hidden-mirrors.mjs" },
9
16
  { id: "CM-15", module: "../transforms/cm-15-dom-selectors.mjs" },
10
17
  { id: "CM-16", module: "../transforms/cm-16-globals-css.mjs" },