@godxjp/ui 28.13.0 → 30.0.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/agent/START-HERE.md +29 -10
- package/agent/components/Anchor.json +6 -1
- package/agent/components/AppShell.json +1 -1
- package/agent/components/AreaChart.json +19 -1
- package/agent/components/BarChart.json +11 -1
- package/agent/components/Cascader.json +1 -1
- package/agent/components/CompactBarTrend.json +1 -1
- package/agent/components/DataState.json +1 -1
- package/agent/components/DataTable.json +4 -4
- package/agent/components/FormField.json +1 -1
- package/agent/components/InfiniteQueryState.json +1 -1
- package/agent/components/Input.json +1 -1
- package/agent/components/LineChart.json +20 -2
- package/agent/components/ListRow.json +1 -1
- package/agent/components/Masonry.json +1 -1
- package/agent/components/MasterDetail.json +1 -1
- package/agent/components/PasswordStrength.json +1 -1
- package/agent/components/PermissionMatrix.json +1 -1
- package/agent/components/Select.json +1 -0
- package/agent/components/Sidebar.json +1 -1
- package/agent/components/Table.json +8 -3
- package/agent/components/ThemeScope.json +49 -0
- package/agent/components/Topbar.json +1 -0
- package/agent/components/TopbarItem.json +2 -1
- package/agent/components/Transfer.json +1 -1
- package/agent/components/TreeSelect.json +1 -1
- package/agent/components/UploadCropDialog.json +1 -1
- package/agent/components/formatDate.json +1 -1
- package/agent/components-index.json +5 -0
- package/agent/components.json +138 -30
- package/agent/index.json +19 -9
- package/agent/llms.txt +10 -10
- package/agent/patterns/tenant-brand-color.json +28 -0
- package/agent/patterns-index.json +27 -0
- package/agent/patterns.json +28 -0
- package/agent/rules.json +15 -0
- package/agent/tokens.json +5055 -1060
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.js +3 -0
- package/dist/app/tenant-theme.d.ts +80 -0
- package/dist/app/tenant-theme.js +154 -0
- package/dist/app/theme-axes.d.ts +14 -1
- package/dist/app/theme-axes.js +24 -31
- package/dist/components/charts/chart-cartesian.d.ts +5 -1
- package/dist/components/charts/chart-cartesian.js +15 -8
- package/dist/components/data-display/badge.d.ts +1 -1
- package/dist/components/data-display/badge.js +21 -3
- package/dist/components/data-display/carousel.js +4 -4
- package/dist/components/data-display/data-table.js +13 -2
- package/dist/components/data-display/permission-matrix.js +1 -1
- package/dist/components/data-display/table.d.ts +11 -2
- package/dist/components/data-display/table.js +19 -3
- package/dist/components/data-entry/control-appearance.d.ts +12 -6
- package/dist/components/data-entry/control-appearance.js +1 -1
- package/dist/components/data-entry/select.js +4 -3
- package/dist/components/feedback/dialog.js +6 -3
- package/dist/components/feedback/overlay-header-tone.d.ts +7 -0
- package/dist/components/feedback/overlay-header-tone.js +4 -4
- package/dist/components/feedback/sheet.d.ts +1 -1
- package/dist/components/feedback/sheet.js +6 -9
- package/dist/components/feedback/sonner.js +16 -3
- package/dist/components/general/button.js +22 -5
- package/dist/components/layout/affix.js +15 -1
- package/dist/components/layout/sidebar.js +7 -1
- package/dist/components/navigation/anchor.d.ts +1 -1
- package/dist/components/navigation/anchor.js +5 -4
- package/dist/components/navigation/app-setting-picker.js +1 -1
- package/dist/components/navigation/pagination.js +1 -1
- package/dist/components/navigation/tabs.js +15 -2
- package/dist/components/query/infinite-query-state.d.ts +22 -6
- package/dist/contracts/measurement.json +1 -1
- package/dist/lib/control-styles.d.ts +31 -11
- package/dist/lib/control-styles.js +6 -6
- package/dist/lib/overlay-portal.d.ts +20 -0
- package/dist/lib/overlay-portal.js +93 -0
- package/dist/props/components/app.prop.d.ts +12 -0
- package/dist/props/components/charts.prop.d.ts +30 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +21 -2
- package/dist/props/components/query.prop.d.ts +36 -2
- package/dist/props/registry.d.ts +46 -1
- package/dist/props/registry.js +38 -3
- package/dist/styles/alert-layout.css +39 -19
- package/dist/styles/badge-layout.css +11 -7
- package/dist/styles/base.css +14 -5
- package/dist/styles/card-layout.css +29 -16
- package/dist/styles/chart-layout.css +27 -5
- package/dist/styles/control.css +225 -103
- package/dist/styles/data-display-layout.css +178 -76
- package/dist/styles/data-entry-layout.css +35 -99
- package/dist/styles/dialog-layout.css +58 -28
- package/dist/styles/float-button-layout.css +5 -5
- package/dist/styles/focus-ring.css +11 -7
- package/dist/styles/layout.css +53 -25
- package/dist/styles/logo-layout.css +3 -3
- package/dist/styles/motion.css +2 -2
- package/dist/styles/navigation-layout.css +117 -44
- package/dist/styles/shell-layout.css +90 -124
- package/dist/styles/table-layout.css +66 -24
- package/dist/styles/text-layout.css +13 -4
- package/dist/styles/toggle.css +9 -3
- package/dist/tokens/components/actions.css +1 -1
- package/dist/tokens/components/attachments.css +4 -4
- package/dist/tokens/components/badge.css +4 -4
- package/dist/tokens/components/banner.css +1 -1
- package/dist/tokens/components/callout.css +1 -1
- package/dist/tokens/components/card.css +13 -8
- package/dist/tokens/components/chart.css +11 -2
- package/dist/tokens/components/chat-bubble.css +2 -2
- package/dist/tokens/components/control.css +41 -22
- package/dist/tokens/components/conversations.css +2 -1
- package/dist/tokens/components/data-display.css +23 -18
- package/dist/tokens/components/data-entry.css +1 -1
- package/dist/tokens/components/descriptions.css +2 -2
- package/dist/tokens/components/draggable-panel.css +2 -2
- package/dist/tokens/components/feedback.css +29 -9
- package/dist/tokens/components/float-button.css +1 -1
- package/dist/tokens/components/legal-document.css +2 -2
- package/dist/tokens/components/logo.css +2 -2
- package/dist/tokens/components/mega-menu.css +6 -4
- package/dist/tokens/components/navigation.css +27 -13
- package/dist/tokens/components/segmented.css +11 -6
- package/dist/tokens/components/separator.css +1 -1
- package/dist/tokens/components/shell.css +24 -10
- package/dist/tokens/components/table.css +13 -5
- package/dist/tokens/components/thought-chain.css +2 -2
- package/dist/tokens/components/toggle.css +3 -1
- package/dist/tokens/components/tree.css +3 -1
- package/dist/tokens/components/upload.css +8 -8
- package/dist/tokens/components/welcome.css +1 -1
- package/dist/tokens/foundation.css +30 -3
- package/docs/COMPOSITION-VS-COMPONENT.md +31 -0
- package/docs/CUSTOMER-THEMING.md +637 -1
- package/docs/FRAME-COVERAGE-REPORT.md +3 -2
- package/docs/GLASSMORPHISM-STANDARD.md +196 -0
- package/docs/THEME-API-COVERAGE.md +538 -0
- package/docs/TOKEN-RESOLUTION.md +195 -0
- package/docs/TOKENS.md +63 -24
- package/docs/asset-modules.d.ts +7 -0
- package/docs/data-display/charts.tsx +80 -0
- package/docs/data-display/data-table/index.tsx +30 -0
- package/docs/data-display/popover.tsx +1 -1
- package/docs/data-display/table.tsx +52 -0
- package/docs/feedback/sheet.tsx +10 -10
- package/docs/foundation/density.tsx +4 -4
- package/docs/i18n/messages/en.json +506 -0
- package/docs/i18n/messages/ja.json +506 -0
- package/docs/i18n/messages/vi.json +506 -0
- package/docs/layout/account-chip.tsx +2 -2
- package/docs/layout/responsive-grid.tsx +1 -1
- package/docs/navigation/toolbar.tsx +20 -12
- package/docs/providers/theme-scope.tsx +186 -0
- package/docs/showcase/caimono-price-comparison.tsx +911 -0
- package/docs/showcase/case4-login.tsx +2 -2
- package/docs/showcase/permission-matrix.tsx +13 -5
- package/docs/showcase/table-pagination.tsx +2 -1
- package/docs/showcase/tenant-brand-color.tsx +338 -0
- package/docs/showcase/theme-lab.tsx +2125 -0
- package/docs/themes/flat.css +462 -0
- package/docs/themes/glassmorphism.css +958 -0
- package/docs/themes/index.ts +219 -0
- package/docs/themes/neubrutalism.css +475 -0
- package/package.json +4 -3
- package/scripts/explain-token.mjs +382 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WHO SETS THIS TOKEN, WHO READS IT, AND WHO WOULD WIN — the resolution trace.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. The owner's complaint, verbatim: *"tao thấy rất nhiều chỗ mày cứ đè cấu hình
|
|
6
|
+
* lung tung làm ảnh hưởng component này sang component khác"* — configuration overriding
|
|
7
|
+
* configuration until a change to one component moves another. Prose cannot settle that argument.
|
|
8
|
+
* A trace can: for one token, print every DECLARATION and every READ in the package, in cascade
|
|
9
|
+
* order, and name the rule that would win.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THE ORDER IS. See docs/TOKEN-RESOLUTION.md. In short, a custom property is resolved by the
|
|
12
|
+
* ordinary cascade, so the winner at an element is the declaration from the nearest ancestor that
|
|
13
|
+
* matched, and among equals the one with the highest specificity, and among those the last:
|
|
14
|
+
*
|
|
15
|
+
* 1 inline `style` on the element (a per-instance prop)
|
|
16
|
+
* 2 the nearest scope that declares it ([data-tenant], .dark, a region wrapper)
|
|
17
|
+
* 3 `:root` in the consumer's own theme.css (unlayered, so it beats every package layer)
|
|
18
|
+
* 4 `:root` in this package's token tier (the default)
|
|
19
|
+
*
|
|
20
|
+
* THE ONE RULE THAT BREAKS IT, and the reason this script reports `FREEZE` loudly: `var()`
|
|
21
|
+
* substitutes where it is DECLARED, not where it is read. So a package default written as
|
|
22
|
+
*
|
|
23
|
+
* :root { --card-border-color: var(--border); } ← FROZEN
|
|
24
|
+
*
|
|
25
|
+
* resolves against the ROOT's `--border` once, and a `[data-tenant]` below root that changes
|
|
26
|
+
* `--border` can never reach it — step 2 of the chain is silently dead for that token. The shape
|
|
27
|
+
* that keeps the chain alive is a knob of `initial` plus the formula at the CALL SITE:
|
|
28
|
+
*
|
|
29
|
+
* :root { --card-border-color: initial; }
|
|
30
|
+
* .ui-card { border-color: var(--card-border-color, hsl(var(--border))); }
|
|
31
|
+
*
|
|
32
|
+
* This repo has paid for that distinction seven times (gh#687, gh#843, gh#848, gh#866, …), which
|
|
33
|
+
* is why it is a reported finding here and not a footnote.
|
|
34
|
+
*
|
|
35
|
+
* USAGE
|
|
36
|
+
* node scripts/explain-token.mjs --card-border-color one token, full trace
|
|
37
|
+
* node scripts/explain-token.mjs --card every token matching a prefix
|
|
38
|
+
* node scripts/explain-token.mjs --audit every FREEZE and every orphan
|
|
39
|
+
* node scripts/explain-token.mjs --json <name> machine-readable, for a gate
|
|
40
|
+
*/
|
|
41
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
42
|
+
import { join, relative } from "node:path";
|
|
43
|
+
|
|
44
|
+
const ROOT = process.cwd();
|
|
45
|
+
|
|
46
|
+
/** Source of truth for what a CONSUMER can set: the published catalog, not the stylesheets. */
|
|
47
|
+
function publishedTokens() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(readFileSync(join(ROOT, "agent/tokens.json"), "utf8"));
|
|
50
|
+
const list = Array.isArray(raw) ? raw : Object.values(raw).find(Array.isArray);
|
|
51
|
+
return new Map(list.map((t) => [t.name, t.tier ?? "component"]));
|
|
52
|
+
} catch {
|
|
53
|
+
return new Map();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cssFiles() {
|
|
58
|
+
const out = [];
|
|
59
|
+
const walk = (dir) => {
|
|
60
|
+
for (const entry of readdirSync(dir)) {
|
|
61
|
+
const full = join(dir, entry);
|
|
62
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
63
|
+
else if (entry.endsWith(".css")) out.push(full);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/* BOTH LAYOUTS, because the same script runs in two places and only one of them has `src/`.
|
|
67
|
+
* In this checkout the CSS lives in `src/tokens` and `src/styles`. In the PUBLISHED package it
|
|
68
|
+
* lives in `dist/tokens` and `dist/styles` — `package.json::files` ships `dist`, never `src`.
|
|
69
|
+
* gh#893 put this file in the tarball, and shipping it was not the same as making it work: a
|
|
70
|
+
* consumer running it against `node_modules/@godxjp/ui` scanned two directories that do not
|
|
71
|
+
* exist there, found zero CSS files, and got a confident empty answer for every token. Walking
|
|
72
|
+
* both and keeping whichever is present costs one loop and removes the whole failure mode. */
|
|
73
|
+
for (const dir of ["src/tokens", "src/styles", "dist/tokens", "dist/styles"]) {
|
|
74
|
+
try {
|
|
75
|
+
walk(join(ROOT, dir));
|
|
76
|
+
} catch {
|
|
77
|
+
/* a tree that is not there is not an error here */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out.sort();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Every `--x: value` declaration, with the selector it sits under and the layer that governs it.
|
|
85
|
+
*
|
|
86
|
+
* Comments are BLANKED rather than removed so byte offsets stay true — a `{`, `}` or `;` inside
|
|
87
|
+
* prose otherwise desynchronises the brace walk and silently moves every selector after it. That
|
|
88
|
+
* failure has a name in this repo: it is what `check-mcp-prop-sync` hit with `=>` (gh#857).
|
|
89
|
+
*/
|
|
90
|
+
function parse(file) {
|
|
91
|
+
const text = readFileSync(file, "utf8");
|
|
92
|
+
const blank = text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " "));
|
|
93
|
+
const rel = relative(ROOT, file);
|
|
94
|
+
const lineAt = (i) => text.slice(0, i).split("\n").length;
|
|
95
|
+
|
|
96
|
+
const decls = [];
|
|
97
|
+
const reads = [];
|
|
98
|
+
const stack = [];
|
|
99
|
+
let selStart = 0;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < blank.length; i += 1) {
|
|
102
|
+
const ch = blank[i];
|
|
103
|
+
if (ch === "{") {
|
|
104
|
+
stack.push(blank.slice(selStart, i).trim().replace(/\s+/g, " "));
|
|
105
|
+
selStart = i + 1;
|
|
106
|
+
} else if (ch === "}") {
|
|
107
|
+
stack.pop();
|
|
108
|
+
selStart = i + 1;
|
|
109
|
+
} else if (ch === ";") {
|
|
110
|
+
selStart = i + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Declarations: `--name: value;` — captured with their enclosing selector chain.
|
|
115
|
+
const declRe = /(--[a-z0-9-]+)\s*:\s*([^;}]+)[;}]/gi;
|
|
116
|
+
for (const m of blank.matchAll(declRe)) {
|
|
117
|
+
const line = lineAt(m.index);
|
|
118
|
+
const value = text.slice(m.index + m[1].length, m.index + m[0].length).replace(/^\s*:\s*/, "");
|
|
119
|
+
decls.push({
|
|
120
|
+
file: rel,
|
|
121
|
+
line,
|
|
122
|
+
name: m[1],
|
|
123
|
+
value: value
|
|
124
|
+
.replace(/[;}]\s*$/, "")
|
|
125
|
+
.trim()
|
|
126
|
+
.replace(/\s+/g, " "),
|
|
127
|
+
context: contextAt(blank, m.index),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Reads: `var(--name` anywhere, including inside another declaration's value.
|
|
132
|
+
for (const m of blank.matchAll(/var\(\s*(--[a-z0-9-]+)/gi)) {
|
|
133
|
+
reads.push({
|
|
134
|
+
file: rel,
|
|
135
|
+
line: lineAt(m.index),
|
|
136
|
+
name: m[1],
|
|
137
|
+
context: contextAt(blank, m.index),
|
|
138
|
+
hasFallback: text
|
|
139
|
+
.slice(m.index, m.index + 400)
|
|
140
|
+
.replace(/\s+/g, " ")
|
|
141
|
+
.includes(`${m[1]},`),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return { decls, reads };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The selector chain enclosing a byte offset, outermost first. */
|
|
148
|
+
function contextAt(blank, at) {
|
|
149
|
+
const chain = [];
|
|
150
|
+
let depth = 0;
|
|
151
|
+
let selStart = 0;
|
|
152
|
+
for (let i = 0; i < at; i += 1) {
|
|
153
|
+
const ch = blank[i];
|
|
154
|
+
if (ch === "{") {
|
|
155
|
+
chain.push({ depth, sel: blank.slice(selStart, i).trim().replace(/\s+/g, " ") });
|
|
156
|
+
depth += 1;
|
|
157
|
+
selStart = i + 1;
|
|
158
|
+
} else if (ch === "}") {
|
|
159
|
+
depth -= 1;
|
|
160
|
+
while (chain.length && chain[chain.length - 1].depth >= depth) chain.pop();
|
|
161
|
+
selStart = i + 1;
|
|
162
|
+
} else if (ch === ";" && depth >= 0) {
|
|
163
|
+
selStart = i + 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return chain.map((c) => c.sel).filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* IS THIS DECLARATION ON THE ROOT ELEMENT? — the one thing here that is mechanically decidable,
|
|
171
|
+
* and the only thing the freeze test needs.
|
|
172
|
+
*
|
|
173
|
+
* An earlier version of this file ranked every selector into four "cascade" buckets by regex and
|
|
174
|
+
* printed them "strongest last". Codex took it apart and was right: `@theme inline` and
|
|
175
|
+
* `[dir="rtl"] .ui-actions[data-fade-in-inline]` both scored TOP precedence because their text
|
|
176
|
+
* contains the substring `inline`; `:root[data-brand="crm"]` scored "descendant scope" although it
|
|
177
|
+
* matches only the root; `[data-slot="card"][data-density="tight"]` — a declaration on the
|
|
178
|
+
* component itself — scored "ambient scope". And "strongest last" sorted by ALPHABETICAL FILE
|
|
179
|
+
* ORDER, not import order, so the ordering was decoration.
|
|
180
|
+
*
|
|
181
|
+
* A tool meant to settle override disputes that manufactures precedence from substrings is worse
|
|
182
|
+
* than no tool: it sends the reader to the wrong fix with confidence. So the ranking is gone. This
|
|
183
|
+
* prints WHERE a token is declared and read, and computes only the one property it can prove.
|
|
184
|
+
*/
|
|
185
|
+
function isRootOnly(context) {
|
|
186
|
+
if (!context.length) return false;
|
|
187
|
+
return context.every((sel) =>
|
|
188
|
+
sel
|
|
189
|
+
.split(",")
|
|
190
|
+
.map((s) => s.trim())
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.every((s) =>
|
|
193
|
+
// `:root`, `:root[data-theme="dark"]`, `html` — matches the root element and nothing
|
|
194
|
+
// below it. A descendant combinator, or any selector that can match an element deeper in
|
|
195
|
+
// the tree, is NOT root-only.
|
|
196
|
+
/^(:root|html)(\[[^\]]*\]|:[a-z-]+(\([^)]*\))?)*$/.test(s),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Every token some declaration BELOW the root can move.
|
|
203
|
+
*
|
|
204
|
+
* Deliberately wider than "a theme scope": `.ui-page-container` inside `@media (max-width: 720px)`
|
|
205
|
+
* restates `--space-section-active` (src/styles/layout.css:992), and `--card-space-inset` binds it
|
|
206
|
+
* at `:root` (src/tokens/components/card.css:6) — so below 720px a Card keeps the root's inset. The
|
|
207
|
+
* previous version missed that because it only accepted selectors that LOOKED like theme scopes.
|
|
208
|
+
* Anything not root-only counts now.
|
|
209
|
+
*/
|
|
210
|
+
function scopedTokenNames(decls) {
|
|
211
|
+
return new Set(decls.filter((d) => !isRootOnly(d.context)).map((d) => d.name));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collect() {
|
|
215
|
+
const decls = [];
|
|
216
|
+
const reads = [];
|
|
217
|
+
for (const f of cssFiles()) {
|
|
218
|
+
const r = parse(f);
|
|
219
|
+
decls.push(...r.decls);
|
|
220
|
+
reads.push(...r.reads);
|
|
221
|
+
}
|
|
222
|
+
return { decls, reads };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A `:root` binding that a scope below root cannot reach.
|
|
227
|
+
*
|
|
228
|
+
* The naive test — "a `:root` declaration whose value contains `var()`" — reports 1028 sites here,
|
|
229
|
+
* and a finding list that long is one nobody reads. Most are harmless: `--actions-gap:
|
|
230
|
+
* var(--space-1)` freezes against a `--space-1` that no scope ever redeclares, so freezing it
|
|
231
|
+
* changes nothing that could ever have differed.
|
|
232
|
+
*
|
|
233
|
+
* The binding only COSTS something when the token it reads is itself restated somewhere below
|
|
234
|
+
* root — a `.dark` block, a `[data-tenant]`, a density or scale scope. Then the scope moves the
|
|
235
|
+
* source and the binding keeps the root's answer, which is the defect this repo has paid for
|
|
236
|
+
* seven times. So the signal is the INTERSECTION, and `scopedNames` is derived from the
|
|
237
|
+
* stylesheets on every run rather than hand-listed: a hand-kept list is what went blind in gh#854.
|
|
238
|
+
*/
|
|
239
|
+
function isFrozen(d, scopedNames) {
|
|
240
|
+
if (!isRootOnly(d.context)) return false;
|
|
241
|
+
if (d.value.trim() === "initial") return false;
|
|
242
|
+
const reads = [...d.value.matchAll(/var\(\s*(--[a-z0-9-]+)/gi)].map((m) => m[1]);
|
|
243
|
+
return reads.some((r) => scopedNames.has(r));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function trace(name, { decls, reads }, published, scopedNames) {
|
|
247
|
+
const mine = decls.filter((d) => d.name === name);
|
|
248
|
+
const myReads = reads.filter((r) => r.name === name);
|
|
249
|
+
const tier = published.get(name);
|
|
250
|
+
|
|
251
|
+
console.log(`\n${name}`);
|
|
252
|
+
console.log(
|
|
253
|
+
` published: ${tier ? `yes (${tier} tier)` : "NO — not in agent/tokens.json, so a consumer cannot discover it"}`,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (!mine.length) {
|
|
257
|
+
console.log(" declared: nowhere — every read falls to its inline fallback, or to nothing");
|
|
258
|
+
} else {
|
|
259
|
+
console.log(
|
|
260
|
+
` declared: ${mine.length} site(s) — DECLARATION SITES, not a cascade ranking; which one`,
|
|
261
|
+
);
|
|
262
|
+
console.log(
|
|
263
|
+
" wins at a given element depends on the DOM, and is not computed here.",
|
|
264
|
+
);
|
|
265
|
+
for (const d of mine.sort(
|
|
266
|
+
(a, b) => Number(isRootOnly(b.context)) - Number(isRootOnly(a.context)),
|
|
267
|
+
)) {
|
|
268
|
+
const frozen = isFrozen(d, scopedNames)
|
|
269
|
+
? " ← FREEZE: binds at :root against a token a scope below DOES restate"
|
|
270
|
+
: "";
|
|
271
|
+
console.log(
|
|
272
|
+
` ${isRootOnly(d.context) ? "root-only " : "below root"} ${d.file}:${d.line}`,
|
|
273
|
+
);
|
|
274
|
+
console.log(` ${d.context.join(" ") || ":root"} { ${name}: ${d.value} }${frozen}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(` read by: ${myReads.length} site(s)`);
|
|
279
|
+
for (const r of myReads.slice(0, 12)) {
|
|
280
|
+
console.log(
|
|
281
|
+
` ${r.file}:${r.line} ${r.context.join(" ") || "(top level)"}${r.hasFallback ? " [has a call-site fallback]" : " [NO fallback — undeclared means unset]"}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (myReads.length > 12) console.log(` … and ${myReads.length - 12} more`);
|
|
285
|
+
return { name, tier, decls: mine, reads: myReads };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function audit({ decls, reads }, published) {
|
|
289
|
+
const scopedNames = scopedTokenNames(decls);
|
|
290
|
+
const frozen = decls.filter((d) => isFrozen(d, scopedNames));
|
|
291
|
+
const declaredNames = new Set(decls.map((d) => d.name));
|
|
292
|
+
const orphanReads = reads.filter((r) => !declaredNames.has(r.name) && !r.hasFallback);
|
|
293
|
+
const unpublished = [...declaredNames].filter((n) => !published.has(n));
|
|
294
|
+
|
|
295
|
+
console.log(
|
|
296
|
+
`\nFROZEN — a :root binding whose SOURCE a scope below root restates (${frozen.length})`,
|
|
297
|
+
);
|
|
298
|
+
console.log(
|
|
299
|
+
` ${scopedNames.size} token(s) are restated in some scope. A :root binding that reads one of`,
|
|
300
|
+
);
|
|
301
|
+
console.log(" them keeps the root's answer, so step 2 of the chain is dead for that token.");
|
|
302
|
+
for (const d of frozen.slice(0, 40)) {
|
|
303
|
+
console.log(` ${d.file}:${d.line} ${d.name}: ${d.value}`);
|
|
304
|
+
}
|
|
305
|
+
if (frozen.length > 40) console.log(` … and ${frozen.length - 40} more`);
|
|
306
|
+
|
|
307
|
+
console.log(
|
|
308
|
+
`\nORPHAN READS — var(--x) with no declaration and no fallback (${orphanReads.length})`,
|
|
309
|
+
);
|
|
310
|
+
for (const r of orphanReads.slice(0, 20)) console.log(` ${r.file}:${r.line} ${r.name}`);
|
|
311
|
+
if (orphanReads.length > 20) console.log(` … and ${orphanReads.length - 20} more`);
|
|
312
|
+
|
|
313
|
+
console.log(
|
|
314
|
+
`\nUNPUBLISHED — declared in CSS but absent from agent/tokens.json (${unpublished.length})`,
|
|
315
|
+
);
|
|
316
|
+
console.log(" A consumer cannot discover these, so they are not part of the theme API.");
|
|
317
|
+
for (const n of unpublished.slice(0, 20)) console.log(` ${n}`);
|
|
318
|
+
if (unpublished.length > 20) console.log(` … and ${unpublished.length - 20} more`);
|
|
319
|
+
|
|
320
|
+
console.log("\nWHAT THIS CANNOT SEE — do not read a clean run as proof of no freeze:");
|
|
321
|
+
console.log(
|
|
322
|
+
" 1. CONSUMER CSS. Only this package's own token/style trees are scanned, so a token it never",
|
|
323
|
+
);
|
|
324
|
+
console.log(
|
|
325
|
+
" restates below root looks safe. `--shadow-md` binds `--shadow-color` at :root; a",
|
|
326
|
+
);
|
|
327
|
+
console.log(
|
|
328
|
+
" consumer's `[data-tenant] { --shadow-color: … }` cannot recolour it, and nothing here says so.",
|
|
329
|
+
);
|
|
330
|
+
console.log(" 2. `@supports` FALLBACKS. src/tokens/derived.css gives engines without relative");
|
|
331
|
+
console.log(
|
|
332
|
+
" colour LITERAL hover/active values, so a tenant seed stops propagating — a lost derivation,",
|
|
333
|
+
);
|
|
334
|
+
console.log(" not a var() freeze, and invisible to a test that requires a var() reference.");
|
|
335
|
+
console.log(
|
|
336
|
+
" 3. CONDITIONS ARE NOT EVALUATED. A declaration inside @media/@container is counted as if it",
|
|
337
|
+
);
|
|
338
|
+
console.log(
|
|
339
|
+
" always applies. That widens the scoped set deliberately, but it is not the cascade.",
|
|
340
|
+
);
|
|
341
|
+
console.log(
|
|
342
|
+
" 4. NO WINNER IS COMPUTED. Which declaration applies at an element depends on the DOM.",
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
console.log(
|
|
346
|
+
`\nsummary: ${declaredNames.size} declared · ${published.size} published · ${frozen.length} frozen · ${orphanReads.length} orphan read(s)`,
|
|
347
|
+
);
|
|
348
|
+
return {
|
|
349
|
+
frozen: frozen.length,
|
|
350
|
+
orphanReads: orphanReads.length,
|
|
351
|
+
unpublished: unpublished.length,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const args = process.argv.slice(2);
|
|
356
|
+
const asJson = args.includes("--json");
|
|
357
|
+
const query = args.filter((a) => a !== "--json" && a !== "--audit")[0];
|
|
358
|
+
const model = collect();
|
|
359
|
+
const published = publishedTokens();
|
|
360
|
+
|
|
361
|
+
if (args.includes("--audit") || !query) {
|
|
362
|
+
if (!query && !args.includes("--audit")) {
|
|
363
|
+
console.log("usage: node scripts/explain-token.mjs <--token-name|prefix|--audit> [--json]\n");
|
|
364
|
+
}
|
|
365
|
+
audit(model, published);
|
|
366
|
+
} else {
|
|
367
|
+
const names = [...new Set(model.decls.map((d) => d.name))]
|
|
368
|
+
.concat([...published.keys()])
|
|
369
|
+
.filter((n, i, a) => a.indexOf(n) === i)
|
|
370
|
+
.filter((n) => n === query || n.startsWith(query));
|
|
371
|
+
if (!names.length) {
|
|
372
|
+
console.log(`no token matches ${query}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
const scopedNames = scopedTokenNames(model.decls);
|
|
376
|
+
const out = names
|
|
377
|
+
.sort()
|
|
378
|
+
.slice(0, 40)
|
|
379
|
+
.map((n) => trace(n, model, published, scopedNames));
|
|
380
|
+
if (asJson) console.log(JSON.stringify(out, null, 2));
|
|
381
|
+
if (names.length > 40) console.log(`\n… ${names.length - 40} more match ${query}`);
|
|
382
|
+
}
|