@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.
Files changed (169) hide show
  1. package/ADOPTING.md +416 -0
  2. package/README.md +32 -31
  3. package/dist/_virtual/jsx-dev-runtime.js +5 -0
  4. package/dist/_virtual/jsx-dev-runtime.js.map +1 -0
  5. package/dist/_virtual/react-jsx-dev-runtime.development.js +5 -0
  6. package/dist/_virtual/react-jsx-dev-runtime.development.js.map +1 -0
  7. package/dist/_virtual/react-jsx-dev-runtime.production.js +5 -0
  8. package/dist/_virtual/react-jsx-dev-runtime.production.js.map +1 -0
  9. package/dist/components/accordion/accordion.js +126 -66
  10. package/dist/components/accordion/accordion.js.map +1 -1
  11. package/dist/components/alert-dialog/alert-dialog.js +239 -137
  12. package/dist/components/alert-dialog/alert-dialog.js.map +1 -1
  13. package/dist/components/avatar/avatar-group.js +102 -47
  14. package/dist/components/avatar/avatar-group.js.map +1 -1
  15. package/dist/components/avatar/avatar.js +97 -73
  16. package/dist/components/avatar/avatar.js.map +1 -1
  17. package/dist/components/badge/badge.js +53 -37
  18. package/dist/components/badge/badge.js.map +1 -1
  19. package/dist/components/breadcrumb/breadcrumb.js +97 -49
  20. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  21. package/dist/components/button/button.js +88 -77
  22. package/dist/components/button/button.js.map +1 -1
  23. package/dist/components/button-group/button-group.js +55 -31
  24. package/dist/components/button-group/button-group.js.map +1 -1
  25. package/dist/components/callout/callout.js +288 -164
  26. package/dist/components/callout/callout.js.map +1 -1
  27. package/dist/components/card/card.js +137 -81
  28. package/dist/components/card/card.js.map +1 -1
  29. package/dist/components/checkbox/checkbox.js +100 -49
  30. package/dist/components/checkbox/checkbox.js.map +1 -1
  31. package/dist/components/collapsible-sidebar/collapsible-sidebar.js +155 -107
  32. package/dist/components/collapsible-sidebar/collapsible-sidebar.js.map +1 -1
  33. package/dist/components/command/command.js +297 -158
  34. package/dist/components/command/command.js.map +1 -1
  35. package/dist/components/data-table/data-table-body.js +114 -54
  36. package/dist/components/data-table/data-table-body.js.map +1 -1
  37. package/dist/components/data-table/data-table-column-header.js +117 -45
  38. package/dist/components/data-table/data-table-column-header.js.map +1 -1
  39. package/dist/components/data-table/data-table-column-visibility.js +98 -39
  40. package/dist/components/data-table/data-table-column-visibility.js.map +1 -1
  41. package/dist/components/data-table/data-table-content.js +90 -58
  42. package/dist/components/data-table/data-table-content.js.map +1 -1
  43. package/dist/components/data-table/data-table-error.js +83 -39
  44. package/dist/components/data-table/data-table-error.js.map +1 -1
  45. package/dist/components/data-table/data-table-header.js +88 -54
  46. package/dist/components/data-table/data-table-header.js.map +1 -1
  47. package/dist/components/data-table/data-table-loading.js +27 -15
  48. package/dist/components/data-table/data-table-loading.js.map +1 -1
  49. package/dist/components/data-table/data-table-pagination.js +27 -19
  50. package/dist/components/data-table/data-table-pagination.js.map +1 -1
  51. package/dist/components/data-table/data-table-row.js +87 -64
  52. package/dist/components/data-table/data-table-row.js.map +1 -1
  53. package/dist/components/data-table/data-table-toolbar.js +24 -16
  54. package/dist/components/data-table/data-table-toolbar.js.map +1 -1
  55. package/dist/components/data-table/data-table.js +182 -145
  56. package/dist/components/data-table/data-table.js.map +1 -1
  57. package/dist/components/date-range-picker/date-range-picker.js +342 -214
  58. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  59. package/dist/components/date-range-picker/range-calendar.js +132 -92
  60. package/dist/components/date-range-picker/range-calendar.js.map +1 -1
  61. package/dist/components/date-time-picker/calendar.js +501 -317
  62. package/dist/components/date-time-picker/calendar.js.map +1 -1
  63. package/dist/components/date-time-picker/date-time-picker-context.js +64 -60
  64. package/dist/components/date-time-picker/date-time-picker-context.js.map +1 -1
  65. package/dist/components/date-time-picker/date-time-picker-input.js +249 -149
  66. package/dist/components/date-time-picker/date-time-picker-input.js.map +1 -1
  67. package/dist/components/date-time-picker/date-time-picker-panel.js +273 -165
  68. package/dist/components/date-time-picker/date-time-picker-panel.js.map +1 -1
  69. package/dist/components/date-time-picker/date-time-picker-popover.js +22 -14
  70. package/dist/components/date-time-picker/date-time-picker-popover.js.map +1 -1
  71. package/dist/components/date-time-picker/date-time-picker.js +134 -90
  72. package/dist/components/date-time-picker/date-time-picker.js.map +1 -1
  73. package/dist/components/date-time-picker/time-picker.js +198 -132
  74. package/dist/components/date-time-picker/time-picker.js.map +1 -1
  75. package/dist/components/density-provider/density-provider.js +48 -0
  76. package/dist/components/density-provider/density-provider.js.map +1 -0
  77. package/dist/components/dialog/dialog.js +280 -150
  78. package/dist/components/dialog/dialog.js.map +1 -1
  79. package/dist/components/drawer/drawer.js +273 -163
  80. package/dist/components/drawer/drawer.js.map +1 -1
  81. package/dist/components/dropdown-menu/dropdown-menu.js +378 -195
  82. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
  83. package/dist/components/field/field-combobox.js +447 -240
  84. package/dist/components/field/field-combobox.js.map +1 -1
  85. package/dist/components/field/field-select.js +23 -19
  86. package/dist/components/field/field-select.js.map +1 -1
  87. package/dist/components/field/field.js +318 -209
  88. package/dist/components/field/field.js.map +1 -1
  89. package/dist/components/label/label.js +27 -19
  90. package/dist/components/label/label.js.map +1 -1
  91. package/dist/components/list/list.js +623 -369
  92. package/dist/components/list/list.js.map +1 -1
  93. package/dist/components/option-card/option-card.js +323 -159
  94. package/dist/components/option-card/option-card.js.map +1 -1
  95. package/dist/components/pill/pill.js +82 -64
  96. package/dist/components/pill/pill.js.map +1 -1
  97. package/dist/components/popover/popover.js +101 -56
  98. package/dist/components/popover/popover.js.map +1 -1
  99. package/dist/components/radio/radio.js +94 -51
  100. package/dist/components/radio/radio.js.map +1 -1
  101. package/dist/components/scroll-overflow/scroll-overflow.js +350 -198
  102. package/dist/components/scroll-overflow/scroll-overflow.js.map +1 -1
  103. package/dist/components/segmented-control/segmented-control.js +141 -97
  104. package/dist/components/segmented-control/segmented-control.js.map +1 -1
  105. package/dist/components/select/select.js +452 -227
  106. package/dist/components/select/select.js.map +1 -1
  107. package/dist/components/separator/separator.js +24 -16
  108. package/dist/components/separator/separator.js.map +1 -1
  109. package/dist/components/skeleton/skeleton.js +83 -43
  110. package/dist/components/skeleton/skeleton.js.map +1 -1
  111. package/dist/components/spinner/spinner.js +44 -20
  112. package/dist/components/spinner/spinner.js.map +1 -1
  113. package/dist/components/switch/switch.js +163 -86
  114. package/dist/components/switch/switch.js.map +1 -1
  115. package/dist/components/table/table-pagination.js +465 -205
  116. package/dist/components/table/table-pagination.js.map +1 -1
  117. package/dist/components/table/table.js +212 -144
  118. package/dist/components/table/table.js.map +1 -1
  119. package/dist/components/tabs/tabs.js +94 -58
  120. package/dist/components/tabs/tabs.js.map +1 -1
  121. package/dist/components/text/text.js +40 -32
  122. package/dist/components/text/text.js.map +1 -1
  123. package/dist/components/toast/toast.js +187 -111
  124. package/dist/components/toast/toast.js.map +1 -1
  125. package/dist/components/toggle/toggle-group.js +40 -24
  126. package/dist/components/toggle/toggle-group.js.map +1 -1
  127. package/dist/components/toggle/toggle.js +48 -40
  128. package/dist/components/toggle/toggle.js.map +1 -1
  129. package/dist/components/tooltip/tooltip.js +101 -55
  130. package/dist/components/tooltip/tooltip.js.map +1 -1
  131. package/dist/index.d.ts +600 -427
  132. package/dist/index.js +215 -212
  133. package/dist/index.js.map +1 -1
  134. package/dist/lib/clear-button.js +27 -15
  135. package/dist/lib/clear-button.js.map +1 -1
  136. package/dist/lib/icons.js +18 -6
  137. package/dist/lib/icons.js.map +1 -1
  138. package/dist/packages/volt-runtime/dist/sheet.js +27 -10
  139. package/dist/packages/volt-runtime/dist/sheet.js.map +1 -1
  140. package/dist/packages/volt-runtime/dist/styles/recipe.js +76 -0
  141. package/dist/packages/volt-runtime/dist/styles/recipe.js.map +1 -0
  142. package/dist/styles/document-context.js +12 -0
  143. package/dist/styles/document-context.js.map +1 -0
  144. package/dist/styles/make-styles.js +18 -10
  145. package/dist/styles/make-styles.js.map +1 -1
  146. package/dist/tokens/core.generated.js +1238 -1166
  147. package/dist/tokens/core.generated.js.map +1 -1
  148. package/dist/tokens/index.d.ts +442 -415
  149. package/dist/tokens/index.js +1105 -1078
  150. package/dist/tokens/index.js.map +1 -1
  151. package/dist/tokens/install.js +9 -5
  152. package/dist/tokens/install.js.map +1 -1
  153. package/dist/tokens/use-palette-tokens.js +20 -0
  154. package/dist/tokens/use-palette-tokens.js.map +1 -0
  155. package/dist/tokens.json +83 -2
  156. package/dist/vendor/react/cjs/react-jsx-dev-runtime.development.js +236 -0
  157. package/dist/vendor/react/cjs/react-jsx-dev-runtime.development.js.map +1 -0
  158. package/dist/vendor/react/cjs/react-jsx-dev-runtime.production.js +21 -0
  159. package/dist/vendor/react/cjs/react-jsx-dev-runtime.production.js.map +1 -0
  160. package/dist/vendor/react/jsx-dev-runtime.js +9 -0
  161. package/dist/vendor/react/jsx-dev-runtime.js.map +1 -0
  162. package/package.json +13 -5
  163. package/scripts/migration-diagnostics-core.mjs +859 -0
  164. package/scripts/migration-diagnostics.mjs +353 -0
  165. package/scripts/migration-sweep.mjs +179 -0
  166. package/AGENTS.md +0 -153
  167. package/AUTHORING.md +0 -292
  168. package/dist/packages/volt-runtime/dist/styles/make-styles.js +0 -22
  169. 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.