@assure-one/design-system 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +48 -28
  2. package/codemods/README.md +24 -7
  3. package/codemods/lib/registry.mjs +27 -0
  4. package/codemods/lib/runner.mjs +8 -3
  5. package/codemods/run.mjs +27 -5
  6. package/codemods/transforms/cm-15-dom-selectors.mjs +16 -9
  7. package/dist/brand-scope-BOHId6BY.d.ts +58 -0
  8. package/dist/{brand-theme-B6rem2II.d.ts → brand-theme-ZYM867Jz.d.ts} +1 -1
  9. package/dist/chunk-CPOGSNXS.js +389 -0
  10. package/dist/chunk-CPOGSNXS.js.map +1 -0
  11. package/dist/chunk-EUHGZA6Q.js +313 -0
  12. package/dist/chunk-EUHGZA6Q.js.map +1 -0
  13. package/dist/chunk-KUSY7BLL.js +441 -0
  14. package/dist/chunk-KUSY7BLL.js.map +1 -0
  15. package/dist/chunk-NBI7E3VT.js +3754 -0
  16. package/dist/chunk-NBI7E3VT.js.map +1 -0
  17. package/dist/chunk-PTI4B2OI.js +5527 -0
  18. package/dist/chunk-PTI4B2OI.js.map +1 -0
  19. package/dist/chunk-X76J4QSJ.js +164 -0
  20. package/dist/chunk-X76J4QSJ.js.map +1 -0
  21. package/dist/chunk-XV2VSEA5.js +4339 -0
  22. package/dist/chunk-XV2VSEA5.js.map +1 -0
  23. package/dist/css/components.css +1 -1
  24. package/dist/css/legacy-aliases.css +2 -0
  25. package/dist/css/tailwind.css +2 -0
  26. package/dist/css/tokens.css +2 -0
  27. package/dist/data-table-view-ep8al7P9.d.ts +1132 -0
  28. package/dist/{design-system-provider-BM_F1C13.d.ts → design-system-provider-CfD2g2lY.d.ts} +11 -12
  29. package/dist/icons/index.d.ts +189 -140
  30. package/dist/icons/index.js +2 -1835
  31. package/dist/icons/index.js.map +1 -1
  32. package/dist/index.d.ts +374 -389
  33. package/dist/index.js +8688 -24271
  34. package/dist/index.js.map +1 -1
  35. package/dist/legacy/index.d.ts +1462 -7
  36. package/dist/legacy/index.js +5 -1
  37. package/dist/legacy/index.js.map +1 -1
  38. package/dist/next/index.d.ts +2 -2
  39. package/dist/styles.css +2 -1
  40. package/dist/suite/index.d.ts +991 -4
  41. package/dist/suite/index.js +6 -1
  42. package/dist/suite/index.js.map +1 -1
  43. package/dist/testing/index.cjs +3 -2
  44. package/dist/testing/index.js +3 -2
  45. package/dist/tokens/index.d.ts +1 -1
  46. package/dist/tokens/index.js +3 -856
  47. package/dist/tokens/index.js.map +1 -1
  48. package/dist/{types-k_kjwipx.d.ts → types-Coqw8iaS.d.ts} +28 -1
  49. package/docs/components.md +56 -56
  50. package/docs/components.registry.json +7 -7
  51. package/docs/for-ai-agents.md +12 -6
  52. package/package.json +8 -5
  53. package/dist/index-BAAuIQqP.d.ts +0 -2579
  54. package/dist/index-DxBjmBBp.d.ts +0 -1038
package/README.md CHANGED
@@ -27,13 +27,23 @@ The single source of truth for visual design across all Assure products.
27
27
  pnpm add @assure-one/design-system
