@assure-one/design-system 1.35.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 +52 -0
- package/codemods/README.md +102 -12
- package/codemods/lib/registry.mjs +3 -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/dist/css/components.css +1 -1
- package/dist/design-system-provider-cPUklDJv.d.ts +220 -0
- package/dist/icons/index.d.ts +2 -2
- package/dist/{index-B3OiMlQv.d.ts → index-CQwzTm0v.d.ts} +1 -1
- package/dist/index.d.ts +2011 -399
- package/dist/index.js +8859 -5262
- 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/{system-lyhOUTpa.d.ts → system-BDU18fVg.d.ts} +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/dist/tokens/index.d.ts +1 -1
- package/docs/components.md +601 -0
- package/docs/components.registry.json +1586 -0
- package/docs/for-ai-agents.md +153 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -8,12 +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`.
|
|
16
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).
|
|
17
21
|
- [**`claude-skills/`**](./claude-skills) — drop-in Claude Code skill for consuming projects.
|
|
18
22
|
|
|
19
23
|
## Install
|
|
@@ -84,6 +88,54 @@ canvas, and guarantees WCAG AA for every foreground it emits. The seed shape is
|
|
|
84
88
|
gated by decision D7 and may change while that decision is open — see
|
|
85
89
|
[`docs/integration/css.md`](./docs/integration/css.md#5-runtime-brand-theming).
|
|
86
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
|
+
|
|
87
139
|
## Develop
|
|
88
140
|
|
|
89
141
|
```bash
|
package/codemods/README.md
CHANGED
|
@@ -107,18 +107,21 @@ 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-05 | A | Button family `iconLeft` / `iconRight` → `iconStart` / `iconEnd` (pure rename; both names of one slot, spreads → "could not be transformed")
|
|
113
|
-
| CM-
|
|
114
|
-
| CM-
|
|
115
|
-
| CM-
|
|
116
|
-
| CM-
|
|
117
|
-
| CM-
|
|
118
|
-
| CM-
|
|
119
|
-
| CM-
|
|
120
|
-
| CM-
|
|
121
|
-
| 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 |
|
|
122
125
|
|
|
123
126
|
The remaining ids of plan §29 land with the waves that ship their replacement
|
|
124
127
|
APIs.
|
|
@@ -146,6 +149,30 @@ the same slot, and which one wins depends on attribute order). Not touched and
|
|
|
146
149
|
not reported: elements already on the new names, local components with the
|
|
147
150
|
same names, test files.
|
|
148
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
|
+
|
|
149
176
|
### CM-04 — Button family legacy `variant` → `variant` + `intent`
|
|
150
177
|
|
|
151
178
|
Class R. W3-02 gave `Button`, `LinkButton` and `SubmitButton` the two axes of
|
|
@@ -292,6 +319,43 @@ capitalised `<Form>` does not count: the react-hook-form / shadcn `Form` is a
|
|
|
292
319
|
context provider, not a form element. `SubmitButton`, `LinkButton`, a Button
|
|
293
320
|
with any `type`, a Button with `asChild` and test files are not reported.
|
|
294
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
|
+
|
|
295
359
|
### CM-14 — hidden-input mirror finder
|
|
296
360
|
|
|
297
361
|
Report-only, **for ever**: deleting a mirror changes what the server receives,
|
|
@@ -350,6 +414,32 @@ Sentinels outside the Select family — grouping keys, tab ids, route segments
|
|
|
350
414
|
are out of scope even when the spelling matches, and nothing is followed across
|
|
351
415
|
files.
|
|
352
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
|
+
|
|
353
443
|
### CM-15 — DOM-selector finder
|
|
354
444
|
|
|
355
445
|
Report-only. It never modifies a file; the runner throws if it tries. What it
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const CODEMODS = [
|
|
6
6
|
{ id: "CM-05", module: "../transforms/cm-05-button-icon-slots.mjs" },
|
|
7
|
+
{ id: "CM-07", module: "../transforms/cm-07-input-size.mjs" },
|
|
7
8
|
{ id: "CM-02", module: "../transforms/cm-02-button-explicit-size.mjs" },
|
|
8
9
|
{ id: "CM-04", module: "../transforms/cm-04-button-variant-intent.mjs" },
|
|
9
10
|
{ id: "CM-06", module: "../transforms/cm-06-tone-to-intent.mjs" },
|
|
10
11
|
{ id: "CM-19", module: "../transforms/cm-19-progress-explicit-intent.mjs" },
|
|
11
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" },
|
|
12
15
|
{ id: "CM-14", module: "../transforms/cm-14-hidden-mirrors.mjs" },
|
|
13
16
|
{ id: "CM-15", module: "../transforms/cm-15-dom-selectors.mjs" },
|
|
14
17
|
{ id: "CM-16", module: "../transforms/cm-16-globals-css.mjs" },
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CM-08 — `SearchSelect` → `Combobox` (class R; plan §29 seq 10, registry
|
|
3
|
+
* `C-SEARCHSELECT`, lands with W4-12).
|
|
4
|
+
*
|
|
5
|
+
* W4-12 turned `SearchSelect` into a deprecated adapter over `Combobox`. The
|
|
6
|
+
* single-mode call sites (33 of 40 uses in the products) are a mechanical
|
|
7
|
+
* rewrite; the multi API and the SearchSelect-only props are not, and are
|
|
8
|
+
* reported for a human.
|
|
9
|
+
*
|
|
10
|
+
* ## What it rewrites (single mode: `value` / `onValueChange`, no spread)
|
|
11
|
+
*
|
|
12
|
+
* - the tag `SearchSelect` → `Combobox`, and the named import with it when
|
|
13
|
+
* the file has no other `SearchSelect` element left
|
|
14
|
+
* - `options` keeps its expression; `getOptionValue={(o) => o.id}` and
|
|
15
|
+
* `getOptionLabel={(o) => o.label}` are inserted after it, so the `{ id,
|
|
16
|
+
* label }` records need no remapping (target architecture §32 accessors)
|
|
17
|
+
* - `placeholder="…"` named the SearchSelect input; a Combobox is named by a
|
|
18
|
+
* Field label or `aria-label`, so a literal placeholder is copied into
|
|
19
|
+
* `aria-label` when the element has none
|
|
20
|
+
*
|
|
21
|
+
* ## What it reports
|
|
22
|
+
*
|
|
23
|
+
* | rule | action | severity | why |
|
|
24
|
+
* | -------------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
|
|
25
|
+
* | `renamed` | applied | low | tag, import and accessors rewritten |
|
|
26
|
+
* | `clear-null` | applied | medium | the rewritten element's Combobox reports `null` on clear where SearchSelect passed `""`; check the handler |
|
|
27
|
+
* | `rendering` | applied / review | low | SearchSelect opened on focus, below the field, never flipping; Combobox flips to stay in the viewport and opens on click/typing (plan §29: flagged per call site — `applied` on a rewritten element, `review` on one left alone) |
|
|
28
|
+
* | `option-fields` | applied | low | `description` / `icon` / `color` on the records are not read through the accessors; move them to `renderOption` if used |
|
|
29
|
+
* | `multi-api` | review | high | `selected` / `onSelect` / `onRemove` → `multiple` + `value: string[]` + `onValueChange`; the state shape changes |
|
|
30
|
+
* | `unmapped-prop` | review | high | `groupFilter` / `pinnedGroups` / `showGroupCounts` / `onCreateNew` / `createNewLabel` / `descriptionVariant` / `closeOnSelect` have no Combobox prop |
|
|
31
|
+
* | `accessible-name` | review | high | no literal placeholder and no `aria-label`: the element is left alone — a Combobox without a name fires the W4-02 development error |
|
|
32
|
+
* | `spread-props` | review | medium | `{...props}` may carry any of the above |
|
|
33
|
+
* | `option-type` | review | low | `SearchSelectOption` is still imported; the Combobox shape is `Option` |
|
|
34
|
+
*
|
|
35
|
+
* Elements already on `Combobox` are not touched (idempotency). Local
|
|
36
|
+
* components with the same name are not touched; test files are skipped.
|
|
37
|
+
*/
|
|
38
|
+
import { analyseForms } from "../lib/forms.mjs";
|
|
39
|
+
import { applyEdits, insertAttribute, openingOf } from "../lib/jsx-edit.mjs";
|
|
40
|
+
|
|
41
|
+
export const meta = {
|
|
42
|
+
id: "CM-08",
|
|
43
|
+
title: "SearchSelect → Combobox (single mode automatic; multi and extras reported)",
|
|
44
|
+
class: "R",
|
|
45
|
+
oneShot: false,
|
|
46
|
+
requires: { codemods: [], dsVersion: null },
|
|
47
|
+
parses: ["code"],
|
|
48
|
+
includeTests: false,
|
|
49
|
+
usesTypeScript: true,
|
|
50
|
+
usesPostcss: false,
|
|
51
|
+
registryIds: ["C-SEARCHSELECT"],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const RULES = {
|
|
55
|
+
renamed: { action: "applied", severity: "low" },
|
|
56
|
+
"clear-null": { action: "applied", severity: "medium" },
|
|
57
|
+
rendering: { action: "applied", severity: "low" },
|
|
58
|
+
"option-fields": { action: "applied", severity: "low" },
|
|
59
|
+
"multi-api": { action: "review", severity: "high" },
|
|
60
|
+
"unmapped-prop": { action: "review", severity: "high" },
|
|
61
|
+
"accessible-name": { action: "review", severity: "high" },
|
|
62
|
+
"spread-props": { action: "review", severity: "medium" },
|
|
63
|
+
"option-type": { action: "review", severity: "low" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Props the Combobox takes under the same name and meaning. */
|
|
67
|
+
export const MAPPED_PROPS = new Set([
|
|
68
|
+
"options",
|
|
69
|
+
"value",
|
|
70
|
+
"onValueChange",
|
|
71
|
+
"placeholder",
|
|
72
|
+
"className",
|
|
73
|
+
"disabled",
|
|
74
|
+
"onCreate",
|
|
75
|
+
"id",
|
|
76
|
+
"aria-label",
|
|
77
|
+
"aria-labelledby",
|
|
78
|
+
"aria-describedby",
|
|
79
|
+
]);
|
|
80
|
+
export const MULTI_PROPS = new Set(["selected", "onSelect", "onRemove"]);
|
|
81
|
+
export const UNMAPPED_PROPS = new Set([
|
|
82
|
+
"groupFilter",
|
|
83
|
+
"pinnedGroups",
|
|
84
|
+
"showGroupCounts",
|
|
85
|
+
"onCreateNew",
|
|
86
|
+
"createNewLabel",
|
|
87
|
+
"descriptionVariant",
|
|
88
|
+
"closeOnSelect",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const ACCESSORS = "getOptionValue={(o) => o.id} getOptionLabel={(o) => o.label}";
|
|
92
|
+
|
|
93
|
+
export function transform(file, { ts }) {
|
|
94
|
+
const facts = analyseForms(ts, file.source, file.rel);
|
|
95
|
+
const findings = [];
|
|
96
|
+
const edits = [];
|
|
97
|
+
let remaining = 0;
|
|
98
|
+
let renamedAny = false;
|
|
99
|
+
|
|
100
|
+
for (const el of facts.elements) {
|
|
101
|
+
if (!el.isDs || el.base !== "SearchSelect" || el.component !== el.base) continue;
|
|
102
|
+
const base = {
|
|
103
|
+
line: el.line,
|
|
104
|
+
registryId: "C-SEARCHSELECT",
|
|
105
|
+
match: `<${el.tag}>`,
|
|
106
|
+
component: el.component,
|
|
107
|
+
gate: null,
|
|
108
|
+
detail: {},
|
|
109
|
+
};
|
|
110
|
+
const add = (rule, detail = {}, override = {}) =>
|
|
111
|
+
findings.push({
|
|
112
|
+
...base,
|
|
113
|
+
rule,
|
|
114
|
+
...RULES[rule],
|
|
115
|
+
...override,
|
|
116
|
+
detail: { ...base.detail, ...detail },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const names = [...el.props.keys()];
|
|
120
|
+
const multi = names.filter((n) => MULTI_PROPS.has(n));
|
|
121
|
+
const unmapped = names.filter((n) => UNMAPPED_PROPS.has(n));
|
|
122
|
+
let blocked = false;
|
|
123
|
+
if (el.spread) {
|
|
124
|
+
add("spread-props");
|
|
125
|
+
blocked = true;
|
|
126
|
+
}
|
|
127
|
+
if (multi.length) {
|
|
128
|
+
add("multi-api", { props: multi });
|
|
129
|
+
blocked = true;
|
|
130
|
+
}
|
|
131
|
+
if (unmapped.length) {
|
|
132
|
+
add("unmapped-prop", { props: unmapped });
|
|
133
|
+
blocked = true;
|
|
134
|
+
}
|
|
135
|
+
if (blocked) {
|
|
136
|
+
remaining += 1;
|
|
137
|
+
add("rendering", {}, { action: "review" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A Combobox without an accessible name fires the W4-02 development
|
|
142
|
+
// error at once, so an element whose name cannot be derived (no literal
|
|
143
|
+
// placeholder, no aria-label) is left for a human rather than rewritten.
|
|
144
|
+
const placeholder = el.props.get("placeholder");
|
|
145
|
+
const hasName = el.props.has("aria-label") || el.props.has("aria-labelledby");
|
|
146
|
+
const literal =
|
|
147
|
+
placeholder && !placeholder.expression && placeholder.literals.length === 1
|
|
148
|
+
? placeholder.literals[0]
|
|
149
|
+
: null;
|
|
150
|
+
if (!hasName && literal === null) {
|
|
151
|
+
remaining += 1;
|
|
152
|
+
add("accessible-name");
|
|
153
|
+
add("rendering", {}, { action: "review" });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const opening = openingOf(ts, el.node);
|
|
158
|
+
const sf = facts.sf;
|
|
159
|
+
edits.push({
|
|
160
|
+
pos: opening.tagName.getStart(sf),
|
|
161
|
+
end: opening.tagName.getEnd(),
|
|
162
|
+
text: "Combobox",
|
|
163
|
+
});
|
|
164
|
+
if (ts.isJsxElement(el.node)) {
|
|
165
|
+
const closing = el.node.closingElement.tagName;
|
|
166
|
+
edits.push({ pos: closing.getStart(sf), end: closing.getEnd(), text: "Combobox" });
|
|
167
|
+
}
|
|
168
|
+
if (el.props.has("options")) {
|
|
169
|
+
edits.push(insertAttribute(ts, sf, opening, ACCESSORS, { after: "options" }));
|
|
170
|
+
}
|
|
171
|
+
if (!hasName) {
|
|
172
|
+
edits.push(
|
|
173
|
+
insertAttribute(ts, sf, opening, `aria-label=${JSON.stringify(literal)}`, {
|
|
174
|
+
after: "placeholder",
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
renamedAny = true;
|
|
179
|
+
add("renamed");
|
|
180
|
+
if (el.props.has("onValueChange")) add("clear-null");
|
|
181
|
+
add("option-fields");
|
|
182
|
+
add("rendering");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The import: `SearchSelect` → `Combobox` when no element is left on the
|
|
186
|
+
// old name; a `SearchSelectOption` type import is reported, never rewritten.
|
|
187
|
+
if (renamedAny) {
|
|
188
|
+
const sf = facts.sf;
|
|
189
|
+
for (const stmt of sf.statements) {
|
|
190
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
191
|
+
if (!stmt.moduleSpecifier.text.startsWith("@assure-one/design-system")) continue;
|
|
192
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
193
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
194
|
+
const hasCombobox = bindings.elements.some(
|
|
195
|
+
(e) => (e.propertyName ?? e.name).text === "Combobox",
|
|
196
|
+
);
|
|
197
|
+
for (const element of bindings.elements) {
|
|
198
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
199
|
+
if (imported === "SearchSelectOption") {
|
|
200
|
+
findings.push({
|
|
201
|
+
line: facts.lineOf(element),
|
|
202
|
+
registryId: "C-SEARCHSELECT",
|
|
203
|
+
match: "SearchSelectOption",
|
|
204
|
+
component: "SearchSelect",
|
|
205
|
+
gate: null,
|
|
206
|
+
rule: "option-type",
|
|
207
|
+
...RULES["option-type"],
|
|
208
|
+
detail: {},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (imported !== "SearchSelect" || element.propertyName || remaining > 0) continue;
|
|
212
|
+
if (hasCombobox) {
|
|
213
|
+
// Drop the specifier: `Combobox` is already imported.
|
|
214
|
+
const list = bindings.elements;
|
|
215
|
+
const index = list.indexOf(element);
|
|
216
|
+
const from = index === 0 ? element.getStart(sf) : list[index - 1].getEnd();
|
|
217
|
+
const end = index === 0 && list.length > 1 ? list[1].getStart(sf) : element.getEnd();
|
|
218
|
+
edits.push({ pos: from, end, text: "" });
|
|
219
|
+
} else {
|
|
220
|
+
edits.push({
|
|
221
|
+
pos: element.name.getStart(sf),
|
|
222
|
+
end: element.name.getEnd(),
|
|
223
|
+
text: "Combobox",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
output: edits.length ? applyEdits(file.source, edits) : file.source,
|
|
232
|
+
findings,
|
|
233
|
+
notTransformed: [],
|
|
234
|
+
parseErrors: facts.parseErrors,
|
|
235
|
+
};
|
|
236
|
+
}
|