@assure-one/design-system 1.30.0 → 1.32.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 +28 -1
- package/codemods/0.2.0-radix-migration.mjs +315 -0
- package/codemods/README.md +305 -0
- package/codemods/lib/css-selectors.mjs +33 -0
- package/codemods/lib/css-values.mjs +223 -0
- package/codemods/lib/ds-stylesheet.mjs +168 -0
- package/codemods/lib/environment.mjs +72 -0
- package/codemods/lib/files.mjs +100 -0
- package/codemods/lib/jsx.mjs +0 -0
- package/codemods/lib/ledger.mjs +84 -0
- package/codemods/lib/registry.mjs +30 -0
- package/codemods/lib/report.mjs +119 -0
- package/codemods/lib/runner.mjs +164 -0
- package/codemods/run.mjs +161 -0
- package/codemods/transforms/cm-15-dom-selectors.mjs +573 -0
- package/codemods/transforms/cm-16-globals-css.mjs +487 -0
- package/dist/css/base.css +60 -0
- package/dist/css/legacy-aliases.css +489 -0
- package/dist/css/shadcn.css +155 -0
- package/dist/css/tailwind.css +233 -0
- package/dist/css/tokens.css +439 -0
- package/dist/index.d.ts +327 -33
- package/dist/index.js +1843 -706
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/testing/index.cjs +458 -0
- package/dist/testing/index.d.cts +253 -0
- package/dist/testing/index.d.ts +253 -0
- package/dist/testing/index.js +452 -0
- package/dist/testing/setup.cjs +123 -0
- package/dist/testing/setup.js +121 -0
- package/dist/testing/style-stub.cjs +7 -0
- package/dist/testing/style-stub.js +5 -0
- package/dist/tokens/index.d.ts +114 -96
- package/dist/tokens/index.js +67 -48
- package/dist/tokens/index.js.map +1 -1
- package/package.json +84 -14
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CM-16 — `globals.css` analyser (class X, report-only; plan §29, feeds §12
|
|
3
|
+
* and the 2.0 gates G1/G2).
|
|
4
|
+
*
|
|
5
|
+
* Every consumer renders design-system components on top of its own
|
|
6
|
+
* independently compiled Tailwind stylesheet, and [CU §12.1] shows that the
|
|
7
|
+
* application — not the design system — decides what a DS component looks
|
|
8
|
+
* like. CM-16 finds the five mechanisms that make that true, in the app's own
|
|
9
|
+
* CSS, with a line number for each:
|
|
10
|
+
*
|
|
11
|
+
* | rule | contract | gate | what it is |
|
|
12
|
+
* | -------------------------- | --------------- | ---- | ----------------------------------------------------------- |
|
|
13
|
+
* | `source-into-ds` | C-CSS-SOURCE | G2 | `@source` pointing into the design-system package |
|
|
14
|
+
* | `ds-styles-import` | C-CSS-STYLES | G1 | the all-in-one `styles.css` is loaded |
|
|
15
|
+
* | `duplicate-preflight` | C-CSS-STYLES | G1 | the app compiles a second Tailwind preflight |
|
|
16
|
+
* | `theme-collision-differs` | C-TOKENS-LEGACY | — | `@theme` key that shadows a DS token with another value |
|
|
17
|
+
* | `theme-collision-equal` | C-TOKENS-LEGACY | — | the same name, the same value today |
|
|
18
|
+
* | `legacy-var-write` | C-TOKENS-WRITE | — | a DS token name declared outside `@theme` ([CU §12.3]) |
|
|
19
|
+
* | `legacy-var-read` | C-TOKENS-LEGACY | — | `var(--<ds token>)` read by app CSS |
|
|
20
|
+
* | `brand-scope-spelling` | C-CSS-DARKCLASS | — | DS tokens re-declared under `.dark` / `[data-brand="…"]` |
|
|
21
|
+
* | `dark-variant-mismatch` | C-CSS-DARKCLASS | — | app `dark` variant that cannot match the `.dark` element |
|
|
22
|
+
* | `unlayered-ds-dom` | (unregistered) | — | unlayered app rule that styles DS markup |
|
|
23
|
+
* | `layered-ds-dom` | (unregistered) | — | the same rule inside the app's own `base`/`components` layer |
|
|
24
|
+
*
|
|
25
|
+
* Boundary with CM-15: selectors that name design-system **internals**
|
|
26
|
+
* (`[data-radix-select-viewport]`, `[data-esign-cover]`, `[data-labels]`,
|
|
27
|
+
* `[cmdk-…]`, the toast region) are DOM coupling and belong to the
|
|
28
|
+
* `C-DOM-*` contracts — CM-15 reports them, including from CSS, and CM-16
|
|
29
|
+
* skips them so the two finders never count the same rule twice. What CM-16
|
|
30
|
+
* owns is the *layering*: element, universal and DS-utility-class selectors
|
|
31
|
+
* that reach DS markup by being unlayered or later in the cascade.
|
|
32
|
+
*
|
|
33
|
+
* Model, and where it is weaker than `derive-consumer-preset` (W1-18):
|
|
34
|
+
* - CM-16 reads the **authored** stylesheets of the project. W1-18 reads
|
|
35
|
+
* the **compiled** output of the app's own Tailwind, so it also sees the
|
|
36
|
+
* theme keys the app inherits from `@import "tailwindcss"`. Collisions
|
|
37
|
+
* that neither sheet authors (`--spacing`, `--text-*`, `--container-*`,
|
|
38
|
+
* `--font-weight-*`) are therefore invisible to CM-16 by construction.
|
|
39
|
+
* - CM-16 resolves values with source order inside one file, on the
|
|
40
|
+
* default root scope only; W1-18 runs a layer-, specificity- and
|
|
41
|
+
* element-aware cascade over both sheets and can be given the app's
|
|
42
|
+
* `<html>` attributes (`data-brand="tax"`).
|
|
43
|
+
* Both call two values "different" through the same normalisation
|
|
44
|
+
* (`codemods/lib/css-values.mjs`), so the differing counts are comparable.
|
|
45
|
+
*
|
|
46
|
+
* Never modifies a file.
|
|
47
|
+
*/
|
|
48
|
+
import {
|
|
49
|
+
classNamesOf,
|
|
50
|
+
dsStylesheet,
|
|
51
|
+
insideAtRule,
|
|
52
|
+
insideKeyframes,
|
|
53
|
+
isRootSelector,
|
|
54
|
+
layerOf,
|
|
55
|
+
selectorParts,
|
|
56
|
+
} from "../lib/ds-stylesheet.mjs";
|
|
57
|
+
import { computeCustomProperties, referencedVars, sameValue } from "../lib/css-values.mjs";
|
|
58
|
+
|
|
59
|
+
export const meta = {
|
|
60
|
+
id: "CM-16",
|
|
61
|
+
title: "globals.css analyser: @source, preflight, colliding @theme keys, unlayered globals",
|
|
62
|
+
class: "X",
|
|
63
|
+
oneShot: false,
|
|
64
|
+
requires: { codemods: [], dsVersion: null },
|
|
65
|
+
parses: ["code", "css"],
|
|
66
|
+
includeTests: false,
|
|
67
|
+
usesTypeScript: false,
|
|
68
|
+
usesPostcss: true,
|
|
69
|
+
registryIds: [
|
|
70
|
+
"C-CSS-SOURCE",
|
|
71
|
+
"C-CSS-STYLES",
|
|
72
|
+
"C-CSS-DARKCLASS",
|
|
73
|
+
"C-TOKENS-LEGACY",
|
|
74
|
+
"C-TOKENS-WRITE",
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const DS_PACKAGE = "@assure-one/design-system";
|
|
79
|
+
const DS_STYLES = `${DS_PACKAGE}/styles.css`;
|
|
80
|
+
|
|
81
|
+
/** How a finding is weighted in the report and in the preset work. */
|
|
82
|
+
export const SEVERITY = {
|
|
83
|
+
high: "high",
|
|
84
|
+
medium: "medium",
|
|
85
|
+
low: "low",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// At-rules
|
|
90
|
+
|
|
91
|
+
const unquote = (s) =>
|
|
92
|
+
s
|
|
93
|
+
.trim()
|
|
94
|
+
.replace(/^url\(\s*/, "")
|
|
95
|
+
.replace(/\s*\)$/, "")
|
|
96
|
+
.replace(/^["']|["']$/g, "");
|
|
97
|
+
|
|
98
|
+
/** `@import` target without its layer/media/supports tail. */
|
|
99
|
+
export const importTarget = (params) => unquote(params.split(/\s+(?=layer|supports|screen|\()/)[0]);
|
|
100
|
+
|
|
101
|
+
/** Whether an `@source` names the design-system package (never `@source not`). */
|
|
102
|
+
export const sourcesDs = (params) => {
|
|
103
|
+
const value = params.trim();
|
|
104
|
+
if (/^not\b/.test(value)) return false;
|
|
105
|
+
if (/^inline\b/.test(value)) return false;
|
|
106
|
+
return unquote(value).includes("design-system");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const APP_PREFLIGHT = new Set([
|
|
110
|
+
"tailwindcss",
|
|
111
|
+
"tailwindcss/preflight",
|
|
112
|
+
"tailwindcss/preflight.css",
|
|
113
|
+
"tailwindcss/index.css",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Selectors that reach design-system markup
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Elements the design system renders itself. A bare type selector on any of
|
|
121
|
+
* them lands inside DS components, whatever the app meant it for.
|
|
122
|
+
*/
|
|
123
|
+
const DS_ELEMENTS = new Set([
|
|
124
|
+
"a",
|
|
125
|
+
"aside",
|
|
126
|
+
"body",
|
|
127
|
+
"button",
|
|
128
|
+
"caption",
|
|
129
|
+
"code",
|
|
130
|
+
"dd",
|
|
131
|
+
"dialog",
|
|
132
|
+
"div",
|
|
133
|
+
"dl",
|
|
134
|
+
"dt",
|
|
135
|
+
"fieldset",
|
|
136
|
+
"footer",
|
|
137
|
+
"form",
|
|
138
|
+
"h1",
|
|
139
|
+
"h2",
|
|
140
|
+
"h3",
|
|
141
|
+
"h4",
|
|
142
|
+
"h5",
|
|
143
|
+
"h6",
|
|
144
|
+
"header",
|
|
145
|
+
"hr",
|
|
146
|
+
"html",
|
|
147
|
+
"img",
|
|
148
|
+
"input",
|
|
149
|
+
"label",
|
|
150
|
+
"legend",
|
|
151
|
+
"li",
|
|
152
|
+
"main",
|
|
153
|
+
"nav",
|
|
154
|
+
"ol",
|
|
155
|
+
"optgroup",
|
|
156
|
+
"option",
|
|
157
|
+
"p",
|
|
158
|
+
"pre",
|
|
159
|
+
"section",
|
|
160
|
+
"select",
|
|
161
|
+
"span",
|
|
162
|
+
"strong",
|
|
163
|
+
"svg",
|
|
164
|
+
"table",
|
|
165
|
+
"tbody",
|
|
166
|
+
"td",
|
|
167
|
+
"textarea",
|
|
168
|
+
"tfoot",
|
|
169
|
+
"th",
|
|
170
|
+
"thead",
|
|
171
|
+
"tr",
|
|
172
|
+
"ul",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Attributes and bracket hooks that name design-system internals. CM-15 owns
|
|
177
|
+
* these (the `C-DOM-*` contracts), so CM-16 stays silent on them.
|
|
178
|
+
*/
|
|
179
|
+
const DS_INTERNALS =
|
|
180
|
+
/\[data-radix-|\[data-esign-|\[data-slot|\[data-labels|\[data-collapsible|\[data-with-toolbar|\[data-sticky-stack|\[cmdk-|aria-label=["']?Notifications/;
|
|
181
|
+
|
|
182
|
+
const SCOPE_SPELLING = /(^|[\s>+~])(?:\.dark\b|\[data-brand)/;
|
|
183
|
+
|
|
184
|
+
/** The rightmost compound of one complex selector. */
|
|
185
|
+
export function rightmostCompound(part) {
|
|
186
|
+
const segments = part.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
|
|
187
|
+
return segments.length ? segments[segments.length - 1] : part;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stripFunctional = (compound) =>
|
|
191
|
+
compound
|
|
192
|
+
.replace(/::?[a-z-]+\([^)]*\)/gi, "")
|
|
193
|
+
.replace(/::?[a-z-]+/gi, "")
|
|
194
|
+
.replace(/\[[^\]]*\]/g, "");
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* How a selector list reaches DS markup, or `null`.
|
|
198
|
+
* @returns {{ via: "universal" | "element" | "ds-class", detail: string } | null}
|
|
199
|
+
*/
|
|
200
|
+
export function reachesDsDom(selector, dsClasses) {
|
|
201
|
+
if (DS_INTERNALS.test(selector)) return null; // CM-15's territory
|
|
202
|
+
let best = null;
|
|
203
|
+
const rank = { universal: 3, element: 2, "ds-class": 1 };
|
|
204
|
+
for (const part of selectorParts(selector)) {
|
|
205
|
+
const compound = rightmostCompound(part);
|
|
206
|
+
const bare = stripFunctional(compound);
|
|
207
|
+
let hit = null;
|
|
208
|
+
if (/\*/.test(bare) || bare === "") {
|
|
209
|
+
// `*`, `*:focus-visible`, or a bare pseudo-element such as
|
|
210
|
+
// `::-webkit-scrollbar`: both apply inside DS components.
|
|
211
|
+
const pseudo = /::?[a-z-]+/i.exec(compound);
|
|
212
|
+
hit = { via: "universal", detail: /\*/.test(bare) ? "*" : (pseudo?.[0] ?? compound) };
|
|
213
|
+
} else if (/^[a-z][a-z0-9]*$/i.test(bare) && DS_ELEMENTS.has(bare.toLowerCase())) {
|
|
214
|
+
hit = { via: "element", detail: bare.toLowerCase() };
|
|
215
|
+
} else {
|
|
216
|
+
const shipped = classNamesOf(compound).filter((c) => dsClasses.has(c));
|
|
217
|
+
if (shipped.length) hit = { via: "ds-class", detail: shipped.sort().join(", ") };
|
|
218
|
+
}
|
|
219
|
+
if (hit && (!best || rank[hit.via] > rank[best.via])) best = hit;
|
|
220
|
+
}
|
|
221
|
+
return best;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// CSS facts for one file
|
|
226
|
+
|
|
227
|
+
function cssFacts(postcss, source, rel) {
|
|
228
|
+
let root;
|
|
229
|
+
try {
|
|
230
|
+
root = postcss.parse(source, { from: rel });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return { error: String(error.reason ?? error.message), line: error.line ?? null };
|
|
233
|
+
}
|
|
234
|
+
const atRules = [];
|
|
235
|
+
const decls = [];
|
|
236
|
+
const rules = [];
|
|
237
|
+
const reads = [];
|
|
238
|
+
root.walkAtRules((at) => {
|
|
239
|
+
atRules.push({ name: at.name, params: at.params.trim(), line: at.source?.start?.line ?? null });
|
|
240
|
+
});
|
|
241
|
+
root.walkDecls((decl) => {
|
|
242
|
+
const line = decl.source?.start?.line ?? null;
|
|
243
|
+
for (const name of referencedVars(decl.value)) reads.push({ name, line });
|
|
244
|
+
if (!decl.prop.startsWith("--") || decl.prop.startsWith("--tw-")) return;
|
|
245
|
+
if (insideKeyframes(decl)) return;
|
|
246
|
+
const parent = decl.parent;
|
|
247
|
+
const theme =
|
|
248
|
+
insideAtRule(decl, "theme") || (parent?.type === "atrule" && parent.name === "theme");
|
|
249
|
+
const selector = parent?.type === "rule" ? parent.selector.replace(/\s+/g, " ") : null;
|
|
250
|
+
decls.push({
|
|
251
|
+
name: decl.prop,
|
|
252
|
+
value: decl.value.trim(),
|
|
253
|
+
line,
|
|
254
|
+
theme,
|
|
255
|
+
selector,
|
|
256
|
+
root: theme || (selector !== null && isRootSelector(selector)),
|
|
257
|
+
layer: layerOf(decl),
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
root.walkRules((rule) => {
|
|
261
|
+
if (insideKeyframes(rule)) return;
|
|
262
|
+
rules.push({
|
|
263
|
+
selector: rule.selector.replace(/\s+/g, " "),
|
|
264
|
+
line: rule.source?.start?.line ?? null,
|
|
265
|
+
layer: layerOf(rule),
|
|
266
|
+
// `@apply` is how the apps write most of their element defaults
|
|
267
|
+
// (`* { @apply border-border }`), so it counts as a declaration.
|
|
268
|
+
declarations:
|
|
269
|
+
rule.nodes?.filter((n) => n.type === "decl" || (n.type === "atrule" && n.name === "apply"))
|
|
270
|
+
.length ?? 0,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
return { atRules, decls, rules, reads };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
export function transform(file, { postcss }) {
|
|
279
|
+
return file.kind === "css" ? cssFindings(file, postcss) : codeFindings(file, postcss);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The all-in-one stylesheet is imported from application code, not from CSS
|
|
284
|
+
* (all four consumers do it in `src/app/layout.tsx`). No parser is needed for
|
|
285
|
+
* a module specifier.
|
|
286
|
+
*/
|
|
287
|
+
function codeFindings(file) {
|
|
288
|
+
const findings = [];
|
|
289
|
+
const lines = file.source.split("\n");
|
|
290
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
291
|
+
if (!lines[i].includes(DS_STYLES)) continue;
|
|
292
|
+
if (!/^\s*(?:import\s|require\()/.test(lines[i])) continue;
|
|
293
|
+
findings.push({
|
|
294
|
+
line: i + 1,
|
|
295
|
+
registryId: "C-CSS-STYLES",
|
|
296
|
+
gate: "G1",
|
|
297
|
+
severity: SEVERITY.high,
|
|
298
|
+
rule: "ds-styles-import",
|
|
299
|
+
scope: "import",
|
|
300
|
+
component: null,
|
|
301
|
+
match: DS_STYLES,
|
|
302
|
+
confidence: "high",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return { findings };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function cssFindings(file, postcss) {
|
|
309
|
+
const ds = dsStylesheet(postcss);
|
|
310
|
+
const facts = cssFacts(postcss, file.source, file.rel);
|
|
311
|
+
if (facts.error) {
|
|
312
|
+
return { parseErrors: [{ line: facts.line, message: facts.error }] };
|
|
313
|
+
}
|
|
314
|
+
const findings = [];
|
|
315
|
+
const add = (f) => findings.push({ component: null, confidence: "high", gate: null, ...f });
|
|
316
|
+
|
|
317
|
+
// 1. Loading: `@source` into the package, and a second preflight.
|
|
318
|
+
let appPreflight = null;
|
|
319
|
+
for (const at of facts.atRules) {
|
|
320
|
+
if (at.name === "source" && sourcesDs(at.params)) {
|
|
321
|
+
add({
|
|
322
|
+
line: at.line,
|
|
323
|
+
registryId: "C-CSS-SOURCE",
|
|
324
|
+
gate: "G2",
|
|
325
|
+
severity: SEVERITY.high,
|
|
326
|
+
rule: "source-into-ds",
|
|
327
|
+
scope: "at-rule",
|
|
328
|
+
match: `@source ${at.params}`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (at.name === "import") {
|
|
332
|
+
const target = importTarget(at.params);
|
|
333
|
+
if (target === DS_STYLES) {
|
|
334
|
+
add({
|
|
335
|
+
line: at.line,
|
|
336
|
+
registryId: "C-CSS-STYLES",
|
|
337
|
+
gate: "G1",
|
|
338
|
+
severity: SEVERITY.high,
|
|
339
|
+
rule: "ds-styles-import",
|
|
340
|
+
scope: "at-rule",
|
|
341
|
+
match: `@import "${target}"`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (APP_PREFLIGHT.has(target) && appPreflight === null) appPreflight = { at, target };
|
|
345
|
+
}
|
|
346
|
+
if (at.name === "custom-variant" && /^dark\b/.test(at.params)) {
|
|
347
|
+
// `&:is(.dark *)` styles descendants of `.dark` but not `.dark` itself;
|
|
348
|
+
// the design system's own variant is `&:where(.dark, .dark *)`.
|
|
349
|
+
const body = at.params.slice(4).trim();
|
|
350
|
+
if (/\.dark\s+\*/.test(body) && !/\.dark\s*[,)]/.test(body)) {
|
|
351
|
+
add({
|
|
352
|
+
line: at.line,
|
|
353
|
+
registryId: "C-CSS-DARKCLASS",
|
|
354
|
+
severity: SEVERITY.low,
|
|
355
|
+
rule: "dark-variant-mismatch",
|
|
356
|
+
scope: "at-rule",
|
|
357
|
+
match: `@custom-variant ${at.params}`,
|
|
358
|
+
confidence: "medium",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (appPreflight && ds.preflight) {
|
|
364
|
+
add({
|
|
365
|
+
line: appPreflight.at.line,
|
|
366
|
+
registryId: "C-CSS-STYLES",
|
|
367
|
+
gate: "G1",
|
|
368
|
+
severity: SEVERITY.high,
|
|
369
|
+
rule: "duplicate-preflight",
|
|
370
|
+
scope: "at-rule",
|
|
371
|
+
match: `@import "${appPreflight.target}" (the design system's ${ds.from} ships preflight too)`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 2. Token collisions. Values are resolved inside this file, with the
|
|
376
|
+
// design system's own values standing in for everything it declares.
|
|
377
|
+
const rootDecls = facts.decls.filter((d) => d.root);
|
|
378
|
+
const winners = new Map();
|
|
379
|
+
for (const d of rootDecls) winners.set(d.name, { value: d.value });
|
|
380
|
+
const appValues = computeCustomProperties(winners, ds.tokens);
|
|
381
|
+
const declaredHere = new Set(facts.decls.map((d) => d.name));
|
|
382
|
+
|
|
383
|
+
const lastRoot = new Map();
|
|
384
|
+
for (const d of rootDecls) lastRoot.set(d.name, d);
|
|
385
|
+
for (const [name, d] of [...lastRoot].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
386
|
+
if (!ds.tokens.has(name) && !ds.declared.has(name)) continue;
|
|
387
|
+
const dsValue = ds.tokens.get(name);
|
|
388
|
+
const appValue = appValues.get(name);
|
|
389
|
+
const differs = !sameValue(dsValue, appValue);
|
|
390
|
+
if (d.theme) {
|
|
391
|
+
add({
|
|
392
|
+
line: d.line,
|
|
393
|
+
registryId: "C-TOKENS-LEGACY",
|
|
394
|
+
severity: differs ? SEVERITY.high : SEVERITY.low,
|
|
395
|
+
rule: differs ? "theme-collision-differs" : "theme-collision-equal",
|
|
396
|
+
scope: "theme",
|
|
397
|
+
match: `${name}: ${d.value}`,
|
|
398
|
+
detail: { ds: dsValue ?? null, app: appValue ?? null },
|
|
399
|
+
confidence: differs ? "high" : "medium",
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
add({
|
|
403
|
+
line: d.line,
|
|
404
|
+
registryId: "C-TOKENS-WRITE",
|
|
405
|
+
severity: differs ? SEVERITY.high : SEVERITY.medium,
|
|
406
|
+
rule: "legacy-var-write",
|
|
407
|
+
scope: d.selector ?? "root",
|
|
408
|
+
match: `${name}: ${d.value}`,
|
|
409
|
+
detail: { ds: dsValue ?? null, app: appValue ?? null },
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 3. DS tokens re-declared inside a scope the design system owns the
|
|
415
|
+
// spelling of (`.dark`, `[data-brand="…"]`).
|
|
416
|
+
for (const d of facts.decls) {
|
|
417
|
+
if (d.root || !d.selector || !SCOPE_SPELLING.test(d.selector)) continue;
|
|
418
|
+
if (!ds.tokens.has(d.name) && !ds.declared.has(d.name)) continue;
|
|
419
|
+
add({
|
|
420
|
+
line: d.line,
|
|
421
|
+
registryId: "C-CSS-DARKCLASS",
|
|
422
|
+
severity: SEVERITY.medium,
|
|
423
|
+
rule: "brand-scope-spelling",
|
|
424
|
+
scope: d.selector,
|
|
425
|
+
match: `${d.name}: ${d.value}`,
|
|
426
|
+
confidence: "medium",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 4. Legacy reads: `var(--<ds token>)` for a token this file does not
|
|
431
|
+
// declare itself, so the value comes from the design system.
|
|
432
|
+
const readLines = new Map();
|
|
433
|
+
for (const r of facts.reads) {
|
|
434
|
+
if (declaredHere.has(r.name)) continue;
|
|
435
|
+
if (!ds.tokens.has(r.name) && !ds.declared.has(r.name)) continue;
|
|
436
|
+
const key = `${r.line} ${r.name}`;
|
|
437
|
+
if (readLines.has(key)) continue;
|
|
438
|
+
readLines.set(key, r);
|
|
439
|
+
}
|
|
440
|
+
for (const r of readLines.values()) {
|
|
441
|
+
add({
|
|
442
|
+
line: r.line,
|
|
443
|
+
registryId: "C-TOKENS-LEGACY",
|
|
444
|
+
severity: SEVERITY.medium,
|
|
445
|
+
rule: "legacy-var-read",
|
|
446
|
+
scope: "var",
|
|
447
|
+
match: `var(${r.name})`,
|
|
448
|
+
confidence: "medium",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 5. App rules that style design-system markup. Unlayered rules beat every
|
|
453
|
+
// layer; rules in the app's own `base`/`components` layers land in the
|
|
454
|
+
// same layer as the design system's and win on source order alone.
|
|
455
|
+
for (const rule of facts.rules) {
|
|
456
|
+
if (!rule.declarations) continue;
|
|
457
|
+
if (isRootSelector(rule.selector) || SCOPE_SPELLING.test(rule.selector)) continue;
|
|
458
|
+
const reach = reachesDsDom(rule.selector, ds.classes);
|
|
459
|
+
if (!reach) continue;
|
|
460
|
+
const unlayered = rule.layer === null;
|
|
461
|
+
add({
|
|
462
|
+
line: rule.line,
|
|
463
|
+
registryId: null,
|
|
464
|
+
severity: unlayered
|
|
465
|
+
? reach.via === "ds-class"
|
|
466
|
+
? SEVERITY.high
|
|
467
|
+
: SEVERITY.medium
|
|
468
|
+
: SEVERITY.low,
|
|
469
|
+
rule: unlayered ? "unlayered-ds-dom" : "layered-ds-dom",
|
|
470
|
+
scope: rule.layer ?? "unlayered",
|
|
471
|
+
component: null,
|
|
472
|
+
match: rule.selector.slice(0, 200),
|
|
473
|
+
detail: { via: reach.via, names: reach.detail },
|
|
474
|
+
confidence: reach.via === "ds-class" ? "high" : "medium",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
findings,
|
|
480
|
+
context: {
|
|
481
|
+
dsSheet: ds.from,
|
|
482
|
+
dsTokens: ds.tokens.size,
|
|
483
|
+
appRootDeclarations: rootDecls.length,
|
|
484
|
+
appPreflight: Boolean(appPreflight),
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Optional document defaults (W1-15).
|
|
2
|
+
*
|
|
3
|
+
* Hand-written, not generated: it carries no token values, only references.
|
|
4
|
+
*
|
|
5
|
+
* `styles.css` ships a Tailwind preflight, `h1`–`h4` element styles, a global
|
|
6
|
+
* number-spinner rule, a global `::selection` rule and a body font size into
|
|
7
|
+
* every host. Target architecture §13 gives all of that back to the
|
|
8
|
+
* application, and this file is the small, opt-in replacement for the part an
|
|
9
|
+
* application usually does want: the document's background, ink, font and
|
|
10
|
+
* colour scheme, expressed as references to `--ds-*` tokens.
|
|
11
|
+
*
|
|
12
|
+
* What this file deliberately does NOT contain:
|
|
13
|
+
* - no preflight and no reset. DS components are written to be correct given
|
|
14
|
+
* a modern reset, which every known consumer already has (an app that has
|
|
15
|
+
* none needs its own; §13).
|
|
16
|
+
* - no element rules beyond `html` and `body`. No `h1`–`h4` (that is what
|
|
17
|
+
* `Heading` is for), no `input[type=number]` spinner rule (scoped to
|
|
18
|
+
* `NumberInput` instead), no global `*` box-sizing, no global
|
|
19
|
+
* reduced-motion override (durations collapse at the token level).
|
|
20
|
+
* - no `@import url(…)`: the design system loads no fonts. The application
|
|
21
|
+
* owns font loading and sets `--ds-font-sans` (§14, D2). Until it does,
|
|
22
|
+
* `--ds-font-sans` resolves to whatever `css/tokens.css` declares.
|
|
23
|
+
* - no `font-size`. `styles.css` sets the body to 14px; this file does not
|
|
24
|
+
* restore that, because a document base size is the application's
|
|
25
|
+
* decision. Moving an app to the new CSS mode is a deliberate visual
|
|
26
|
+
* change for exactly this kind of reason (ADR-004 rule 10, plan §12.4).
|
|
27
|
+
*
|
|
28
|
+
* Importing the file is the opt-in. Everything in it sits in `@layer base`, so
|
|
29
|
+
* an application's own base rules — declared later in the same layer — win,
|
|
30
|
+
* and any utility in a higher layer wins over all of it.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
@layer theme, base, ds, components, utilities;
|
|
34
|
+
|
|
35
|
+
@layer base {
|
|
36
|
+
/* Tells the browser which form controls, scrollbars and default canvas to
|
|
37
|
+
* render. Both spellings of the scope, as everywhere in the new mode. */
|
|
38
|
+
html {
|
|
39
|
+
color-scheme: light;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
html:where([data-ds-scheme="dark"], .dark) {
|
|
43
|
+
color-scheme: dark;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
body {
|
|
47
|
+
background-color: var(--ds-color-canvas);
|
|
48
|
+
color: var(--ds-color-content);
|
|
49
|
+
font-family: var(--ds-font-sans);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Selection ink. Scoped to the document body rather than written as a bare
|
|
53
|
+
* `::selection`, so the rule cannot reach a shadow host or a detached
|
|
54
|
+
* subtree the application renders outside `<body>`. */
|
|
55
|
+
body::selection,
|
|
56
|
+
body *::selection {
|
|
57
|
+
background-color: var(--ds-color-action-brand-subtle-bg);
|
|
58
|
+
color: var(--ds-color-content);
|
|
59
|
+
}
|
|
60
|
+
}
|