28
28
  ```
29
29
 
30
- Import the consumer stylesheet once in your root layout. Its bundled component
31
- utilities intentionally live below your app's Tailwind `utilities` layer, so
32
- local classes and responsive variants remain authoritative even when a bundler
33
- reorders CSS chunks:
30
+ Load the design system's CSS from your app's global stylesheet, as the split
31
+ entries, under one layer statement (Tailwind 4 in your build):
34
32
 
35
- ```ts
36
- import "@assure-one/design-system/styles.css";
33
+ ```css
34
+ /* src/app/globals.css — the layer statement is the first line of the file */
35
+ @layer theme, base, ds, components, utilities;
36
+
37
+ @import "tailwindcss";
38
+
39
+ @import "@assure-one/design-system/css/tokens.css";
40
+ @import "@assure-one/design-system/css/legacy-aliases.css"; /* optional */
41
+ @import "@assure-one/design-system/css/tailwind.css";
42
+ @import "@assure-one/design-system/css/shadcn.css"; /* optional */
43
+ @import "@assure-one/design-system/css/base.css"; /* optional */
44
+ @import "@assure-one/design-system/css/components.css";
45
+
46
+ @import "./ds-preset.css"; /* your app's preset, optional */
37
47
  ```
38
48
 
39
49
  Then use components:
@@ -46,32 +56,42 @@ export default function Page() {
46
56
  }
