@assure-one/design-system 1.38.0 → 1.40.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 (68) hide show
  1. package/README.md +63 -28
  2. package/codemods/README.md +76 -25
  3. package/codemods/lib/entry-map.mjs +315 -0
  4. package/codemods/lib/registry.mjs +28 -0
  5. package/codemods/lib/runner.mjs +8 -3
  6. package/codemods/run.mjs +27 -5
  7. package/codemods/transforms/cm-01-entry-imports.mjs +202 -0
  8. package/codemods/transforms/cm-15-dom-selectors.mjs +16 -9
  9. package/dist/brand-scope-BOHId6BY.d.ts +58 -0
  10. package/dist/brand-theme-ZYM867Jz.d.ts +59 -0
  11. package/dist/chunk-2MKLYYIY.js +3754 -0
  12. package/dist/chunk-2MKLYYIY.js.map +1 -0
  13. package/dist/chunk-6S2R2VWN.js +5527 -0
  14. package/dist/chunk-6S2R2VWN.js.map +1 -0
  15. package/dist/chunk-7U3ADR2Q.js +4336 -0
  16. package/dist/chunk-7U3ADR2Q.js.map +1 -0
  17. package/dist/chunk-CPOGSNXS.js +389 -0
  18. package/dist/chunk-CPOGSNXS.js.map +1 -0
  19. package/dist/chunk-EUHGZA6Q.js +313 -0
  20. package/dist/chunk-EUHGZA6Q.js.map +1 -0
  21. package/dist/chunk-KQBYTTAN.js +1829 -0
  22. package/dist/chunk-KQBYTTAN.js.map +1 -0
  23. package/dist/chunk-X76J4QSJ.js +164 -0
  24. package/dist/chunk-X76J4QSJ.js.map +1 -0
  25. package/dist/css/components.css +1 -1
  26. package/dist/css/legacy-aliases.css +2 -0
  27. package/dist/css/tailwind.css +2 -0
  28. package/dist/css/tokens.css +8 -6
  29. package/dist/data-table-view-ep8al7P9.d.ts +1132 -0
  30. package/dist/design-system-provider-CfD2g2lY.d.ts +142 -0
  31. package/dist/icons/index.d.ts +441 -3
  32. package/dist/icons/index.js +2 -1835
  33. package/dist/icons/index.js.map +1 -1
  34. package/dist/index.d.ts +404 -3847
  35. package/dist/index.js +8728 -24213
  36. package/dist/index.js.map +1 -1
  37. package/dist/legacy/index.d.ts +1464 -0
  38. package/dist/legacy/index.js +7 -0
  39. package/dist/legacy/index.js.map +1 -0
  40. package/dist/next/index.d.ts +2 -1
  41. package/dist/reference-BkDbrPIB.d.ts +310 -0
  42. package/dist/styles.css +2 -1
  43. package/dist/suite/index.d.ts +989 -0
  44. package/dist/suite/index.js +8 -0
  45. package/dist/suite/index.js.map +1 -0
  46. package/dist/{system-CyAVbHhM.d.ts → system-DfJIVBlJ.d.ts} +1 -310
  47. package/dist/testing/index.cjs +3 -2
  48. package/dist/testing/index.js +3 -2
  49. package/dist/tokens/index.d.ts +3 -61
  50. package/dist/tokens/index.js +3 -856
  51. package/dist/tokens/index.js.map +1 -1
  52. package/dist/{design-system-provider-B5MbvbRc.d.ts → types-Coqw8iaS.d.ts} +36 -142
  53. package/dist/vocab-DornLJbW.d.ts +76 -0
  54. package/docs/components.md +54 -52
  55. package/docs/components.registry.json +22 -8
  56. package/docs/for-ai-agents.md +12 -6
  57. package/package.json +17 -4
  58. package/public/brand/assure-audit-mark.svg +6 -0
  59. package/public/brand/assure-audit.svg +8 -0
  60. package/public/brand/assure-books-mark.svg +6 -0
  61. package/public/brand/assure-books.svg +8 -0
  62. package/public/brand/assure-pro-mark.svg +6 -0
  63. package/public/brand/assure-pro.svg +8 -0
  64. package/public/brand/assure-tax-mark.svg +6 -0
  65. package/public/brand/assure-tax.svg +8 -0
  66. package/public/brand/mycpe-teams-mark.svg +36 -0
  67. package/public/brand/mycpe-teams.svg +66 -0
  68. package/dist/index-vztxMSfl.d.ts +0 -512
