@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.
- package/README.md +53 -0
- package/codemods/README.md +225 -10
- package/codemods/lib/jsx-edit.mjs +63 -4
- package/codemods/lib/registry.mjs +7 -0
- package/codemods/transforms/cm-04-button-variant-intent.mjs +223 -0
- package/codemods/transforms/cm-05-button-icon-slots.mjs +117 -0
- package/codemods/transforms/cm-06-tone-to-intent.mjs +190 -0
- package/codemods/transforms/cm-07-input-size.mjs +104 -0
- package/codemods/transforms/cm-08-search-select-to-combobox.mjs +236 -0
- package/codemods/transforms/cm-10-date-picker-value-change.mjs +416 -0
- package/codemods/transforms/cm-14-hidden-mirrors.mjs +2 -2
- package/codemods/transforms/cm-19-progress-explicit-intent.mjs +192 -0
- package/dist/css/components.css +1 -1
- package/dist/css/legacy-aliases.css +5 -0
- package/dist/css/tokens.css +5 -0
- package/dist/design-system-provider-cPUklDJv.d.ts +220 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.js +1833 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index-CQwzTm0v.d.ts +509 -0
- package/dist/index.d.ts +2967 -823
- package/dist/index.js +7644 -3062
- package/dist/index.js.map +1 -1
- package/dist/next/index.d.ts +20 -0
- package/dist/next/index.js +16 -0
- package/dist/next/index.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/testing/index.cjs +133 -11
- package/dist/testing/index.d.cts +98 -2
- package/dist/testing/index.d.ts +98 -2
- package/dist/testing/index.js +132 -12
- package/docs/components.md +601 -0
- package/docs/components.registry.json +1586 -0
- package/docs/for-ai-agents.md +153 -0
- 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
|
package/codemods/README.md
CHANGED
|
@@ -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-
|
|
113
|
-
| CM-
|
|
114
|
-
| CM-
|
|
115
|
-
| CM-
|
|
116
|
-
| CM-
|
|
117
|
-
| CM-
|
|
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
|
|
121
|
-
|
|
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
|
-
/**
|
|
50
|
-
export function
|
|
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
|
-
|
|
53
|
-
|
|
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" },
|