47
57
  ```
48
58
 
49
- ### The split CSS entries (experimental)
59
+ ### The split CSS entries
50
60
 
51
- `styles.css` stays the supported way to consume the design system, unchanged.
52
- Alongside it, the package now also publishes the individual stylesheets the new
53
- CSS mode is built from. They are **experimental**: nothing requires them yet,
54
- no component reads them, and importing them changes nothing on its own.
61
+ The split entries are the documented default CSS integration since 1.40. Every
62
+ design-system class is `ds:`-prefixed and compiled into `css/components.css`
63
+ inside `@layer ds`, so your own utilities always win over component styles and
64
+ your Tailwind build never has to scan our package.
55
65
 
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. |
66
+ | Subpath | What it is |
67
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `./css/tokens.css` | The `--ds-*` design tokens: `:root`, the colour scheme scope and the brand scopes. Usable without any component. |
69
+ | `./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. |
70
+ | `./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. |
71
+ | `./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. |
72
+ | `./css/base.css` | Optional document defaults: body background, ink, font and `color-scheme`. Ships no preflight, no element rules and no font import. |
73
+ | `./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. |
64
74
 
65
75
  Import order is `tokens.css` → `legacy-aliases.css` (optional) → `tailwind.css`
66
76
  → `shadcn.css` (optional) → `base.css` (optional) → `components.css`, after your own
67
77
  `@import "tailwindcss"` and under the layer statement
68
78
  `@layer theme, base, ds, components, utilities;`.
69
79
  [**`docs/integration/css.md`**](./docs/integration/css.md) has the full
70
- sequence, where the per-app preset goes, when the `@source` into our package may
71
- be removed, and the fixture run behind each of those claims. The reasoning lives
80
+ sequence, where the per-app preset goes, why no `@source` into our package is
81
+ needed, and the fixture run behind each of those claims. The reasoning lives
72
82
  in [ADR-004](./docs/adr/004-css-delivery-cascade.md) and
73
83
  [ADR-005](./docs/adr/005-app-vocabulary-bridge.md).
74
84
 
85
+ ### `styles.css` (deprecated)
86
+
87
+ `@assure-one/design-system/styles.css` — the all-in-one sheet with its own
88
+ preflight, a Google Fonts `@import` and the frozen unprefixed legacy utilities —
89
+ is **deprecated since 1.40** (registry `C-CSS-STYLES`). It still ships, unchanged,
90
+ and is removed in 3.0; the built file opens with a deprecation
91
+ banner. Moving to the split entries is a deliberate visual change (document font
92
+ size, fonts, preflight): follow [`docs/integration/css.md`](./docs/integration/css.md)
93
+ §6 and [`docs/migration/1.40.md`](./docs/migration/1.40.md).
94
+
75
95
  ### Runtime brand theming (experimental)
76
96
 
77
97
  For a tenant colour known only at runtime, `@assure-one/design-system/tokens`
@@ -107,8 +127,9 @@ is required to get a readable lockup.
107
127
  ### The provider and the `/next` entry (experimental)
108
128
 
109
129
  Nothing requires a provider: every component renders English strings, formats
110
- with `en-US` and (for `LinkButton` / `Logo`) routes through `next/link` on its
111
- own. `DesignSystemProvider` is the one optional, thin, client-side provider
130
+ with `en-US` and (for `LinkButton` / `Logo`) renders a plain `<a>` / `<img>`.
131
+ On Next.js, render `NextDesignSystemProvider` to get `next/link` routing and
132
+ `next/image`: the root imports no `next/*` since 2.0. `DesignSystemProvider` is the one optional, thin, client-side provider
112
133
  for what CSS cannot do — the generic strings, the locale, the text direction,
113
134
  the portal container, the link and image adapters, shared tooltip defaults and
114
135
  (opt-in) the toast region. It renders no DOM wrapper ([ADR-010](./docs/adr/010-design-system-provider.md)).
@@ -143,11 +164,10 @@ export default function RootLayout({ children }) {
143
164
  Pass the locale your app already knows server-side; the design system never
144
165
  reads `navigator.language` during render, so server and client HTML agree.
145
166
  - **`linkComponent` / `imageComponent`** — what `LinkButton` and `Logo` render
146
- through. Without them the components keep their static `next/link` /
147
- `next/image` imports until 2.0, so a Next.js app that renders no provider is
148
- unchanged. A non-Next app passes its router's link; a Next.js app uses
149
- `NextDesignSystemProvider` so it is ready for the 2.0 removal of the static
150
- imports.
167
+ through. Without them the components render a plain `<a>` / `<img>` (full
168
+ page navigation). A non-Next app passes its router's link; a Next.js app uses
169
+ `NextDesignSystemProvider`. `next` is an optional peer, needed only by the
170
+ `/next` entry.
151
171
  - **`portalContainer`**, **`dir`**, **`tooltip`**, **`toasts`** — documented on
152
172
  the props. `toasts` mounts the toast region for `useToast()`; leave it off if
153
173
  you already render `ToastProvider`.
@@ -19,6 +19,9 @@ node node_modules/@assure-one/design-system/codemods/run.mjs CM-02 src --report
19
19
 
20
20
  # Everything this project is missing, in program order
21
21
  node node_modules/@assure-one/design-system/codemods/run.mjs upgrade --dry
22
+
23
+ # Only what a project needs before 1.40 (docs/migration/1.40.md)
24
+ node node_modules/@assure-one/design-system/codemods/run.mjs upgrade --to 1.40 --dry
22
25
  ```
23
26
 
24
27
  There is deliberately **no** `bin` entry and **no** `exports` entry for the
@@ -30,6 +33,7 @@ is `pnpm codemods <id> <paths>`.
30
33
  | Option | Effect |
31
34
  | ------------------------------- | ------------------------------------------------------------------------------ |
32
35
  | `--dry` | report what would happen; write neither files nor the ledger |
36
+ | `--to <version>` | `upgrade` only: run the bundle that release needs (`--to 1.40`, see below) |
33
37
  | `--root <dir>` | project root (default: the nearest `package.json` above the working directory) |
34
38
  | `--report <prefix>` | write `<prefix>.json` and `<prefix>.md` |
35
39
  | `--json <file>` / `--md <file>` | write one of the two |
@@ -96,6 +100,19 @@ entry. Report-only codemods and dry runs never touch it. The consumer scanner
96
100
  reads the file and the migration dashboard shows the applied ids
97
101
  (`migration-status/<app>.json` → `codemodLedger`).
98
102
 
103
+ ## `upgrade --to 1.40`
104
+
105
+ `upgrade --to 1.40` runs, in program order, the codemods of the 1.40 bundle
106
+ (`UPGRADE_TARGETS` in `lib/registry.mjs`): one per registry contract that 1.40
107
+ removes or flips (plan §21.2) — CM-01, CM-04, CM-19, CM-12, and the finders
108
+ CM-15 and CM-16. A transforming codemod already in the ledger is skipped, so
109
+ the command is safe to repeat and safe from any 1.x; the report-only finders
110
+ run every time. With `--report <prefix>` each codemod writes
111
+ `<prefix>-<id>.json` and `.md`. The migration guide is
112
+ [`docs/migration/1.40.md`](../docs/migration/1.40.md). `--to 2` (also `2.0`,
113
+ `v2`) still works: it is the name the bundle had while Wave 9 was planned as
114
+ 2.0 (decision D11 shipped it as 1.40).
115
+
99
116
  ## Reports
100
117
 
101
118
  Every run produces the same report: a JSON document (`schema`, the codemod, the
@@ -136,8 +153,8 @@ Class R. W8-02 gave the shared Assure patterns the `/suite` entry and the
136
153
  frozen and product-owned modules `/legacy`. Both are re-export barrels over the
137
154
  same module instances the root exports, so moving an import is a pure path
138
155
  change: no duplicated React context, no behaviour difference. The root keeps
139
- every name through 1.x (registry `C-ROOT-SUITE`) and drops them in 2.0, so
140
- running this codemod early costs nothing and running it late is a 2.0 blocker.
156
+ every name through 1.39 (registry `C-ROOT-SUITE`) and drops them in 1.40, so
157
+ running this codemod early costs nothing and running it late is a 1.40 blocker.
141
158
 
142
159
  ```tsx
143
160
  import { Button, AgreementViewer, TimeLogger } from "@assure-one/design-system";
@@ -306,7 +323,7 @@ components with the same names, test files.
306
323
 
307
324
  Class R. `ProgressBar` and `ProgressRing` colour themselves **by value** when
308
325
  no colour is passed (`< 50%` brand, `≥ 50%` warning, `≥ 80%` success) — a
309
- business mapping the design system should not own, which 2.0 switches off
326
+ business mapping the design system should not own, which 1.40 switches off
310
327
  (W9-10, contract C-PROGRESS). W3-21 added `intent`; passing it disables the
311
328
  rule. CM-19 makes every call site say what it renders today, so the flip
312
329
  changes nothing a consumer did not write down: `variant="<legacy word>"`
@@ -338,7 +355,7 @@ gets `size="md"` — the value the component resolves today
338
355
  (`defaultVariants.size`, 40px), so nothing moves on screen. It exists because
339
356
  a size _meaning_ can only change once no call site depends on the default:
340
357
  after CM-02, the D5 size flip (CM-03 phase 2, W9-06) touches nothing a
341
- consumer did not write down, and 2.0 gate G6 counts exactly this.
358
+ consumer did not write down, and gate G6 counts exactly this.
342
359
 
343
360
  The attribute goes after `variant` when there is one (the order the
344
361
  applications use) and first otherwise, on its own line when the element's
@@ -357,7 +374,7 @@ scanner that measures G6 counts production files only).
357
374
  ### CM-12 — Button `type="submit"` where evident
358
375
 
359
376
  Class R. An untyped `Button` renders `<button>` without a `type`, which the
360
- browser treats as `submit`; 2.0 flips the default to `"button"` (W9-05,
377
+ browser treats as `submit`; 1.40 flips the default to `"button"` (W9-05,
361
378
  C-BTN-TYPE, gate G4). CM-12 writes down, before the flip, which Buttons submit
362
379
  on purpose — and only where the file itself makes that evident:
363
380
 
@@ -598,8 +615,8 @@ yet — those are the candidates for new registry entries. `confidence` is `high
598
615
  for a token on the component itself, `medium` for a wrapper or a heuristic,
599
616
  `low` for an unmapped wrapper.
600
617
 
601
- CM-15 feeds 2.0 gate G7 (`consumerSelectors = 0` for the freezes with removal
602
- target 2.0). Use `--fail-on-findings` to gate on it. The committed findings for
618
+ CM-15 feeds gate G7 (`consumerSelectors = 0` for the freezes with registry
619
+ removal target 2.0, which 1.40 lifts). Use `--fail-on-findings` to gate on it. The committed findings for
603
620
  the four consumer apps are in `docs/codemods/`.
604
621
 
605
622
  ### CM-16 — `globals.css` analyser
@@ -22,6 +22,33 @@ export const CODEMODS = [
22
22
  { id: "CM-20", module: "../transforms/cm-20-select-sentinels.mjs" },
23
23
  ];
24
24
 
25
+ /**
26
+ * `upgrade --to <version>`: the codemods a 1.x project runs before it installs
27
+ * that release, by the registry contracts the release removes or flips (plan
28
+ * §21.2). Applied in program order; transforming ones already in the ledger
29
+ * are skipped, report-only ones run every time.
30
+ *
31
+ * 1.40 (Wave 9, D11) — CM-01 root suite/legacy/accidental exports leave the
32
+ * root (C-ROOT-SUITE, C-ROOT-ACCIDENTAL) · CM-04 `variant="accent"` removed
33
+ * (C-BTN-ACCENT) · CM-19 Progress auto-colour off by default (C-PROGRESS) ·
34
+ * CM-12 Button `type="button"` default (C-BTN-TYPE) · CM-15 the 1.40 DOM
35
+ * freezes are lifted (C-DOM-*, C-TOAST-ARIALABEL) · CM-16 `styles.css`
36
+ * deprecated (C-CSS-STYLES). CM-02 is not in it: D5 keeps today's size scale.
37
+ */
38
+ export const UPGRADE_TARGETS = {
39
+ "1.40": ["CM-01", "CM-04", "CM-19", "CM-12", "CM-15", "CM-16"],
40
+ };
41
+
42
+ /** Older names of a target: the 1.40 bundle was `--to 2` while Wave 9 was planned as 2.0. */
43
+ const UPGRADE_ALIASES = { 2: "1.40" };
44
+
45
+ /** The bundle for `--to 1.40`, `1.40.0`, `v1.40` (or the alias `--to 2`, `2.0`, `v2`…), in program order, or `null`. */
46
+ export function upgradeBundle(to) {
47
+ const [major, minor] = String(to).trim().replace(/^v/i, "").split(".");
48
+ const ids = UPGRADE_TARGETS[UPGRADE_ALIASES[major] ?? `${major}.${Number(minor)}`];
49
+ return ids ? CODEMODS.filter((c) => ids.includes(c.id)).map((c) => c.id) : null;
50
+ }
51
+
25
52
  export const findCodemod = (id) =>
