@assure-one/design-system 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/codemods/README.md +40 -0
- package/codemods/lib/registry.mjs +1 -0
- package/codemods/transforms/cm-18-data-table-parts.mjs +234 -0
- package/dist/css/components.css +1 -1
- package/dist/css/legacy-aliases.css +8 -0
- package/dist/css/tokens.css +8 -0
- package/dist/{design-system-provider-C8UlFe52.d.ts → design-system-provider-B5MbvbRc.d.ts} +61 -1
- package/dist/index.d.ts +1358 -206
- package/dist/index.js +2201 -607
- package/dist/index.js.map +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/docs/components.md +36 -8
- package/docs/components.registry.json +2399 -463
- package/docs/for-ai-agents.md +2 -0
- package/eslint-config/index.mjs +210 -0
- package/eslint-config/stylelint-rules.mjs +54 -0
- package/eslint-config/stylelint.mjs +73 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The single source of truth for visual design across all Assure products.
|
|
|
16
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.
|
|
17
17
|
- [**`docs/testing.md`**](./docs/testing.md) — rendering the real package in your app's Jest tests (`@assure-one/design-system/testing`, experimental).
|
|
18
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/data-tables.md`**](./docs/data-tables.md) — building a data table: the headless `useDataTable` controller (sorting, pagination, search, selection, column visibility, filters — each controllable), server `manual` mode, and the URL-state recipe.
|
|
19
20
|
- [**`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
21
|
- [**`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).
|
|
21
22
|
- [**`claude-skills/`**](./claude-skills) — drop-in Claude Code skill for consuming projects.
|
|
@@ -52,14 +53,14 @@ Alongside it, the package now also publishes the individual stylesheets the new
|
|
|
52
53
|
CSS mode is built from. They are **experimental**: nothing requires them yet,
|
|
53
54
|
no component reads them, and importing them changes nothing on its own.
|
|
54
55
|
|
|
55
|
-
| Subpath
|
|
56
|
-
|
|
|
57
|
-
| `./css/tokens.css`
|
|
58
|
-
| `./css/legacy-aliases.css` | Read-aliases from today's token names to the namespaced ones (`--color-surface: var(--ds-color-surface)`), for as long as your own CSS reads design-system token names.
|
|
59
|
-
| `./css/tailwind.css`
|
|
60
|
-
| `./css/shadcn.css`
|
|
61
|
-
| `./css/base.css`
|
|
62
|
-
| `./css/components.css`
|
|
56
|
+
| Subpath | What it is |
|
|
57
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `./css/tokens.css` | The `--ds-*` design tokens: `:root`, the colour scheme scope and the brand scopes. Usable without any component. |
|
|
59
|
+
| `./css/legacy-aliases.css` | Read-aliases from today's token names to the namespaced ones (`--color-surface: var(--ds-color-surface)`), for as long as your own CSS reads design-system token names. |
|
|
60
|
+
| `./css/tailwind.css` | A Tailwind `@theme` bridge, so `bg-surface/40`, `hover:text-fg-3` and `rounded-control` compile in _your_ build instead of silently producing nothing. |
|
|
61
|
+
| `./css/shadcn.css` | The app-vocabulary bridge: `background`, `foreground`, `primary`, `muted`, `destructive`, the radius scale and the rest, each reading one `--app-*` input you can override. |
|
|
62
|
+
| `./css/base.css` | Optional document defaults: body background, ink, font and `color-scheme`. Ships no preflight, no element rules and no font import. |
|
|
63
|
+
| `./css/components.css` | The component utilities compiled with Tailwind's `ds` prefix (`ds:flex`, `ds:bg-surface`), inside `@layer ds` and bound to the `--ds-*` tokens. **Unused until the class-vocabulary flip (W1-11):** no component emits a prefixed class yet, so importing it styles nothing. Published so the file, its size and its hygiene can be reviewed before the flip. |
|
|
63
64
|
|
|
64
65
|
Import order is `tokens.css` → `legacy-aliases.css` (optional) → `tailwind.css`
|
|
65
66
|
→ `shadcn.css` (optional) → `base.css` (optional) → `components.css`, after your own
|
|
@@ -119,11 +120,11 @@ export default function RootLayout({ children }) {
|
|
|
119
120
|
|
|
120
121
|
- **`messages`** — any subset of the typed catalogue `DsMessages`, by component
|
|
121
122
|
namespace (`select`, `searchInput`, `combobox`, `field`, `fileUpload`; more
|
|
122
|
-
join as the overlays adopt it). Resolution is always
|
|
123
|
-
provider > English
|
|
123
|
+
join as the overlays adopt it). Resolution is always _component prop >
|
|
124
|
+
provider > English default_, per key. The full English list is
|
|
124
125
|
`src/foundation/messages/en.json`; parameterised messages are functions
|
|
125
126
|
(`combobox.create: (query) => string`).
|
|
126
|
-
- **`locale`** — a BCP 47 tag, default `"en-US"` on the server
|
|
127
|
+
- **`locale`** — a BCP 47 tag, default `"en-US"` on the server _and_ the client.
|
|
127
128
|
Pass the locale your app already knows server-side; the design system never
|
|
128
129
|
reads `navigator.language` during render, so server and client HTML agree.
|
|
129
130
|
- **`linkComponent` / `imageComponent`** — what `LinkButton` and `Logo` render
|
|
@@ -136,6 +137,34 @@ export default function RootLayout({ children }) {
|
|
|
136
137
|
the props. `toasts` mounts the toast region for `useToast()`; leave it off if
|
|
137
138
|
you already render `ToastProvider`.
|
|
138
139
|
|
|
140
|
+
## Lint configs for consumers
|
|
141
|
+
|
|
142
|
+
Two shareable configs ship with the package (**experimental**, W7-18) so a
|
|
143
|
+
product lints for the same things this repository ratchets on itself. Every
|
|
144
|
+
rule is a **warning**: adopt it on the code you have, record the count, and
|
|
145
|
+
ratchet it down.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
// eslint.config.mjs
|
|
149
|
+
import ds from "@assure-one/design-system/eslint-config";
|
|
150
|
+
export default [...ds.configs.recommended];
|
|
151
|
+
|
|
152
|
+
// stylelint.config.mjs (stylelint is yours — an optional peer dependency here)
|
|
153
|
+
export { default } from "@assure-one/design-system/stylelint-config";
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Rule | Reports |
|
|
157
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
158
|
+
| `ds/no-raw-controls` | raw `<button>`, `<input>`, `<select>`, `<textarea>` where a design-system control exists |
|
|
159
|
+
| `ds/no-radix-imports` | a direct `@radix-ui/*` import instead of the primitive that wraps it |
|
|
160
|
+
| `ds/no-deep-imports` | an import past the published entries (`dist/…`, `src/…` — ADR-011) |
|
|
161
|
+
| `ds/no-ds-token-redefinition` | a `--ds-*` token set inline, outside a file under `presets/` |
|
|
162
|
+
| `assure-ds/no-ds-token-redefinition` | the same in CSS (stylelint; `presets/` and `*.preset.css` are exempt) |
|
|
163
|
+
| `assure-ds/no-important-on-ds-selectors` | `!important` on a `ds:`/`data-slot` selector — pass `className`/`classNames` instead |
|
|
164
|
+
|
|
165
|
+
Raise a rule to `"error"` in your own config once you are clean. Adoption per
|
|
166
|
+
app is tracked as W7-19.
|
|
167
|
+
|
|
139
168
|
## Develop
|
|
140
169
|
|
|
141
170
|
```bash
|
|
@@ -153,11 +182,11 @@ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full dev loop, including iter
|
|
|
153
182
|
3-tier token system, authored as DTCG JSON in [`tokens/src`](./tokens/README.md)
|
|
154
183
|
and generated into `src/tokens/` and the `./css/*` entries (ADR-003):
|
|
155
184
|
|
|
156
|
-
| Tier
|
|
157
|
-
|
|
|
158
|
-
| reference
|
|
159
|
-
| system (semantic) | `--ds-color-action-brand-bg: var(--ds-ref-brand-pro-fg)` | the reference tier; **this is the public contract**
|
|
160
|
-
| component
|
|
185
|
+
| Tier | Example (namespaced name → value) | Reads |
|
|
186
|
+
| ----------------- | -------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
187
|
+
| reference | `--ds-ref-brand-pro-fg: #6c42f8` | a literal; the raw ramps, one per brand |
|
|
188
|
+
| system (semantic) | `--ds-color-action-brand-bg: var(--ds-ref-brand-pro-fg)` | the reference tier; **this is the public contract** |
|
|
189
|
+
| component | `--ds-menu-item-hover-bg: var(--ds-color-canvas-sunken)` | the system tier; only where a component must be themable on its own |
|
|
161
190
|
|
|
162
191
|
Components consume **system tokens**, never reference values directly. Themes
|
|
163
192
|
(colour scheme, per-product brand, a runtime brand from `createBrandTheme()`)
|
package/codemods/README.md
CHANGED
|
@@ -120,6 +120,7 @@ the list a human has to pick up — spread props, dynamic expressions, conflicti
|
|
|
120
120
|
| 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 |
|
|
121
121
|
| 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 |
|
|
122
122
|
| CM-09 | R | `SideDrawer` → `Sheet`: drawers with a `SideDrawer.Header` rewritten (root → `Sheet` + `SheetContent side size`, `onClose` → `onOpenChange`, header → `SheetHeader`/`SheetTitle`/`SheetDescription`, Body/Footer → `SheetBody`/`SheetFooter`, import); no header, dynamic width, spreads → report |
|
|
123
|
+
| CM-18 | A | `DataTable*` presentational parts → `Table*` (`DataTableHead` → `TableHeader`, `Body`, `Row`, `Cell`, `DataTableHeader` → `TableHead`) where the mapping is 1:1; uncontrolled sorting, a dynamic `sortable` and spreads → "could not be transformed" |
|
|
123
124
|
| CM-14 | X | hidden-input mirror finder: hidden `<input name>` a consumer added because a design-system control posts nothing |
|
|
124
125
|
| CM-15 | X | DOM-selector finder: consumer code that depends on the internal DOM of design-system components, mapped to registry `C-DOM-*` ids |
|
|
125
126
|
| CM-16 | X | `globals.css` analyser: `@source` into the package, duplicate preflight, colliding `@theme` keys, unlayered globals, legacy `var()` |
|
|
@@ -493,6 +494,45 @@ Not touched: elements already on `Sheet` (idempotency), local components with
|
|
|
493
494
|
the same name, test files. The rewritten children are re-indented one level;
|
|
494
495
|
run the project's formatter afterwards.
|
|
495
496
|
|
|
497
|
+
### CM-18 — `DataTable*` parts → `Table*`
|
|
498
|
+
|
|
499
|
+
Class A. W6-03 made the `DataTable*` table parts aliases of the `Table*` parts
|
|
500
|
+
(contract C-DT-PARTS, removed in 3.0): one styling source, the same props. The
|
|
501
|
+
1:1 parts are a pure rename — tag, closing tag and the named import:
|
|
502
|
+
|
|
503
|
+
| from | to |
|
|
504
|
+
| ----------------- | ------------- |
|
|
505
|
+
| `DataTableHead` | `TableHeader` |
|
|
506
|
+
| `DataTableBody` | `TableBody` |
|
|
507
|
+
| `DataTableRow` | `TableRow` |
|
|
508
|
+
| `DataTableCell` | `TableCell` |
|
|
509
|
+
| `DataTableHeader` | `TableHead` |
|
|
510
|
+
|
|
511
|
+
`DataTableHeader sortable sort={…} onSortChange={…}` loses the `sortable`
|
|
512
|
+
attribute: `TableHead` sorts whenever `sort` is passed (`null` = sortable, not
|
|
513
|
+
sorted). What changes in the DOM is the `data-slot` value (`table-*` instead of
|
|
514
|
+
`data-table-*`) and the dropped `scope="col"` — a `<th>` in a `<thead>` is a
|
|
515
|
+
column header already. Classes and pixels are the same, because since W6-03 the
|
|
516
|
+
`DataTable*` parts render the `Table*` parts.
|
|
517
|
+
|
|
518
|
+
| rule | Contract | What happens |
|
|
519
|
+
| --------------- | ---------- | ------------------------------------------------------------------- |
|
|
520
|
+
| `part-renamed` | C-DT-PARTS | tag, closing tag and the named import rewritten (applied, low) |
|
|
521
|
+
|
|
522
|
+
Reported under "could not be transformed": `DataTableHeader sortable` with no
|
|
523
|
+
controlled `sort`, or with `defaultSort` (`uncontrolled-sort` — `TableHead` has
|
|
524
|
+
no uncontrolled sort state; wire `sort` + `onSortChange`, e.g. from
|
|
525
|
+
`useDataTable`); `sortable={expr}` (`dynamic-sortable`); a spread on the header
|
|
526
|
+
(`spread-props`).
|
|
527
|
+
|
|
528
|
+
Not touched: the parts that are not parallel copies of a `Table*` part —
|
|
529
|
+
`DataTable` (the card frame), `DataTableToolbar`, `DataTableSearch`,
|
|
530
|
+
`DataTableSpacer`, `DataTableResultsCount`, `DataTableCellName` / `CellMono` /
|
|
531
|
+
`CellId` / `CellDue`, `DataTableCheckbox`, `DataTablePagination` — an aliased
|
|
532
|
+
import (`DataTableCell as Cell`), a namespace import, local components with the
|
|
533
|
+
same names, and test files. A file that still refers to the old name as a value
|
|
534
|
+
(`typeof DataTableCell`) keeps its import and gains the new name next to it.
|
|
535
|
+
|
|
496
536
|
### CM-15 — DOM-selector finder
|
|
497
537
|
|
|
498
538
|
Report-only. It never modifies a file; the runner throws if it tries. What it
|
|
@@ -14,6 +14,7 @@ export const CODEMODS = [
|
|
|
14
14
|
{ id: "CM-10", module: "../transforms/cm-10-date-picker-value-change.mjs" },
|
|
15
15
|
{ id: "CM-08", module: "../transforms/cm-08-search-select-to-combobox.mjs" },
|
|
16
16
|
{ id: "CM-09", module: "../transforms/cm-09-side-drawer-to-sheet.mjs" },
|
|
17
|
+
{ id: "CM-18", module: "../transforms/cm-18-data-table-parts.mjs" },
|
|
17
18
|
{ id: "CM-14", module: "../transforms/cm-14-hidden-mirrors.mjs" },
|
|
18
19
|
{ id: "CM-15", module: "../transforms/cm-15-dom-selectors.mjs" },
|
|
19
20
|
{ id: "CM-16", module: "../transforms/cm-16-globals-css.mjs" },
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CM-18 — `DataTable*` presentational parts → `Table*` (class A; plan §29
|
|
3
|
+
* seq 12, registry `C-DT-PARTS`, lands with W6-03).
|
|
4
|
+
*
|
|
5
|
+
* W6-03 made the `DataTable*` table parts thin aliases of the `Table*` parts:
|
|
6
|
+
* one styling source, the same props. Where a part is a 1:1 copy the rename
|
|
7
|
+
* is mechanical and this codemod applies it — tag, closing tag and the named
|
|
8
|
+
* import:
|
|
9
|
+
*
|
|
10
|
+
* | from | to | note |
|
|
11
|
+
* | ----------------- | ------------- | ----------------------------------------------------------- |
|
|
12
|
+
* | `DataTableHead` | `TableHeader` | `<thead>` |
|
|
13
|
+
* | `DataTableBody` | `TableBody` | `<tbody>` |
|
|
14
|
+
* | `DataTableRow` | `TableRow` | `selected` is the same prop |
|
|
15
|
+
* | `DataTableCell` | `TableCell` | same props (`align`, `numeric`, `muted`, `truncate`, `width`)|
|
|
16
|
+
* | `DataTableHeader` | `TableHead` | plain, or controlled `sortable sort onSortChange` — `sortable` is dropped because `TableHead` is sortable whenever `sort` is passed |
|
|
17
|
+
*
|
|
18
|
+
* What changes for the rendered DOM: the `data-slot` attributes read
|
|
19
|
+
* `table-*` instead of `data-table-*`, and `TableHead` does not stamp
|
|
20
|
+
* `scope="col"` (a `<th>` inside `<thead>` is a column header by default).
|
|
21
|
+
* Classes, geometry and colours are the same — the `DataTable*` parts render
|
|
22
|
+
* through the `Table*` parts since W6-03.
|
|
23
|
+
*
|
|
24
|
+
* ## What it leaves alone, and lists under "could not be transformed"
|
|
25
|
+
*
|
|
26
|
+
* - `DataTableHeader sortable` **without** a controlled `sort` (uncontrolled
|
|
27
|
+
* sorting, `defaultSort`): `TableHead` has no uncontrolled sort state — the
|
|
28
|
+
* consumer wires `sort` from `useDataTable` or its own state.
|
|
29
|
+
* - `DataTableHeader sortable={expr}`: whether the header is sortable is not
|
|
30
|
+
* visible here.
|
|
31
|
+
* - `DataTableHeader {...props}`: the spread may carry `sortable`.
|
|
32
|
+
*
|
|
33
|
+
* Not touched at all (not parallel copies of a `Table*` part, they stay):
|
|
34
|
+
* `DataTable` (the card frame), `DataTableToolbar`, `DataTableSearch`,
|
|
35
|
+
* `DataTableSpacer`, `DataTableResultsCount`, `DataTableCellName` /
|
|
36
|
+
* `DataTableCellMono` / `DataTableCellId` / `DataTableCellDue`,
|
|
37
|
+
* `DataTableCheckbox`, `DataTablePagination`, and the `DataTable*Props` types.
|
|
38
|
+
* An aliased import (`DataTableCell as Cell`) and a namespace import are left
|
|
39
|
+
* alone too. The import specifier is rewritten only when no reference to the
|
|
40
|
+
* old name remains in the file (a `typeof DataTableCell` keeps it, and the
|
|
41
|
+
* new name is added next to it). Local components with the same names are
|
|
42
|
+
* not touched; test files are skipped, as by the scanner that measures
|
|
43
|
+
* C-DT-PARTS.
|
|
44
|
+
*/
|
|
45
|
+
import { analyseForms } from "../lib/forms.mjs";
|
|
46
|
+
import { applyEdits, openingOf, removeAttribute } from "../lib/jsx-edit.mjs";
|
|
47
|
+
|
|
48
|
+
export const meta = {
|
|
49
|
+
id: "CM-18",
|
|
50
|
+
title: "DataTable* presentational parts → Table* (pure rename where the part is a 1:1 copy)",
|
|
51
|
+
class: "A",
|
|
52
|
+
oneShot: false,
|
|
53
|
+
requires: { codemods: [], dsVersion: null },
|
|
54
|
+
parses: ["code"],
|
|
55
|
+
includeTests: false,
|
|
56
|
+
usesTypeScript: true,
|
|
57
|
+
usesPostcss: false,
|
|
58
|
+
registryIds: ["C-DT-PARTS"],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `DataTable*` part → the `Table*` part it aliases (src/composites/data-table.tsx, W6-03). */
|
|
62
|
+
export const PART_MAP = {
|
|
63
|
+
DataTableHead: "TableHeader",
|
|
64
|
+
DataTableBody: "TableBody",
|
|
65
|
+
DataTableRow: "TableRow",
|
|
66
|
+
DataTableCell: "TableCell",
|
|
67
|
+
DataTableHeader: "TableHead",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const RULES = {
|
|
71
|
+
"part-renamed": { action: "applied", severity: "low" },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DS_PACKAGE = "@assure-one/design-system";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Identifier references to `name` outside its import specifier and outside
|
|
78
|
+
* JSX tag names — `typeof DataTableCell`, `as={DataTableCell}`, a plain
|
|
79
|
+
* value use. While one exists the old import must stay.
|
|
80
|
+
*/
|
|
81
|
+
function hasOtherReferences(ts, sf, name) {
|
|
82
|
+
let found = false;
|
|
83
|
+
const visit = (node) => {
|
|
84
|
+
if (found) return;
|
|
85
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
86
|
+
const parent = node.parent;
|
|
87
|
+
const isImport = parent && ts.isImportSpecifier(parent);
|
|
88
|
+
const isTag =
|
|
89
|
+
parent &&
|
|
90
|
+
(ts.isJsxOpeningElement(parent) ||
|
|
91
|
+
ts.isJsxSelfClosingElement(parent) ||
|
|
92
|
+
ts.isJsxClosingElement(parent)) &&
|
|
93
|
+
parent.tagName === node;
|
|
94
|
+
if (!isImport && !isTag) found = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
ts.forEachChild(node, visit);
|
|
98
|
+
};
|
|
99
|
+
visit(sf);
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function transform(file, { ts }) {
|
|
104
|
+
const facts = analyseForms(ts, file.source, file.rel);
|
|
105
|
+
const sf = facts.sf;
|
|
106
|
+
const findings = [];
|
|
107
|
+
const notTransformed = [];
|
|
108
|
+
const edits = [];
|
|
109
|
+
/** old local name → new name, for the elements that were renamed */
|
|
110
|
+
const renamed = new Map();
|
|
111
|
+
/** old local names that still have an element on them */
|
|
112
|
+
const remaining = new Set();
|
|
113
|
+
|
|
114
|
+
for (const el of facts.elements) {
|
|
115
|
+
if (!el.isDs || !PART_MAP[el.base] || el.component !== el.base) continue;
|
|
116
|
+
// An aliased import (`DataTableCell as Cell`): the local name is not the part's.
|
|
117
|
+
if (el.tag !== el.base) {
|
|
118
|
+
remaining.add(el.tag);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const to = PART_MAP[el.base];
|
|
122
|
+
const opening = openingOf(ts, el.node);
|
|
123
|
+
const inner = [];
|
|
124
|
+
let sortableDropped = false;
|
|
125
|
+
|
|
126
|
+
if (el.base === "DataTableHeader") {
|
|
127
|
+
const sortable = el.props.get("sortable");
|
|
128
|
+
if (el.spread) {
|
|
129
|
+
notTransformed.push({
|
|
130
|
+
line: el.line,
|
|
131
|
+
reason: "spread-props",
|
|
132
|
+
detail: `<${el.tag} {…}> — the spread may carry \`sortable\`; rename to \`TableHead\` by hand once you know it does not`,
|
|
133
|
+
});
|
|
134
|
+
remaining.add(el.tag);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (sortable) {
|
|
138
|
+
if (sortable.text !== "true") {
|
|
139
|
+
notTransformed.push({
|
|
140
|
+
line: el.line,
|
|
141
|
+
reason: "dynamic-sortable",
|
|
142
|
+
detail: `<${el.tag} sortable={…}> — whether the header sorts is not visible here; \`TableHead\` sorts whenever \`sort\` is passed`,
|
|
143
|
+
});
|
|
144
|
+
remaining.add(el.tag);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!el.props.has("sort") || el.props.has("defaultSort")) {
|
|
148
|
+
notTransformed.push({
|
|
149
|
+
line: el.line,
|
|
150
|
+
reason: "uncontrolled-sort",
|
|
151
|
+
detail: `<${el.tag} sortable> without a controlled \`sort\` — \`TableHead\` has no uncontrolled sort state; wire \`sort\` + \`onSortChange\` (from \`useDataTable\`) first`,
|
|
152
|
+
});
|
|
153
|
+
remaining.add(el.tag);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const drop = removeAttribute(ts, sf, opening, "sortable");
|
|
157
|
+
if (drop) {
|
|
158
|
+
inner.push(drop);
|
|
159
|
+
sortableDropped = true;
|
|
160
|
+
}
|
|
161
|
+
} else if (el.props.has("defaultSort")) {
|
|
162
|
+
notTransformed.push({
|
|
163
|
+
line: el.line,
|
|
164
|
+
reason: "uncontrolled-sort",
|
|
165
|
+
detail: `<${el.tag} defaultSort> — \`TableHead\` has no uncontrolled sort state`,
|
|
166
|
+
});
|
|
167
|
+
remaining.add(el.tag);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
inner.push({ pos: opening.tagName.getStart(sf), end: opening.tagName.getEnd(), text: to });
|
|
173
|
+
if (ts.isJsxElement(el.node)) {
|
|
174
|
+
const closing = el.node.closingElement.tagName;
|
|
175
|
+
inner.push({ pos: closing.getStart(sf), end: closing.getEnd(), text: to });
|
|
176
|
+
}
|
|
177
|
+
edits.push(...inner);
|
|
178
|
+
renamed.set(el.base, to);
|
|
179
|
+
findings.push({
|
|
180
|
+
line: el.line,
|
|
181
|
+
registryId: "C-DT-PARTS",
|
|
182
|
+
rule: "part-renamed",
|
|
183
|
+
match: `<${el.tag}>`,
|
|
184
|
+
component: el.component,
|
|
185
|
+
action: "applied",
|
|
186
|
+
gate: null,
|
|
187
|
+
severity: RULES["part-renamed"].severity,
|
|
188
|
+
detail: { from: el.base, to, sortableDropped },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The import: each renamed name becomes its `Table*` part when nothing else
|
|
193
|
+
// in the file still refers to it; otherwise the new name is added next to it.
|
|
194
|
+
if (renamed.size) {
|
|
195
|
+
for (const stmt of sf.statements) {
|
|
196
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
197
|
+
const spec = stmt.moduleSpecifier.text;
|
|
198
|
+
if (spec !== DS_PACKAGE && !spec.startsWith(`${DS_PACKAGE}/`)) continue;
|
|
199
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
200
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
201
|
+
const present = new Set(bindings.elements.map((e) => e.name.text));
|
|
202
|
+
const list = bindings.elements;
|
|
203
|
+
for (const [from, to] of renamed) {
|
|
204
|
+
const index = list.findIndex((e) => !e.propertyName && e.name.text === from);
|
|
205
|
+
if (index < 0) continue;
|
|
206
|
+
const element = list[index];
|
|
207
|
+
const keep = remaining.has(from) || hasOtherReferences(ts, sf, from);
|
|
208
|
+
if (keep) {
|
|
209
|
+
if (!present.has(to)) {
|
|
210
|
+
edits.push({ pos: element.getEnd(), text: `, ${to}` });
|
|
211
|
+
present.add(to);
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (present.has(to)) {
|
|
216
|
+
// `TableCell` is already imported: the old specifier goes.
|
|
217
|
+
const fromPos = index === 0 ? element.getStart(sf) : list[index - 1].getEnd();
|
|
218
|
+
const toPos = index === 0 && list.length > 1 ? list[1].getStart(sf) : element.getEnd();
|
|
219
|
+
edits.push({ pos: fromPos, end: toPos, text: "" });
|
|
220
|
+
} else {
|
|
221
|
+
edits.push({ pos: element.getStart(sf), end: element.getEnd(), text: to });
|
|
222
|
+
present.add(to);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
output: edits.length ? applyEdits(file.source, edits) : file.source,
|
|
230
|
+
findings,
|
|
231
|
+
notTransformed,
|
|
232
|
+
parseErrors: facts.parseErrors,
|
|
233
|
+
};
|
|
234
|
+
}
|