@epilot/volt-ui-experimental-griffel 0.2.3 → 0.3.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/ADOPTING.md +416 -0
- package/README.md +32 -31
- package/dist/_virtual/jsx-dev-runtime.js +5 -0
- package/dist/_virtual/jsx-dev-runtime.js.map +1 -0
- package/dist/_virtual/react-jsx-dev-runtime.development.js +5 -0
- package/dist/_virtual/react-jsx-dev-runtime.development.js.map +1 -0
- package/dist/_virtual/react-jsx-dev-runtime.production.js +5 -0
- package/dist/_virtual/react-jsx-dev-runtime.production.js.map +1 -0
- package/dist/components/accordion/accordion.js +126 -66
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/alert-dialog/alert-dialog.js +239 -137
- package/dist/components/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/components/avatar/avatar-group.js +102 -47
- package/dist/components/avatar/avatar-group.js.map +1 -1
- package/dist/components/avatar/avatar.js +97 -73
- package/dist/components/avatar/avatar.js.map +1 -1
- package/dist/components/badge/badge.js +53 -37
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.js +97 -49
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/button/button.js +88 -77
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button-group/button-group.js +55 -31
- package/dist/components/button-group/button-group.js.map +1 -1
- package/dist/components/callout/callout.js +288 -164
- package/dist/components/callout/callout.js.map +1 -1
- package/dist/components/card/card.js +137 -81
- package/dist/components/card/card.js.map +1 -1
- package/dist/components/checkbox/checkbox.js +100 -49
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/collapsible-sidebar/collapsible-sidebar.js +155 -107
- package/dist/components/collapsible-sidebar/collapsible-sidebar.js.map +1 -1
- package/dist/components/command/command.js +297 -158
- package/dist/components/command/command.js.map +1 -1
- package/dist/components/data-table/data-table-body.js +114 -54
- package/dist/components/data-table/data-table-body.js.map +1 -1
- package/dist/components/data-table/data-table-column-header.js +117 -45
- package/dist/components/data-table/data-table-column-header.js.map +1 -1
- package/dist/components/data-table/data-table-column-visibility.js +98 -39
- package/dist/components/data-table/data-table-column-visibility.js.map +1 -1
- package/dist/components/data-table/data-table-content.js +90 -58
- package/dist/components/data-table/data-table-content.js.map +1 -1
- package/dist/components/data-table/data-table-error.js +83 -39
- package/dist/components/data-table/data-table-error.js.map +1 -1
- package/dist/components/data-table/data-table-header.js +88 -54
- package/dist/components/data-table/data-table-header.js.map +1 -1
- package/dist/components/data-table/data-table-loading.js +27 -15
- package/dist/components/data-table/data-table-loading.js.map +1 -1
- package/dist/components/data-table/data-table-pagination.js +27 -19
- package/dist/components/data-table/data-table-pagination.js.map +1 -1
- package/dist/components/data-table/data-table-row.js +87 -64
- package/dist/components/data-table/data-table-row.js.map +1 -1
- package/dist/components/data-table/data-table-toolbar.js +24 -16
- package/dist/components/data-table/data-table-toolbar.js.map +1 -1
- package/dist/components/data-table/data-table.js +182 -145
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/date-range-picker/date-range-picker.js +342 -214
- package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
- package/dist/components/date-range-picker/range-calendar.js +132 -92
- package/dist/components/date-range-picker/range-calendar.js.map +1 -1
- package/dist/components/date-time-picker/calendar.js +501 -317
- package/dist/components/date-time-picker/calendar.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker-context.js +64 -60
- package/dist/components/date-time-picker/date-time-picker-context.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker-input.js +249 -149
- package/dist/components/date-time-picker/date-time-picker-input.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker-panel.js +273 -165
- package/dist/components/date-time-picker/date-time-picker-panel.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker-popover.js +22 -14
- package/dist/components/date-time-picker/date-time-picker-popover.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker.js +134 -90
- package/dist/components/date-time-picker/date-time-picker.js.map +1 -1
- package/dist/components/date-time-picker/time-picker.js +198 -132
- package/dist/components/date-time-picker/time-picker.js.map +1 -1
- package/dist/components/density-provider/density-provider.js +48 -0
- package/dist/components/density-provider/density-provider.js.map +1 -0
- package/dist/components/dialog/dialog.js +280 -150
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/drawer/drawer.js +273 -163
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +378 -195
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/field/field-combobox.js +447 -240
- package/dist/components/field/field-combobox.js.map +1 -1
- package/dist/components/field/field-select.js +23 -19
- package/dist/components/field/field-select.js.map +1 -1
- package/dist/components/field/field.js +318 -209
- package/dist/components/field/field.js.map +1 -1
- package/dist/components/label/label.js +27 -19
- package/dist/components/label/label.js.map +1 -1
- package/dist/components/list/list.js +623 -369
- package/dist/components/list/list.js.map +1 -1
- package/dist/components/option-card/option-card.js +323 -159
- package/dist/components/option-card/option-card.js.map +1 -1
- package/dist/components/pill/pill.js +82 -64
- package/dist/components/pill/pill.js.map +1 -1
- package/dist/components/popover/popover.js +101 -56
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/radio/radio.js +94 -51
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/scroll-overflow/scroll-overflow.js +350 -198
- package/dist/components/scroll-overflow/scroll-overflow.js.map +1 -1
- package/dist/components/segmented-control/segmented-control.js +141 -97
- package/dist/components/segmented-control/segmented-control.js.map +1 -1
- package/dist/components/select/select.js +452 -227
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/separator/separator.js +24 -16
- package/dist/components/separator/separator.js.map +1 -1
- package/dist/components/skeleton/skeleton.js +83 -43
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/spinner/spinner.js +44 -20
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/switch/switch.js +163 -86
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/table/table-pagination.js +465 -205
- package/dist/components/table/table-pagination.js.map +1 -1
- package/dist/components/table/table.js +212 -144
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/tabs/tabs.js +94 -58
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/components/text/text.js +40 -32
- package/dist/components/text/text.js.map +1 -1
- package/dist/components/toast/toast.js +187 -111
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/toggle/toggle-group.js +40 -24
- package/dist/components/toggle/toggle-group.js.map +1 -1
- package/dist/components/toggle/toggle.js +48 -40
- package/dist/components/toggle/toggle.js.map +1 -1
- package/dist/components/tooltip/tooltip.js +101 -55
- package/dist/components/tooltip/tooltip.js.map +1 -1
- package/dist/index.d.ts +600 -427
- package/dist/index.js +215 -212
- package/dist/index.js.map +1 -1
- package/dist/lib/clear-button.js +27 -15
- package/dist/lib/clear-button.js.map +1 -1
- package/dist/lib/icons.js +18 -6
- package/dist/lib/icons.js.map +1 -1
- package/dist/packages/volt-runtime/dist/sheet.js +27 -10
- package/dist/packages/volt-runtime/dist/sheet.js.map +1 -1
- package/dist/packages/volt-runtime/dist/styles/recipe.js +76 -0
- package/dist/packages/volt-runtime/dist/styles/recipe.js.map +1 -0
- package/dist/styles/document-context.js +12 -0
- package/dist/styles/document-context.js.map +1 -0
- package/dist/styles/make-styles.js +18 -10
- package/dist/styles/make-styles.js.map +1 -1
- package/dist/tokens/core.generated.js +1238 -1166
- package/dist/tokens/core.generated.js.map +1 -1
- package/dist/tokens/index.d.ts +442 -415
- package/dist/tokens/index.js +1105 -1078
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/install.js +9 -5
- package/dist/tokens/install.js.map +1 -1
- package/dist/tokens/use-palette-tokens.js +20 -0
- package/dist/tokens/use-palette-tokens.js.map +1 -0
- package/dist/tokens.json +83 -2
- package/dist/vendor/react/cjs/react-jsx-dev-runtime.development.js +236 -0
- package/dist/vendor/react/cjs/react-jsx-dev-runtime.development.js.map +1 -0
- package/dist/vendor/react/cjs/react-jsx-dev-runtime.production.js +21 -0
- package/dist/vendor/react/cjs/react-jsx-dev-runtime.production.js.map +1 -0
- package/dist/vendor/react/jsx-dev-runtime.js +9 -0
- package/dist/vendor/react/jsx-dev-runtime.js.map +1 -0
- package/package.json +13 -5
- package/scripts/migration-diagnostics-core.mjs +859 -0
- package/scripts/migration-diagnostics.mjs +353 -0
- package/scripts/migration-sweep.mjs +179 -0
- package/AGENTS.md +0 -153
- package/AUTHORING.md +0 -292
- package/dist/packages/volt-runtime/dist/styles/make-styles.js +0 -22
- package/dist/packages/volt-runtime/dist/styles/make-styles.js.map +0 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fs-bound migration-diagnostics ORCHESTRATION for a migrated (v1 -> Griffel) consumer tree —
|
|
3
|
+
* directory/file walking, symlink boundary checks, hashing, package identity resolution, and
|
|
4
|
+
* TypeScript resolution from the local filesystem. The actual rule logic (what counts as a
|
|
5
|
+
* defect) lives in the sibling `migration-diagnostics-core.mjs`, which this module delegates every
|
|
6
|
+
* read file to via `analyzeSource` — see that module for the rule catalogue and the
|
|
7
|
+
* supported-analysis-boundary documentation. This split exists so a future non-fs caller (e.g. a
|
|
8
|
+
* remote tool analyzing a submitted string) can reuse the identical rule implementation without
|
|
9
|
+
* pulling in any of this module's filesystem/process access (WP6B-2 extraction — see
|
|
10
|
+
* `WP6B-DECISION-CORE-EXTRACTION.md`; no such caller is wired up yet).
|
|
11
|
+
*
|
|
12
|
+
* Used by the CLI wrapper (`migration-sweep.mjs`) and by tests. Importing this module has no
|
|
13
|
+
* side effects (no process.exit, no argv parsing, no eager TypeScript import — see below).
|
|
14
|
+
*
|
|
15
|
+
* TypeScript itself is resolved LAZILY from the consumer's own installation (see
|
|
16
|
+
* `resolveConsumerTypeScript`) — this package does not depend on or bundle `typescript` at
|
|
17
|
+
* runtime. If it cannot be resolved, rules `legacy-volt-class` and `hand-written-volt-css-var`
|
|
18
|
+
* (pure text, no TypeScript needed) still run; `unestablished-palette-install` is skipped with an
|
|
19
|
+
* explicit, actionable note rather than crashing the whole scan.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto"
|
|
22
|
+
import { createRequire } from "node:module"
|
|
23
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
24
|
+
import { join, relative } from "node:path"
|
|
25
|
+
import { pathToFileURL } from "node:url"
|
|
26
|
+
|
|
27
|
+
import { analyzeSource, makeFinding, RULES, SCRIPT_EXT_RE } from "./migration-diagnostics-core.mjs"
|
|
28
|
+
|
|
29
|
+
// Re-exported so existing imports of `RULES` from this module keep working unchanged — the rule
|
|
30
|
+
// catalogue itself now lives in the core (single source of truth), per WP6B-2.
|
|
31
|
+
export { RULES }
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Package/token-family identity — derived from the built package, never hardcoded
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function sha256(content) {
|
|
38
|
+
return createHash("sha256").update(content).digest("hex")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readGeneratedFamilies(packageDir, file) {
|
|
42
|
+
const full = join(packageDir, "dist/tokens", file)
|
|
43
|
+
if (!existsSync(full)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Missing generated token file: dist/tokens/${file} under ${packageDir}. The package is not built — run \`bun run build\` there first.`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
const content = readFileSync(full, "utf8")
|
|
49
|
+
const families = new Set([...content.matchAll(/--volt2-[a-z0-9]+-([a-z]+)-/g)].map((m) => m[1]))
|
|
50
|
+
if (families.size === 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Generated token file dist/tokens/${file} under ${packageDir} contains no recognizable family declarations. ` +
|
|
53
|
+
"It exists but is empty or malformed (does not match the expected --volt2-<hash>-<family>- pattern) — " +
|
|
54
|
+
"the package build is likely stale or broken. Re-run `bun run build` there, do not proceed with an empty family set."
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
return { families, content }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves package name/version, its core/palette token family sets, and a deterministic hash of
|
|
62
|
+
* the generated-token inputs those sets were read from, from a built package directory. Throws
|
|
63
|
+
* (never silently falls back to empty sets) on a missing/unreadable package.json, missing
|
|
64
|
+
* generated token files, or generated files with no recognizable family declarations — "invalid
|
|
65
|
+
* package selection" is a hard error, not a quietly-disabled rule.
|
|
66
|
+
*/
|
|
67
|
+
export function resolvePackageIdentity(packageDir) {
|
|
68
|
+
const pkgJsonPath = join(packageDir, "package.json")
|
|
69
|
+
if (!existsSync(pkgJsonPath)) {
|
|
70
|
+
throw new Error(`Invalid package selection: no package.json at ${packageDir}`)
|
|
71
|
+
}
|
|
72
|
+
let pkgJson
|
|
73
|
+
try {
|
|
74
|
+
pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"))
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new Error(`Invalid package selection: could not parse ${pkgJsonPath}: ${err.message}`)
|
|
77
|
+
}
|
|
78
|
+
if (!pkgJson.name || !pkgJson.version) {
|
|
79
|
+
throw new Error(`Invalid package selection: ${pkgJsonPath} is missing "name" or "version"`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const coreRead = readGeneratedFamilies(packageDir, "core.generated.js")
|
|
83
|
+
const paletteRead = readGeneratedFamilies(packageDir, "palette.generated.js")
|
|
84
|
+
const core = coreRead.families
|
|
85
|
+
const palette = new Set([...paletteRead.families].filter((f) => !core.has(f)))
|
|
86
|
+
const generatedInputHash = sha256(`${coreRead.content}\u0000${paletteRead.content}`)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
packageDir,
|
|
90
|
+
packageName: pkgJson.name,
|
|
91
|
+
packageVersion: pkgJson.version,
|
|
92
|
+
core,
|
|
93
|
+
palette,
|
|
94
|
+
generatedInputHash,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// TypeScript — lazily resolved from the consumer's own installation, never bundled/depended-on
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolves `typescript` using Node's own module resolution, searching from each of `searchRoots`
|
|
104
|
+
* in order (first match wins). Never installs anything. Throws a clear, actionable error — never
|
|
105
|
+
* a cryptic MODULE_NOT_FOUND — naming exactly what to do, if none of the roots have it.
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveConsumerTypeScript(searchRoots) {
|
|
108
|
+
const roots = searchRoots.filter(Boolean)
|
|
109
|
+
const req = createRequire(join(roots[0] ?? process.cwd(), "noop-resolver.cjs"))
|
|
110
|
+
let tsPath
|
|
111
|
+
for (const root of roots) {
|
|
112
|
+
try {
|
|
113
|
+
tsPath = req.resolve("typescript", { paths: [root] })
|
|
114
|
+
break
|
|
115
|
+
} catch {
|
|
116
|
+
// try the next root
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!tsPath) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'Could not resolve a "typescript" installation from: ' +
|
|
122
|
+
roots.join(", ") +
|
|
123
|
+
'. The "unestablished-palette-install" rule parses source with your project\'s own TypeScript — ' +
|
|
124
|
+
"it is not bundled with this package. Install it as a devDependency in the project being scanned " +
|
|
125
|
+
"(`npm install -D typescript` or `bun add -d typescript`) and re-run. " +
|
|
126
|
+
"legacy-volt-class and hand-written-volt-css-var do not need TypeScript and are unaffected."
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
const mod = await import(pathToFileURL(tsPath).href)
|
|
130
|
+
return { ts: mod.default ?? mod, tsPath }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// File walking
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
const SCAN_EXT_RE = /\.(tsx?|jsx?|s?css)$/
|
|
138
|
+
/** `*.generated.*` build output — the generator's own emitted source of truth, never
|
|
139
|
+
* "hand-written" by a consumer. See §4 of the WP6A report. */
|
|
140
|
+
const GENERATED_FILE_RE = /\.generated\.[a-z]+$/i
|
|
141
|
+
/** A leading `/* GENERATED ... *\/` / `// GENERATED ...` marker — this repo's own convention
|
|
142
|
+
* (`packages/tokens/scripts/build.ts`), and common generally; catches generated files that don't
|
|
143
|
+
* follow the filename convention (e.g. tokens/index.ts). */
|
|
144
|
+
const GENERATED_HEADER_RE = /^\s*(\/\*|\/\/|<!--)\s*generated\b/i
|
|
145
|
+
|
|
146
|
+
function isExcluded(rel, excludes) {
|
|
147
|
+
return excludes.some((ex) => rel === ex || rel.startsWith(`${ex}/`))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isGeneratedByHeader(filePath) {
|
|
151
|
+
try {
|
|
152
|
+
const fd = readFileSync(filePath, { encoding: "utf8", flag: "r" }).slice(0, 200)
|
|
153
|
+
return GENERATED_HEADER_RE.test(fd.split("\n")[0] ?? "")
|
|
154
|
+
} catch {
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function walk(dir, { excludes, root, skippedSymlinks, skippedGenerated }) {
|
|
160
|
+
const out = []
|
|
161
|
+
let entries
|
|
162
|
+
try {
|
|
163
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new Error(`Cannot read directory ${dir}: ${err.message}`)
|
|
166
|
+
}
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue
|
|
169
|
+
const full = join(dir, entry.name)
|
|
170
|
+
const rel = relative(root, full)
|
|
171
|
+
if (isExcluded(rel, excludes)) continue
|
|
172
|
+
if (entry.isSymbolicLink()) {
|
|
173
|
+
skippedSymlinks.push(rel)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
out.push(...walk(full, { excludes, root, skippedSymlinks, skippedGenerated }))
|
|
178
|
+
} else if (SCAN_EXT_RE.test(entry.name)) {
|
|
179
|
+
if (GENERATED_FILE_RE.test(entry.name) || isGeneratedByHeader(full)) {
|
|
180
|
+
skippedGenerated.push(rel)
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
out.push(full)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Reads one file and delegates the actual analysis to the core's `analyzeSource` — this function
|
|
190
|
+
* is the ENTIRE fs-touching surface for per-file analysis; `analyzeSource` itself never reads a
|
|
191
|
+
* file. `relPath` doubles as `analyzeSource`'s `fileNameHint` (parse-mode detection + the `file`
|
|
192
|
+
* label on findings) — the same string was already used for both purposes before this split. */
|
|
193
|
+
function analyzeFile(ts, identity, filePath, relPath) {
|
|
194
|
+
const raw = readFileSync(filePath, "utf8")
|
|
195
|
+
const findings = analyzeSource(raw, { identity, ts, fileNameHint: relPath })
|
|
196
|
+
return { findings, contentHash: sha256(raw) }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Public entrypoint
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolves `targetPath` into a boundary-checked root: rejects a target (or its auto-selected
|
|
205
|
+
* `src/`) that is itself a symlink, rather than silently following it (which could escape the
|
|
206
|
+
* requested scope entirely). Supports both a directory target and a single FILE target.
|
|
207
|
+
*/
|
|
208
|
+
function resolveScanRoot(targetPath) {
|
|
209
|
+
const targetLstat = lstatSync(targetPath)
|
|
210
|
+
if (targetLstat.isSymbolicLink()) {
|
|
211
|
+
return { error: `Target path is a symlink (${targetPath} -> ${realpathSync(targetPath)}); point directly at the real path instead of a symlink.` }
|
|
212
|
+
}
|
|
213
|
+
if (targetLstat.isFile()) {
|
|
214
|
+
return { file: targetPath }
|
|
215
|
+
}
|
|
216
|
+
if (!targetLstat.isDirectory()) {
|
|
217
|
+
return { error: `Target path is neither a file nor a directory: ${targetPath}` }
|
|
218
|
+
}
|
|
219
|
+
const srcDir = join(targetPath, "src")
|
|
220
|
+
if (existsSync(srcDir)) {
|
|
221
|
+
const srcLstat = lstatSync(srcDir)
|
|
222
|
+
if (srcLstat.isSymbolicLink()) {
|
|
223
|
+
return {
|
|
224
|
+
error: `The target's auto-selected src/ is a symlink (${srcDir} -> ${realpathSync(srcDir)}); this would scan outside the requested scope. Point the target directly at the real directory to scan, or pass it as the target itself.`,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (srcLstat.isDirectory()) {
|
|
228
|
+
return { root: srcDir }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { root: targetPath }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Scans one target (a repo root, whose `src/` is scanned if present; any directory directly; or a
|
|
236
|
+
* single file) and returns `{ target, filesScanned, targetInputHash, skippedSymlinks,
|
|
237
|
+
* skippedGenerated, toolingWarning?, findings }` on success, or `{ target, error }` for a
|
|
238
|
+
* missing/unreadable path, a symlinked target/src boundary, or an internal walk failure — never a
|
|
239
|
+
* silent "skipped" a caller could read as success by checking only `findings`.
|
|
240
|
+
*/
|
|
241
|
+
export async function analyzeTarget(targetPath, { identity, excludes = [], resolveTs } = {}) {
|
|
242
|
+
if (!identity) {
|
|
243
|
+
throw new Error("analyzeTarget requires a resolved package `identity` — see resolvePackageIdentity()")
|
|
244
|
+
}
|
|
245
|
+
if (!existsSync(targetPath)) {
|
|
246
|
+
return { target: targetPath, error: `Target path does not exist: ${targetPath}` }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let scanRoot
|
|
250
|
+
try {
|
|
251
|
+
scanRoot = resolveScanRoot(targetPath)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return { target: targetPath, error: `Target path is not readable: ${targetPath} (${err.message})` }
|
|
254
|
+
}
|
|
255
|
+
if (scanRoot.error) {
|
|
256
|
+
return { target: targetPath, error: scanRoot.error }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let files
|
|
260
|
+
const skippedSymlinks = []
|
|
261
|
+
const skippedGenerated = []
|
|
262
|
+
if (scanRoot.file) {
|
|
263
|
+
if (GENERATED_FILE_RE.test(scanRoot.file) || isGeneratedByHeader(scanRoot.file)) {
|
|
264
|
+
skippedGenerated.push(relative(targetPath, scanRoot.file) || scanRoot.file)
|
|
265
|
+
files = []
|
|
266
|
+
} else if (SCAN_EXT_RE.test(scanRoot.file)) {
|
|
267
|
+
files = [scanRoot.file]
|
|
268
|
+
} else {
|
|
269
|
+
return { target: targetPath, error: `Target file has an unsupported extension for scanning: ${scanRoot.file}` }
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
try {
|
|
273
|
+
files = walk(scanRoot.root, { excludes, root: targetPath, skippedSymlinks, skippedGenerated })
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return { target: targetPath, error: err.message }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// unestablished-palette-install only applies to script files — a CSS/SCSS-only target never
|
|
280
|
+
// needs TypeScript at all, and must not report a tooling problem for a rule that could not have
|
|
281
|
+
// run there anyway. Reuses the core's own SCRIPT_EXT_RE so this check can never drift from what
|
|
282
|
+
// analyzeSource itself treats as script-parseable.
|
|
283
|
+
const hasScriptFiles = files.some((f) => SCRIPT_EXT_RE.test(f))
|
|
284
|
+
|
|
285
|
+
let ts = null
|
|
286
|
+
let toolingWarning
|
|
287
|
+
let resolvedTypeScript
|
|
288
|
+
let analysisIncomplete = false
|
|
289
|
+
if (hasScriptFiles) {
|
|
290
|
+
// No explicit resolver from the caller does NOT mean "skip rule 3 silently" — the engine's
|
|
291
|
+
// own default path still attempts real resolution (cwd, the target, the package dir) so a
|
|
292
|
+
// caller of the public API who simply omits `resolveTs` gets the same honest
|
|
293
|
+
// toolingWarning/analysisIncomplete signal a CLI invocation would.
|
|
294
|
+
const resolver = resolveTs ?? (() => resolveConsumerTypeScript([process.cwd(), targetPath, identity.packageDir]))
|
|
295
|
+
try {
|
|
296
|
+
const resolved = await resolver()
|
|
297
|
+
ts = resolved.ts
|
|
298
|
+
let tsVersion
|
|
299
|
+
try {
|
|
300
|
+
tsVersion = JSON.parse(readFileSync(join(resolved.tsPath, "..", "..", "package.json"), "utf8")).version
|
|
301
|
+
} catch {
|
|
302
|
+
tsVersion = undefined
|
|
303
|
+
}
|
|
304
|
+
resolvedTypeScript = { path: resolved.tsPath, version: tsVersion }
|
|
305
|
+
} catch (err) {
|
|
306
|
+
toolingWarning = err.message
|
|
307
|
+
// A required rule could not run at all for this target — the scan is INCOMPLETE, not clean.
|
|
308
|
+
// A caller must not read `findings` alone (which may still be a truthful, non-empty result
|
|
309
|
+
// for the two textual rules) as proof "everything was checked."
|
|
310
|
+
analysisIncomplete = true
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const findings = []
|
|
315
|
+
const perFileHashes = []
|
|
316
|
+
for (const file of files) {
|
|
317
|
+
const rel = relative(targetPath, file) || file.split("/").pop()
|
|
318
|
+
try {
|
|
319
|
+
const { findings: fileFindings, contentHash } = analyzeFile(ts, identity, file, rel)
|
|
320
|
+
findings.push(...fileFindings)
|
|
321
|
+
perFileHashes.push(`${rel}:${contentHash}`)
|
|
322
|
+
} catch (err) {
|
|
323
|
+
findings.push(
|
|
324
|
+
makeFinding(identity, {
|
|
325
|
+
code: "analysis-error",
|
|
326
|
+
severity: "error",
|
|
327
|
+
confidence: "high",
|
|
328
|
+
file: rel,
|
|
329
|
+
line: 1,
|
|
330
|
+
column: 1,
|
|
331
|
+
evidence: String(err?.message ?? err),
|
|
332
|
+
explanation: "The diagnostic engine could not parse or read this file.",
|
|
333
|
+
suggestion:
|
|
334
|
+
"Fix the underlying syntax/read error, or pass --exclude for this path if it is intentionally out of scope (e.g. generated code).",
|
|
335
|
+
})
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const targetInputHash = sha256(perFileHashes.sort().join("\n"))
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
target: targetPath,
|
|
344
|
+
filesScanned: files.length,
|
|
345
|
+
targetInputHash,
|
|
346
|
+
skippedSymlinks,
|
|
347
|
+
skippedGenerated,
|
|
348
|
+
...(toolingWarning ? { toolingWarning } : {}),
|
|
349
|
+
...(resolvedTypeScript ? { resolvedTypeScript } : {}),
|
|
350
|
+
...(analysisIncomplete ? { analysisIncomplete: true } : {}),
|
|
351
|
+
findings,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI for the migration-diagnostics engine — checks a migrated consumer for the defects that
|
|
3
|
+
* survive every other gate (see migration-diagnostics.mjs for the rule catalogue and the
|
|
4
|
+
* analysis-boundary documentation; this file is argv parsing and output formatting only).
|
|
5
|
+
*
|
|
6
|
+
* bun scripts/migration-sweep.mjs [--json] [--package <dir>] [--exclude <path>]... <path> [...]
|
|
7
|
+
*
|
|
8
|
+
* Exit code: 2 if any target has a hard input/argument error OR the requested analysis was
|
|
9
|
+
* INCOMPLETE (a script-file target whose TypeScript could not be resolved — a required rule did
|
|
10
|
+
* not run, which is not the same as "it ran and found nothing"); else 1 if any finding has
|
|
11
|
+
* severity "error" (legacy-volt-class / hand-written-volt-css-var — definite, unconditional textual
|
|
12
|
+
* defects); else 0. unestablished-palette-install never reports severity "error" — single-file
|
|
13
|
+
* analysis cannot prove a defect it might be wrong about (a bootstrap/provider elsewhere), so its
|
|
14
|
+
* findings are always "warning" and never independently produce a nonzero exit.
|
|
15
|
+
*/
|
|
16
|
+
import { resolve } from "node:path"
|
|
17
|
+
import { analyzeTarget, resolveConsumerTypeScript, resolvePackageIdentity, RULES } from "./migration-diagnostics.mjs"
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PACKAGE_DIR = resolve(import.meta.dirname, "..")
|
|
20
|
+
const KNOWN_FLAGS = new Set(["--json", "--package", "--exclude"])
|
|
21
|
+
|
|
22
|
+
function usage() {
|
|
23
|
+
return (
|
|
24
|
+
"usage: node migration-sweep.mjs [--json] [--package <dir>] [--exclude <path>]... <path> [...]\n" +
|
|
25
|
+
" <path> a repo root (scans its src/), a directory, or a single source file\n" +
|
|
26
|
+
" --json machine-readable JSON output instead of text (errors too)\n" +
|
|
27
|
+
" --package <dir> Griffel package to read token families from (default: this package)\n" +
|
|
28
|
+
" --exclude <path> path relative to the target to skip, repeatable\n" +
|
|
29
|
+
" During a flag-gated migration, point it at the NEW tree:\n" +
|
|
30
|
+
" node migration-sweep.mjs src/components/<name>-griffel src/surface"
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns `{ targets, excludes, json, packageDir }` or `{ argError }` for a missing operand or
|
|
35
|
+
* an unrecognized `--flag` — never silently treats either as a target path. */
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const targets = []
|
|
38
|
+
const excludes = []
|
|
39
|
+
let json = false
|
|
40
|
+
let packageDir = DEFAULT_PACKAGE_DIR
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i]
|
|
44
|
+
if (!a.startsWith("--")) {
|
|
45
|
+
targets.push(a)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (!KNOWN_FLAGS.has(a)) {
|
|
49
|
+
return { argError: `Unknown flag: ${a}` }
|
|
50
|
+
}
|
|
51
|
+
if (a === "--json") {
|
|
52
|
+
json = true
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
const operand = argv[i + 1]
|
|
56
|
+
if (operand === undefined || operand.startsWith("--")) {
|
|
57
|
+
return { argError: `Missing operand for ${a}` }
|
|
58
|
+
}
|
|
59
|
+
i++
|
|
60
|
+
if (a === "--package") packageDir = resolve(operand)
|
|
61
|
+
else if (a === "--exclude") excludes.push(operand)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { targets, excludes, json, packageDir }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function emitError(json, message, extra = {}) {
|
|
68
|
+
if (json) {
|
|
69
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: message, ...extra }, null, 2))
|
|
70
|
+
} else {
|
|
71
|
+
console.error(message)
|
|
72
|
+
if (extra.usage) console.error(extra.usage)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runCli(argv) {
|
|
77
|
+
const parsed = parseArgs(argv)
|
|
78
|
+
// Flag parsing errors can occur before we know whether --json was requested (e.g. an unknown
|
|
79
|
+
// flag before --json itself) — best-effort: honor --json if it appears anywhere in argv.
|
|
80
|
+
const jsonRequested = parsed.json ?? argv.includes("--json")
|
|
81
|
+
|
|
82
|
+
if (parsed.argError) {
|
|
83
|
+
emitError(jsonRequested, parsed.argError, { usage: usage() })
|
|
84
|
+
return 2
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { targets, excludes, json, packageDir } = parsed
|
|
88
|
+
if (!targets.length) {
|
|
89
|
+
emitError(json, "No target path(s) given.", { usage: usage() })
|
|
90
|
+
return 2
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let identity
|
|
94
|
+
try {
|
|
95
|
+
identity = resolvePackageIdentity(packageDir)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
emitError(json, String(err?.message ?? err))
|
|
98
|
+
return 2
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const resolveTs = () => resolveConsumerTypeScript([process.cwd(), ...targets.map((t) => resolve(t)), packageDir])
|
|
102
|
+
|
|
103
|
+
const results = []
|
|
104
|
+
for (const t of targets) {
|
|
105
|
+
results.push(await analyzeTarget(resolve(t), { identity, excludes, resolveTs }))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (json) {
|
|
109
|
+
console.log(
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
package: {
|
|
114
|
+
name: identity.packageName,
|
|
115
|
+
version: identity.packageVersion,
|
|
116
|
+
generatedInputHash: identity.generatedInputHash,
|
|
117
|
+
},
|
|
118
|
+
rules: RULES,
|
|
119
|
+
results,
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
} else {
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
const name = r.target.split("/").pop()
|
|
128
|
+
if (r.error) {
|
|
129
|
+
console.log(` ✗ ${name} — ${r.error}`)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (r.resolvedTypeScript) {
|
|
133
|
+
console.log(` typescript: ${r.resolvedTypeScript.version ?? "unknown version"} (${r.resolvedTypeScript.path})`)
|
|
134
|
+
}
|
|
135
|
+
if (r.skippedGenerated?.length) {
|
|
136
|
+
console.log(` skipped as generated: ${r.skippedGenerated.join(", ")}`)
|
|
137
|
+
}
|
|
138
|
+
if (r.skippedSymlinks?.length) {
|
|
139
|
+
console.log(` skipped symlinks: ${r.skippedSymlinks.join(", ")}`)
|
|
140
|
+
}
|
|
141
|
+
if (r.toolingWarning) {
|
|
142
|
+
// Never a green check here, even if findings is empty — the required rule did not run,
|
|
143
|
+
// so "no findings" means "not fully checked", not "checked and clean".
|
|
144
|
+
console.log(
|
|
145
|
+
` ! ${name} — INCOMPLETE ANALYSIS: ${r.toolingWarning}` +
|
|
146
|
+
(r.findings.length ? ` (${r.findings.length} finding(s) from the rules that did run, below)` : "")
|
|
147
|
+
)
|
|
148
|
+
for (const f of r.findings) {
|
|
149
|
+
console.log(
|
|
150
|
+
` [${f.severity}] ${f.code.padEnd(28)} ${f.file}:${f.line}:${f.column} ${f.evidence}`
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (!r.findings.length) {
|
|
156
|
+
console.log(` ✓ ${name} (${r.filesScanned} file(s) scanned, targetInputHash ${r.targetInputHash.slice(0, 12)}…)`)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
console.log(` ✗ ${name} — ${r.findings.length} finding(s)`)
|
|
160
|
+
for (const f of r.findings) {
|
|
161
|
+
console.log(
|
|
162
|
+
` [${f.severity}] ${f.code.padEnd(28)} ${f.file}:${f.line}:${f.column} ${f.evidence}`
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
console.log("")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hasIncompleteOrInputError = results.some((r) => r.error || r.analysisIncomplete)
|
|
170
|
+
if (hasIncompleteOrInputError) return 2
|
|
171
|
+
|
|
172
|
+
const hasHardFailure = results.some((r) => r.findings?.some((f) => f.severity === "error"))
|
|
173
|
+
return hasHardFailure ? 1 : 0
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
177
|
+
const code = await runCli(process.argv.slice(2))
|
|
178
|
+
process.exit(code)
|
|
179
|
+
}
|
package/AGENTS.md
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
# Agent rules — `@epilot/volt-ui-experimental-griffel`
|
|
2
|
-
|
|
3
|
-
These rules govern every file under `packages/experimental-griffel/`. They are normative: where any
|
|
4
|
-
other document in this package disagrees with this file, **this file wins** and the other one is a
|
|
5
|
-
bug worth fixing.
|
|
6
|
-
|
|
7
|
-
`packages/experimental/AGENTS.md` does **not** apply here. It governs
|
|
8
|
-
`@epilot/volt-ui-experimental-css-modules`, a different package with a different styling engine, and
|
|
9
|
-
its rules on CSS Modules, `var(--volt-*)` tokens and `bun run *:experimental` scripts are all wrong
|
|
10
|
-
for this one. The two are siblings, not parent and child.
|
|
11
|
-
|
|
12
|
-
**What the other documents are for.** `AUTHORING.md` explains the defect behind each rule — read it
|
|
13
|
-
when you want to know _why_, not _what_. `DECISIONS.md` records architecture rationale as a dated
|
|
14
|
-
log. `STATUS.md` is a snapshot of measurements and progress at a point in time; treat every number
|
|
15
|
-
in it as of its commit, never as a live fact. This file holds no counts and no measurements on
|
|
16
|
-
purpose, because those are what rot.
|
|
17
|
-
|
|
18
|
-
## Architecture, as it is now
|
|
19
|
-
|
|
20
|
-
- **Primitives are Radix**, at the same versions v1 pins, plus `sonner` for Toast and `cmdk` for
|
|
21
|
-
Command. There is no `@base-ui/react` in this package — not in the manifest, not in `src`.
|
|
22
|
-
Anything telling you to use Base UI primitives, or to prefer `data-checked` over Radix's
|
|
23
|
-
`data-state="checked"`, is describing an abandoned detour. Radix state attributes are
|
|
24
|
-
**value-based**: `&[data-state='checked']`, not `&[data-checked]`.
|
|
25
|
-
- **Styling is Griffel**, emitted as runtime atoms. No Tailwind, no `cn`, no `tailwind-merge`, no
|
|
26
|
-
shipped stylesheet. Use `mergeClasses` from `@epilot/volt-ui-runtime` — this package
|
|
27
|
-
does not import `@griffel/core` anywhere, and `gate:one-bare-specifier` enforces that.
|
|
28
|
-
- **The runtime target is React 18**, while the repo compiles against `@types/react@19`. Both suites
|
|
29
|
-
must stay green.
|
|
30
|
-
- ESM only. `react` and `react-dom` are peers; everything else is vendored into `dist`.
|
|
31
|
-
`sideEffects: false`.
|
|
32
|
-
|
|
33
|
-
## Authoring rules
|
|
34
|
-
|
|
35
|
-
1. **`&` must be the first token of every selector.** Griffel prepends its class to the whole
|
|
36
|
-
selector and substitutes `&` nowhere else, so `"[data-pressed] &"` emits
|
|
37
|
-
`.hash[data-pressed] .hash` and can never match. No warning, no error, no failing test. To style a
|
|
38
|
-
descendant from an ancestor's state, declare the rule on the _ancestor's_ slot and reach down via
|
|
39
|
-
`SLOT_ATTR`. A selector list is fine when each part is legal alone.
|
|
40
|
-
`src/ancestor-selector.test.ts` enforces this.
|
|
41
|
-
|
|
42
|
-
2. **Declaration order inside a slot encodes precedence, and v1's order does not.** Griffel emits
|
|
43
|
-
atoms in source order, so the later rule wins. v1's precedence comes from Tailwind's utility
|
|
44
|
-
layer, where `aria-invalid:` beats `disabled:` regardless of class order. Transcribing v1's order
|
|
45
|
-
inverts the outcome. Put the state that must **win** last.
|
|
46
|
-
|
|
47
|
-
3. **Match a neighbour's property shape, not just its value.** Griffel dedupes only when expanded
|
|
48
|
-
property keys match, so `paddingLeft`/`paddingRight` against a neighbour's
|
|
49
|
-
`shorthands.paddingInline` leaves both standing and the cascade picks the wrong one.
|
|
50
|
-
|
|
51
|
-
4. **No CSS shorthands** — an atom is one declaration, so a shorthand cannot be resolved against a
|
|
52
|
-
longhand. Use `shorthands.*`. Griffel only warns at runtime, so the warnings-as-errors test is
|
|
53
|
-
what catches it.
|
|
54
|
-
|
|
55
|
-
5. **Animations use the object form**, `animationName: { from: {…}, to: {…} }`. Griffel hashes
|
|
56
|
-
keyframe names from content but does **not** apply the class-name salt to them, so a hand-authored
|
|
57
|
-
global `@keyframes` collides across co-resident copies silently.
|
|
58
|
-
|
|
59
|
-
6. **`forwardRef` is required.** `ref`-as-a-plain-prop is React 19; every consumer runs React 18,
|
|
60
|
-
where a function component without `forwardRef` never receives `ref`.
|
|
61
|
-
|
|
62
|
-
7. **`asChild` is supported and should stay supported.** Components wrapping a Radix primitive
|
|
63
|
-
inherit it through `React.ComponentProps<typeof Primitive.X>` and need no work. Components that
|
|
64
|
-
render their own element — Button and BreadcrumbItem — implement it with Radix's `Slot`. v1
|
|
65
|
-
exposes it on thirteen components and consumers use it to wrap router links, so dropping it
|
|
66
|
-
breaks call sites. It was briefly removed while this package was on Base UI; that reason no
|
|
67
|
-
longer exists. Do not expose Base UI's `render` prop, which is a different thing.
|
|
68
|
-
|
|
69
|
-
8. **Colour is a value, never part of a class name.** Components with a `color` prop follow Badge:
|
|
70
|
-
atoms read inline custom properties the component sets. Never emit a per-colour rule.
|
|
71
|
-
|
|
72
|
-
9. **Call `ensurePaletteTokens()` whenever you reference any family outside
|
|
73
|
-
accent / gray / error / warning / success / info** — whether or not the component has a `color`
|
|
74
|
-
prop. Those six are in the core sheet; every other family is in the palette sheet, which is not
|
|
75
|
-
loaded by default, and referencing it without installing gives an invalid computed value and no
|
|
76
|
-
error.
|
|
77
|
-
|
|
78
|
-
10. **No module-scope DOM or stylesheet mutation.** Installing tokens, inserting styles or touching
|
|
79
|
-
`document` must happen lazily at first render, never at import time — that is what makes
|
|
80
|
-
`sideEffects: false` true and keeps an unused import free.
|
|
81
|
-
`src/components/avatar/avatar.tsx` currently calls `ensurePaletteTokens()` at module scope. **That
|
|
82
|
-
is a known defect, not a precedent.** Do not copy it.
|
|
83
|
-
|
|
84
|
-
11. **`SLOT_ATTR` for selectors, `data-slot` for diagnostics.** Emit both, and never write a selector
|
|
85
|
-
keyed on `data-slot` — those values are shared with v1, so such a rule also matches v1-rendered
|
|
86
|
-
DOM. Portalled parts each carry their own styling class and their own slot marker; a class on an
|
|
87
|
-
ancestor outside the portal does not reach them.
|
|
88
|
-
|
|
89
|
-
12. **Preserve the caller's `className` and forward DOM refs.**
|
|
90
|
-
|
|
91
|
-
## Tokens
|
|
92
|
-
|
|
93
|
-
- Use a token wherever one exists (`tokens.textSm`, `tokens.spacingElement2`, `tokens.grayA11`).
|
|
94
|
-
Where none exists, a literal is correct — say so in your report. Never
|
|
95
|
-
`calc(var(--spacing) * n)`: reintroducing Tailwind's numeric scale is what this rebuild exists to
|
|
96
|
-
avoid.
|
|
97
|
-
- **Spacing and size are different axes.** There is no dimension token family, so `width: "0.75rem"`
|
|
98
|
-
on an svg stays a literal even though `spacingElement3` shares the number.
|
|
99
|
-
- **Never edit a generated token file, and never hardcode a namespace or salt hash.** Run
|
|
100
|
-
`bun run tokens`.
|
|
101
|
-
- Core and palette carry **independent** namespaces, so a palette-only change does not rename core
|
|
102
|
-
properties. `GRIFFEL_CLASS_SALT` covers the pinned Griffel version and nothing else — token
|
|
103
|
-
isolation is the namespaces' job, not the salt's.
|
|
104
|
-
|
|
105
|
-
## Rule 0 — v1's rendered OUTPUT is the fidelity target
|
|
106
|
-
|
|
107
|
-
Not v1's source, and not a task file's summary of it. Where a task file and v1 disagree on a visual
|
|
108
|
-
value, v1 wins; where v1's source and what v1 actually renders disagree, **what it renders wins**.
|
|
109
|
-
Reproduce v1's defects when they are cosmetic, and say so. Do not reproduce one that makes a
|
|
110
|
-
component non-functional. Every intentional difference needs an explicit, reviewed VR exception.
|
|
111
|
-
|
|
112
|
-
There is one standing exception, and it is the only one: **leading**. v1's shipped sheet references
|
|
113
|
-
every `--text-*--line-height` unprefixed and so applies none of them, and v2 follows what v1
|
|
114
|
-
declares instead of what it renders. That is decided in `DECISIONS.md` D9, corrected on the v1 arm of
|
|
115
|
-
the VR harness by `V1_DECLARED_LEADING_SHIM`, and dated by `vr/v1-leading-references.test.ts`. Pair
|
|
116
|
-
every `fontSize: tokens.text*` with the matching `lineHeight: tokens.leading*` in the same slot; do
|
|
117
|
-
not read it as licence to prefer v1's source anywhere else.
|
|
118
|
-
|
|
119
|
-
## Verifying
|
|
120
|
-
|
|
121
|
-
Run these, by name — not against a remembered count:
|
|
122
|
-
|
|
123
|
-
```sh
|
|
124
|
-
bun run build # tokens, tsc, vite
|
|
125
|
-
bun run test # React 19
|
|
126
|
-
bun run react18:test # React 18
|
|
127
|
-
bun run gate:vr # visual regression against v1
|
|
128
|
-
bun run gates # all architecture gates, fail-fast
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Wire your component into `src/index.ts` **before** running the gates: several build the package from
|
|
132
|
-
its entrypoint, so an unexported component is invisible to them and they will pass without having
|
|
133
|
-
seen it.
|
|
134
|
-
|
|
135
|
-
**A green gate is evidence for the assertions that gate actually makes, and nothing wider.** Every
|
|
136
|
-
gate here has a `GATE_NEGATIVE_CONTROL` mode, documented in its own header, demonstrated to trip
|
|
137
|
-
exactly the checks it should. If you add or strengthen an architectural claim, add a control that
|
|
138
|
-
breaks the property under test and show it failing. A check that cannot fail is worse than no check,
|
|
139
|
-
because it reads as coverage — this suite had two such checks for months.
|
|
140
|
-
|
|
141
|
-
## Done means
|
|
142
|
-
|
|
143
|
-
Exported from `src/index.ts`, both suites green, tsc clean, VR specimens covering the component's
|
|
144
|
-
**states** and not merely its default, and gates green. A component with no state-bearing specimens
|
|
145
|
-
is not finished.
|
|
146
|
-
|
|
147
|
-
## Working agreements
|
|
148
|
-
|
|
149
|
-
- Do not add dependencies or edit `package.json` unless the task says so explicitly.
|
|
150
|
-
- Do not commit, push, or run any git write command unless asked.
|
|
151
|
-
- Put throwaway probes in `.scratch/` (gitignored) inside this package, never in `/tmp`.
|
|
152
|
-
- Report: file paths, the vitest and tsc results, any literal used for want of a token, any behaviour
|
|
153
|
-
delta from v1, and anything you were unsure about.
|