26
53
  CODEMODS.find((c) => c.id.toLowerCase() === String(id).toLowerCase()) ?? null;
27
54
 
@@ -170,14 +170,19 @@ const countBy = (items, key) => {
170
170
  return Object.fromEntries([...map].sort(([a], [b]) => (a < b ? -1 : 1)));
171
171
  };
172
172
 
173
- /** Codemods a project has not applied yet, in program order. */
174
- export async function pendingCodemods(root) {
173
+ /**
174
+ * Codemods a project has not applied yet, in program order. With `only` (an
175
+ * `upgrade --to` bundle) the list is limited to those ids and keeps their
176
+ * report-only codemods, which never enter the ledger.
177
+ */
178
+ export async function pendingCodemods(root, { only = null } = {}) {
175
179
  const { CODEMODS } = await import("./registry.mjs");
176
180
  const ids = appliedIds(readLedger(root));
177
181
  const out = [];
178
182
  for (const { id } of CODEMODS) {
183
+ if (only && !only.includes(id)) continue;
179
184
  const mod = await loadCodemod(id);
180
185
  out.push({ ...mod.meta, applied: ids.has(id) });
181
186
  }
182
- return out.filter((m) => m.class !== "X" && !m.applied);
187
+ return out.filter((m) => (m.class === "X" ? Boolean(only) : !m.applied));
183
188
  }
package/codemods/run.mjs CHANGED
@@ -7,9 +7,12 @@
7
7
  * node node_modules/@assure-one/design-system/codemods/run.mjs list
8
8
  * node node_modules/@assure-one/design-system/codemods/run.mjs CM-15 src --dry
9
9
  * node node_modules/@assure-one/design-system/codemods/run.mjs upgrade --dry
10
+ * node node_modules/@assure-one/design-system/codemods/run.mjs upgrade --to 1.40 --dry
10
11
  *
11
12
  * Options
12
13
  * --dry report only; write neither files nor the ledger
14
+ * --to <version> upgrade: only the codemods that release needs (`--to 1.40`;
15
+ * `--to 2` is an alias)
13
16
  * --root <dir> project root (default: nearest package.json above cwd)
14
17
  * --report <prefix> write <prefix>.json and <prefix>.md
15
18
  * --json <file> write the JSON report
@@ -27,11 +30,12 @@ import { dirname, resolve } from "node:path";
27
30
  import { findProjectRoot } from "./lib/files.mjs";
28
31
  import { installedDsVersion } from "./lib/environment.mjs";
29
32
  import { renderMarkdown, renderSummary } from "./lib/report.mjs";
30
- import { CODEMODS, loadAll } from "./lib/registry.mjs";
33
+ import { readLedger } from "./lib/ledger.mjs";
34
+ import { CODEMODS, UPGRADE_TARGETS, loadAll, upgradeBundle } from "./lib/registry.mjs";
31
35
  import { Refused, pendingCodemods, runCodemod } from "./lib/runner.mjs";
32
36
 
33
37
  const FLAGS = new Set(["--dry", "--print", "--quiet", "--fail-on-findings", "--help", "-h"]);
34
- const VALUES = new Set(["--root", "--report", "--json", "--md"]);
38
+ const VALUES = new Set(["--root", "--report", "--json", "--md", "--to"]);
35
39
 
36
40
  function parseArgs(argv) {
37
41
  const options = {};
@@ -55,7 +59,7 @@ class UsageError extends Error {}
55
59
  const usage = () => `Usage:
56
60
  run.mjs list
57
61
  run.mjs <CM-ID> [paths…] [--dry] [--root dir] [--report prefix] [--print] [--fail-on-findings]
58
- run.mjs upgrade [paths…] [--dry]
62
+ run.mjs upgrade [paths…] [--to <version>] [--dry]
59
63
 
60
64
  Codemods: ${CODEMODS.map((c) => c.id).join(", ")} (design system ${installedDsVersion()})
61
65
  `;
@@ -112,7 +116,21 @@ async function main(argv) {
112
116
  }
113
117
 
114
118
  if (command === "upgrade") {
115
- const pending = await pendingCodemods(root);
119
+ const only = options.to ? upgradeBundle(options.to) : null;
120
+ if (options.to && !only) {
121
+ throw new UsageError(
122
+ `--to ${options.to}: no upgrade bundle (known: ${Object.keys(UPGRADE_TARGETS).join(", ")}; 2 is an alias of 1.40)`,
123
+ );
124
+ }
125
+ const pending = await pendingCodemods(root, { only });
126
+ if (only) {
127
+ const recorded = new Set(readLedger(root).applied.map((e) => e.id));
128
+ const skipped = only.filter((id) => recorded.has(id));
129
+ say(
130
+ `Upgrade to ${options.to}: ${pending.map((m) => m.id).join(", ")}` +
131
+ (skipped.length ? `; already in the ledger: ${skipped.join(", ")}` : ""),
132
+ );
133
+ }
116
134
  if (!pending.length) {
117
135
  say(`Nothing to apply in ${root}; the ledger is up to date.`);
118
136
  return 0;
@@ -125,8 +143,12 @@ async function main(argv) {
125
143
  dry: Boolean(options.dry),
126
144
  });
127
145
  say(renderSummary(report));
146
+ for (const warning of report.warnings) say(`! ${warning}`);
128
147
  if (options.print) process.stdout.write(renderMarkdown(report));
129
- for (const path of writeReport(report, options)) say(`Report: ${path}`);
148
+ const prefix = options.report && pending.length > 1 ? `${options.report}-${meta.id}` : null;
149
+ for (const path of writeReport(report, prefix ? { report: prefix } : options))
150
+ say(`Report: ${path}`);
151
+ if (report.ledger.recorded) say(`Recorded ${meta.id} in ${root}/${report.ledger.file}`);
130
152
  if (options["fail-on-findings"] && report.summary.registered) code = 1;
131
153
  }
132
154
  return code;
@@ -17,8 +17,9 @@
17
17
  *
18
18
  * Findings without a registry id are reported as `unregistered`: they are
19
19
  * DOM coupling the registry does not track yet. Radix `data-state`-style
20
- * attributes, ARIA roles and cmdk attributes are permanent contracts
21
- * (P-DATA-STATE, P-ROLES, C-CMDK-ATTR) and are not reported.
20
+ * attributes, ARIA roles, cmdk attributes and the `dl/dt/dd` of
21
+ * MetadataGrid/DataItem are permanent contracts (P-DATA-STATE, P-ROLES,
22
+ * C-CMDK-ATTR, P-NATIVE-TABLE) and are not reported.
22
23
  *
23
24
  * Never modifies files.
24
25
  */
@@ -185,16 +186,19 @@ const TAG_RULES = [
185
186
  { id: "C-DOM-12", components: /^ProgressBar$/, test: has(/role/) },
186
187
  { id: "C-DOM-13", components: /^(?:DataTableHeader|DataTableHead)$/, test: has(/button/) },
187
188
  { id: "C-DOM-15", components: /^DocumentRequestCard/, test: any },
188
- {
189
- id: "C-DOM-16",
190
- components: /^(?:MetadataGrid|DataItem)$/,
191
- test: has(/(?:^|[_>\s])(?:dt|dd|span)\b/),
192
- },
189
+ { id: "C-DOM-16", components: /^(?:MetadataGrid|DataItem)$/, test: has(/(?:^|[_>\s])span\b/) },
193
190
  ];
194
191
 
195
192
  const ruleFor = (component, reach) =>
196
193
  TAG_RULES.find((r) => r.components.test(component) && r.test(reach)) ?? null;
197
194
 
195
+ // `dl/dt/dd` inside MetadataGrid/DataItem are permanent (P-NATIVE-TABLE;
196
+ // only the `span` part of C-DOM-16 is frozen), so selectors on them are not
197
+ // reported.
198
+ const isPermanent = (component, reach) =>
199
+ /^(?:MetadataGrid|DataItem)$/.test(component) &&
200
+ /(?:^|[_>\s])(?:dl|dt|dd)\b/.test(reach.selector);
201
+
198
202
  /**
199
203
  * Whether an unmapped selector on a *wrapper* element is worth reporting: it
200
204
  * must name an element, `*`, or a `data-`/`role` attribute. A wrapper selector
@@ -310,6 +314,7 @@ function codeFindings(file, ts) {
310
314
  const reach = domReach(token);
311
315
  if (!reach) continue;
312
316
  const rule = ruleFor(usage.component, reach) ?? ruleFor(usage.base, reach);
317
+ if (!rule && isPermanent(usage.base, reach)) continue;
313
318
  add({
314
319
  line,
315
320
  registryId: rule?.id ?? null,
@@ -332,6 +337,7 @@ function codeFindings(file, ts) {
332
337
  .map((c) => ({ c, rule: ruleFor(c, reach) }))
333
338
  .filter((x) => x.rule && x.rule.test !== any);
334
339
  const hit = rules[0];
340
+ if (!hit && el.dsDescendants.some((c) => isPermanent(c, reach))) continue;
335
341
  if (!hit && !namesStructure(reach.selector)) continue;
336
342
  add({
337
343
  line,
@@ -381,12 +387,13 @@ function codeFindings(file, ts) {
381
387
  });
382
388
  }
383
389
 
384
- // 4. MetadataGrid / DataItem wrappers style dt/dd from anywhere in the file.
390
+ // 4. MetadataGrid / DataItem wrappers style their `span` from anywhere in
391
+ // the file (`dt/dd` are permanent, see `isPermanent`).
385
392
  if (used.has("MetadataGrid") || used.has("DataItem")) {
386
393
  const reported = new Set(findings.map((f) => `${f.line}${f.match}`));
387
394
  for (const { token, line } of facts.fileTokens) {
388
395
  const reach = domReach(token);
389
- if (!reach || !/(?:^|[_>\s])(?:dt|dd)\b/.test(reach.selector)) continue;
396
+ if (!reach || !/(?:^|[_>\s])span\b/.test(reach.selector)) continue;
390
397
  if (reported.has(`${line}${token}`)) continue;
391
398
  add({
392
399
  line,
@@ -0,0 +1,58 @@
1
+ import * as React$1 from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Brand scope — ambient product theming.
6
+ *
7
+ * The `brand` color family (bg-brand, hover:bg-brand-hover, bg-brand-bg,
8
+ * border-brand-line, text-fg-on-brand, plus the accent/focus/primary
9
+ * interaction tokens) follows the nearest `[data-brand]` scope and defaults to
10
+ * Pro. Authors type the STATE only; the product comes from the scope. Set it
11
+ * once high in the tree — ideally on the same root as the `.dark` class
12
+ * (`<html>`/`<body>`) — so Radix-portalled surfaces inherit it through the DOM.
13
+ *
14
+ * <html data-brand="tax"> // whole shell renders teal
15
+ * <div {...brandScope("audit")}> // a subtree renders blue
16
+ *
17
+ * For the rarer case of multiple scopes on one page WITH portalled content
18
+ * (a Dialog/Popover that escapes its scoped subtree to document.body), wrap the
19
+ * region in <BrandScope> / <BrandScopeProvider> and have the portalled
20
+ * `*Content` re-stamp `data-brand={useBrandScope()}` on its own element.
21
+ */
22
+ type BrandProduct = "pro" | "audit" | "books" | "tax";
23
+ /**
24
+ * `data-brand` attribute object to spread on any element (mirrors
25
+ * `serviceToneStyle`). This is the primary API for opting a subtree into a
26
+ * product without naming the product in a utility class.
27
+ */
28
+ declare function brandScope(product: BrandProduct): {
29
+ "data-brand": BrandProduct;
30
+ };
31
+ /** Every product, in suite order — for switchers and story matrices. */
32
+ declare const BRAND_PRODUCTS: BrandProduct[];
33
+ /** Human label for a product, for switchers and headings. */
34
+ declare const brandLabel: Record<BrandProduct, string>;
35
+ /**
36
+ * Provides the active product to descendants so portalled surfaces can
37
+ * re-stamp `data-brand` on themselves via {@link useBrandScope}. Purely a
38
+ * structural carrier — the visual theming still comes from the `[data-brand]`
39
+ * attribute in the DOM, not from this context.
40
+ */
41
+ declare function BrandScopeProvider({ product, children, }: {
42
+ product: BrandProduct;
43
+ children: React.ReactNode;
44
+ }): react_jsx_runtime.JSX.Element;
45
+ /** Read the ambient product from the nearest provider (defaults to Pro). */
46
+ declare function useBrandScope(): BrandProduct;
47
+ interface BrandScopeProps extends React.HTMLAttributes<HTMLDivElement> {
48
+ /** The product this subtree renders as. */
49
+ product: BrandProduct;
50
+ }
51
+ /**
52
+ * Convenience wrapper that stamps `data-brand` on a `<div>` AND provides the
53
+ * product via context, so a portalled descendant can re-stamp itself. Use
54
+ * `brandScope()` directly when you want the attribute on your own element.
55
+ */
56
+ declare const BrandScope: React$1.ForwardRefExoticComponent<BrandScopeProps & React$1.RefAttributes<HTMLDivElement>>;
57
+
58
+ export { BRAND_PRODUCTS as B, type BrandProduct as a, brandLabel as b, BrandScope as c, type BrandScopeProps as d, BrandScopeProvider as e, brandScope as f, useBrandScope as u };
@@ -14,7 +14,7 @@ interface BrandThemeSeed {
14
14
  * `#rrggbb`. `undefined` or `null` means "the same as `brand`", which is what
15
15
  * the built-in `[data-ds-brand]` scopes do.
16
16
  */
17
- accent?: string | null;
17
+ accent?: string | null | undefined;
18
18
  }
19
19
  /** Every custom property `createBrandTheme()` sets, in output order. */
20
20
  declare const BRAND_THEME_PROPERTIES: readonly ["--ds-color-action-brand-bg", "--ds-color-action-brand-bg-hover", "--ds-color-action-brand-bg-active", "--ds-color-action-brand-bg-disabled", "--ds-color-action-brand-subtle-bg", "--ds-color-action-brand-subtle-bg-hover", "--ds-color-action-brand-subtle-bg-active", "--ds-color-action-brand-border", "--ds-color-action-brand-ring", "--ds-color-action-brand-fg", "--ds-color-action-accent-bg", "--ds-color-action-accent-bg-strong", "--ds-color-action-accent-bg-hover", "--ds-color-action-accent-bg-active", "--ds-color-action-accent-subtle-bg", "--ds-color-action-accent-ring", "--ds-color-action-accent-fg", "--ds-color-border-focus", "--ds-focus-ring-color", "--ds-focus-ring-border", "--ds-gradient-brand", "--ds-compat-color-brand-soft", "--ds-compat-color-brand-soft-foreground"];