@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 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 | What it is |
56
- | --- | --- |
57
- | `./css/tokens.css` | The `--ds-*` design tokens: `:root`, the colour scheme scope and the brand scopes. Usable without any component. |
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` | A Tailwind `@theme` bridge, so `bg-surface/40`, `hover:text-fg-3` and `rounded-control` compile in *your* build instead of silently producing nothing. |
60
- | `./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. |
61
- | `./css/base.css` | Optional document defaults: body background, ink, font and `color-scheme`. Ships no preflight, no element rules and no font import. |
62
- | `./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. |
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 *component prop >
123
- provider > English default*, per key. The full English list is
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 *and* the client.
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 | Example (namespaced name → value) | Reads |
157
- | --- | --- | --- |
158
- | reference | `--ds-ref-brand-pro-fg: #6c42f8` | a literal; the raw ramps, one per brand |
159
- | system (semantic) | `--ds-color-action-brand-bg: var(--ds-ref-brand-pro-fg)` | the reference tier; **this is the public contract** |
160
- | component | `--ds-menu-item-hover-bg: var(--ds-color-canvas-sunken)` | the system tier; only where a component must be themable on its own |
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()`)
@@ -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
+ }