@@ -3,6 +3,7 @@
3
3
  * codemods listed here can be run by id; the sequence is the plan's.
4
4
  */
5
5
  export const CODEMODS = [
6
+ { id: "CM-01", module: "../transforms/cm-01-entry-imports.mjs" },
6
7
  { id: "CM-05", module: "../transforms/cm-05-button-icon-slots.mjs" },
7
8
  { id: "CM-13", module: "../transforms/cm-13-tooltip-delay.mjs" },
8
9
  { id: "CM-07", module: "../transforms/cm-07-input-size.mjs" },
@@ -21,6 +22,33 @@ export const CODEMODS = [
21
22
  { id: "CM-20", module: "../transforms/cm-20-select-sentinels.mjs" },
22
23
  ];
23
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
+
24
52
  export const findCodemod = (id) =>
25
53
  CODEMODS.find((c) => c.id.toLowerCase() === String(id).toLowerCase()) ?? null;
26
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;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * CM-01 — root imports → `/suite` and `/legacy` (class R; plan §29 seq 1,
3
+ * registry `C-ROOT-SUITE`, lands with W8-02).
4
+ *
5
+ * W8-02 gave the shared Assure patterns their own entry and put the frozen and
6
+ * product-owned modules behind `/legacy`. Both are re-export barrels over the
7
+ * very same module instances the root exports, so moving an import is a pure
8
+ * path change: no duplicated context, no behaviour difference, nothing to test
9
+ * afterwards. The root keeps the names through 1.x and drops them in 2.0.
10
+ *
11
+ * ## What it rewrites
12
+ *
13
+ * ```tsx
14
+ * import { Button, AgreementViewer, TimeLogger } from "@assure-one/design-system";
15
+ * ```
16
+ *
17
+ * becomes
18
+ *
19
+ * ```tsx
20
+ * import { Button } from "@assure-one/design-system";
21
+ * import { AgreementViewer } from "@assure-one/design-system/suite";
22
+ * import { TimeLogger } from "@assure-one/design-system/legacy";
23
+ * ```
24
+ *
25
+ * A declaration whose names all move keeps its shape and only its module
26
+ * specifier changes. `import type` and per-specifier `type` prefixes, aliases
27
+ * (`X as Y`), quote style and a default import are preserved; the default
28
+ * import always stays on the root.
29
+ *
30
+ * Which entry owns which name comes from `codemods/lib/entry-map.mjs`, which
31
+ * `pnpm entry-map` generates from `architecture/entry-map.json` — so the
32
+ * installed copy of the codemod always knows the map of its own version.
33
+ *
34
+ * ## What it reports
35
+ *
36
+ * | rule | action | severity | why |
37
+ * | ------------------ | ------- | -------- | --------------------------------------------------------------------------------------- |
38
+ * | `moved-to-suite` | applied | low | shared Assure patterns now import from `/suite` |
39
+ * | `moved-to-legacy` | applied | low | frozen modules with no known consumer now import from `/legacy` |
40
+ * | `product-owned` | applied | medium | the import moved to `/legacy`, but the module is leaving for a product repository |
41
+ * | `namespace-import` | review | high | `import * as DS` cannot be split; the named members have to be imported one by one first |
42
+ *
43
+ * Imports already on `/suite` or `/legacy` are not touched (idempotency), and
44
+ * neither is a name the root still owns. Test files are skipped.
45
+ */
46
+ import { ENTRY_BY_SYMBOL, ENTRY_SPECIFIER, PRODUCT_TARGET } from "../lib/entry-map.mjs";
47
+ import { applyEdits } from "../lib/jsx-edit.mjs";
48
+ import { analyseForms } from "../lib/forms.mjs";
49
+
50
+ export const meta = {
51
+ id: "CM-01",
52
+ title: "Root imports of suite, legacy and product-owned names → /suite and /legacy",
53
+ class: "R",
54
+ oneShot: false,
55
+ requires: { codemods: [], dsVersion: null },
56
+ parses: ["code"],
57
+ includeTests: false,
58
+ usesTypeScript: true,
59
+ usesPostcss: false,
60
+ registryIds: ["C-ROOT-SUITE"],
61
+ };
62
+
63
+ export const RULES = {
64
+ "moved-to-suite": { action: "applied", severity: "low" },
65
+ "moved-to-legacy": { action: "applied", severity: "low" },
66
+ "product-owned": { action: "applied", severity: "medium" },
67
+ "namespace-import": { action: "review", severity: "high" },
68
+ };
69
+
70
+ const ROOT = "@assure-one/design-system";
71
+
72
+ /** The entry that owns an imported name, or `null` when the root keeps it. */
73
+ export const entryOf = (name) => ENTRY_BY_SYMBOL[name] ?? null;
74
+
75
+ const ruleFor = (name, entry) =>
76
+ PRODUCT_TARGET[name] ? "product-owned" : entry === "suite" ? "moved-to-suite" : "moved-to-legacy";
77
+
78
+ /** One `import …` statement built from the specifiers of the original one. */
79
+ function declaration({ typeOnly, defaultName, specifiers, quote, semi }, specifier) {
80
+ const clause = [defaultName, specifiers.length ? `{ ${specifiers.join(", ")} }` : null].filter(
81
+ Boolean,
82
+ );
83
+ return `import ${typeOnly ? "type " : ""}${clause.join(", ")} from ${quote}${specifier}${quote}${semi}`;
84
+ }
85
+
86
+ export function transform(file, { ts }) {
87
+ const facts = analyseForms(ts, file.source, file.rel);
88
+ const sf = facts.sf;
89
+ const findings = [];
90
+ const edits = [];
91
+
92
+ for (const stmt of sf.statements) {
93
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
94
+ if (stmt.moduleSpecifier.text !== ROOT) continue;
95
+ const clause = stmt.importClause;
96
+ if (!clause) continue;
97
+
98
+ const line = sf.getLineAndCharacterOfPosition(stmt.getStart(sf)).line + 1;
99
+ const base = { line, registryId: "C-ROOT-SUITE", component: null, gate: null };
100
+
101
+ // `import * as DS` — the members are reached through the namespace, so the
102
+ // import cannot be split by name. Only worth reporting when the file
103
+ // actually reads a name that moved.
104
+ if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
105
+ const ns = clause.namedBindings.name.text;
106
+ const used = new Set();
107
+ const visit = (node) => {
108
+ if (
109
+ ts.isPropertyAccessExpression(node) &&
110
+ ts.isIdentifier(node.expression) &&
111
+ node.expression.text === ns &&
112
+ entryOf(node.name.text)
113
+ ) {
114
+ used.add(node.name.text);
115
+ }
116
+ if (ts.isQualifiedName(node) && ts.isIdentifier(node.left) && node.left.text === ns) {
117
+ if (entryOf(node.right.text)) used.add(node.right.text);
118
+ }
119
+ ts.forEachChild(node, visit);
120
+ };
121
+ ts.forEachChild(sf, visit);
122
+ if (used.size) {
123
+ findings.push({
124
+ ...base,
125
+ rule: "namespace-import",
126
+ ...RULES["namespace-import"],
127
+ match: `import * as ${ns}`,
128
+ detail: { namespace: ns, symbols: [...used].sort() },
129
+ });
130
+ }
131
+ continue;
132
+ }
133
+
134
+ const named = clause.namedBindings;
135
+ if (!named || !ts.isNamedImports(named)) continue;
136
+
137
+ const buckets = { root: [], suite: [], legacy: [] };
138
+ const movedNames = { suite: [], legacy: [] };
139
+ for (const element of named.elements) {
140
+ const imported = (element.propertyName ?? element.name).text;
141
+ const entry = entryOf(imported);
142
+ const bucket = entry ?? "root";
143
+ buckets[bucket].push(element.getText(sf));
144
+ if (entry) movedNames[entry].push(imported);
145
+ }
146
+ if (!movedNames.suite.length && !movedNames.legacy.length) continue;
147
+
148
+ const text = stmt.getText(sf);
149
+ const shape = {
150
+ typeOnly: clause.isTypeOnly,
151
+ defaultName: clause.name ? clause.name.text : null,
152
+ quote: stmt.moduleSpecifier.getText(sf)[0],
153
+ semi: text.endsWith(";") ? ";" : "",
154
+ };
155
+ const rows = [];
156
+ if (buckets.root.length || shape.defaultName) {
157
+ rows.push(declaration({ ...shape, specifiers: buckets.root }, ROOT));
158
+ }
159
+ for (const entry of ["suite", "legacy"]) {
160
+ if (!buckets[entry].length) continue;
161
+ rows.push(
162
+ declaration(
163
+ { ...shape, defaultName: null, specifiers: buckets[entry] },
164
+ ENTRY_SPECIFIER[entry],
165
+ ),
166
+ );
167
+ }
168
+ edits.push({ pos: stmt.getStart(sf), end: stmt.getEnd(), text: rows.join("\n") });
169
+
170
+ for (const entry of ["suite", "legacy"]) {
171
+ const byRule = new Map();
172
+ for (const name of movedNames[entry]) {
173
+ const rule = ruleFor(name, entry);
174
+ if (!byRule.has(rule)) byRule.set(rule, []);
175
+ byRule.get(rule).push(name);
176
+ }
177
+ for (const [rule, names] of byRule) {
178
+ findings.push({
179
+ ...base,
180
+ rule,
181
+ ...RULES[rule],
182
+ match: `{ ${names.sort().join(", ")} }`,
183
+ detail: {
184
+ from: ROOT,
185
+ to: ENTRY_SPECIFIER[entry],
186
+ symbols: names,
187
+ ...(rule === "product-owned"
188
+ ? { owners: [...new Set(names.map((n) => PRODUCT_TARGET[n]))].sort() }
189
+ : {}),
190
+ },
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ return {
197
+ output: edits.length ? applyEdits(file.source, edits) : file.source,
198
+ findings,
199
+ notTransformed: [],
200
+ parseErrors: facts.parseErrors,
201
+ };
202
+ }
@@ -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 };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The seed a host supplies per tenant.
3
+ *
4
+ * @experimental Gated by decision D7.
5
+ */
6
+ interface BrandThemeSeed {
7
+ /**
8
+ * The brand fill, as `#rgb` or `#rrggbb`. Drives the `action.brand` family,
9
+ * the focus ring and the brand gradient.
10
+ */
11
+ brand: string;
12
+ /**
13
+ * An optional second hue for the `action.accent` family, as `#rgb` or
14
+ * `#rrggbb`. `undefined` or `null` means "the same as `brand`", which is what
15
+ * the built-in `[data-ds-brand]` scopes do.
16
+ */
17
+ accent?: string | null | undefined;
18
+ }
19
+ /** Every custom property `createBrandTheme()` sets, in output order. */
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"];
21
+ /** One of the custom properties `createBrandTheme()` sets. */
22
+ type BrandThemeProperty = (typeof BRAND_THEME_PROPERTIES)[number];
23
+ /**
24
+ * The style object `createBrandTheme()` returns: every property of
25
+ * {@link BRAND_THEME_PROPERTIES} with a CSS value.
26
+ *
27
+ * React types `style` as `CSSProperties`, which has no index signature for
28
+ * custom properties, so spread it or cast it at the call site:
29
+ * `style={{ ...createBrandTheme(seed) }}`.
30
+ *
31
+ * @experimental Gated by decision D7.
32
+ */
33
+ type BrandTheme = {
34
+ readonly [P in BrandThemeProperty]: string;
35
+ };
36
+ /**
37
+ * Turn a tenant's colours into the design system's brand and accent tokens.
38
+ *
39
+ * ```tsx
40
+ * const theme = createBrandTheme({ brand: firm.primaryColor, accent: firm.accentColor });
41
+ * return <div style={{ ...theme }}>{children}</div>;
42
+ * ```
43
+ *
44
+ * Apply it to the element that scopes the brand — `<body>` for a whole-tenant
45
+ * theme, a wrapper for a preview. The properties are the same ones a built-in
46
+ * `[data-ds-brand]` scope declares, so anything reading them (every design
47
+ * system component, and any application CSS reading `--ds-*`) follows.
48
+ *
49
+ * @param seed - `brand`, and optionally `accent` (decision D7).
50
+ * @returns A frozen object of custom property names to CSS values.
51
+ * @throws TypeError when the seed is not an object, carries an unknown key, or
52
+ * a colour is missing or not `#rgb`/`#rrggbb`.
53
+ *
54
+ * @experimental Gated by decision D7. The property set may grow, and the seed
55
+ * shape may change, until D7 is resolved.
56
+ */
57
+ declare function createBrandTheme(seed: BrandThemeSeed): BrandTheme;
58
+
59
+ export { BRAND_THEME_PROPERTIES as B, type BrandTheme as a, type BrandThemeProperty as b, type BrandThemeSeed as c, createBrandTheme as d };