@viglet/viglet-design-system 2026.3.9 → 2026.3.11

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 (259) hide show
  1. package/README.md +528 -44
  2. package/consumers.json +186 -0
  3. package/dist/badge-locale-CJkHDlPg.cjs +1 -0
  4. package/dist/badge-locale-DxDp8Q3S.js +397 -0
  5. package/dist/bento/bento-actions-menu.d.ts +8 -0
  6. package/dist/bento/bento-actions-menu.d.ts.map +1 -1
  7. package/dist/bento/bento-back-to-top.d.ts +9 -1
  8. package/dist/bento/bento-back-to-top.d.ts.map +1 -1
  9. package/dist/bento/bento-command-palette.d.ts +56 -9
  10. package/dist/bento/bento-command-palette.d.ts.map +1 -1
  11. package/dist/bento/bento-data-table.d.ts +81 -0
  12. package/dist/bento/bento-data-table.d.ts.map +1 -0
  13. package/dist/bento/bento-diff.d.ts +41 -0
  14. package/dist/bento/bento-diff.d.ts.map +1 -0
  15. package/dist/bento/bento-empty-state.d.ts +8 -1
  16. package/dist/bento/bento-empty-state.d.ts.map +1 -1
  17. package/dist/bento/bento-entity-shell.d.ts.map +1 -1
  18. package/dist/bento/bento-filter-bar.d.ts +60 -0
  19. package/dist/bento/bento-filter-bar.d.ts.map +1 -0
  20. package/dist/bento/bento-form-section.d.ts +14 -1
  21. package/dist/bento/bento-form-section.d.ts.map +1 -1
  22. package/dist/bento/bento-inline-edit.d.ts.map +1 -1
  23. package/dist/bento/bento-nav-rail.d.ts +13 -13
  24. package/dist/bento/bento-nav-rail.d.ts.map +1 -1
  25. package/dist/bento/bento-nav.d.ts +10 -2
  26. package/dist/bento/bento-nav.d.ts.map +1 -1
  27. package/dist/bento/bento-palette-trigger.d.ts +18 -0
  28. package/dist/bento/bento-palette-trigger.d.ts.map +1 -0
  29. package/dist/bento/bento-section-chrome.d.ts +19 -39
  30. package/dist/bento/bento-section-chrome.d.ts.map +1 -1
  31. package/dist/bento/bento-shell-shortcuts.d.ts +67 -0
  32. package/dist/bento/bento-shell-shortcuts.d.ts.map +1 -0
  33. package/dist/bento/bento-shell.d.ts +53 -0
  34. package/dist/bento/bento-shell.d.ts.map +1 -0
  35. package/dist/bento/bento-shortcuts-dialog.d.ts +9 -7
  36. package/dist/bento/bento-shortcuts-dialog.d.ts.map +1 -1
  37. package/dist/bento/bento-status-marker.d.ts +5 -2
  38. package/dist/bento/bento-status-marker.d.ts.map +1 -1
  39. package/dist/bento/bento-tile.d.ts.map +1 -1
  40. package/dist/bento/bento-user-menu.d.ts +9 -8
  41. package/dist/bento/bento-user-menu.d.ts.map +1 -1
  42. package/dist/bento/bento-version-rail.d.ts +31 -0
  43. package/dist/bento/bento-version-rail.d.ts.map +1 -0
  44. package/dist/bento/index.d.ts +15 -6
  45. package/dist/bento/index.d.ts.map +1 -1
  46. package/dist/bento.cjs +5 -1
  47. package/dist/bento.css +70 -4
  48. package/dist/bento.es.js +1778 -701
  49. package/dist/catalogue.json +5283 -0
  50. package/dist/components/app-footer.d.ts +7 -0
  51. package/dist/components/app-footer.d.ts.map +1 -1
  52. package/dist/components/app-switcher.d.ts.map +1 -1
  53. package/dist/components/backend-status.d.ts +19 -11
  54. package/dist/components/backend-status.d.ts.map +1 -1
  55. package/dist/components/badge-colorful.d.ts +13 -0
  56. package/dist/components/badge-colorful.d.ts.map +1 -1
  57. package/dist/components/badge-locale.d.ts +7 -0
  58. package/dist/components/badge-locale.d.ts.map +1 -1
  59. package/dist/components/error-boundary.d.ts +15 -13
  60. package/dist/components/error-boundary.d.ts.map +1 -1
  61. package/dist/components/language-select.d.ts +8 -0
  62. package/dist/components/language-select.d.ts.map +1 -1
  63. package/dist/components/login/login.d.ts +27 -0
  64. package/dist/components/login/login.d.ts.map +1 -1
  65. package/dist/components/mode-toggle.d.ts +9 -0
  66. package/dist/components/mode-toggle.d.ts.map +1 -1
  67. package/dist/components/nav-secondary.d.ts +7 -0
  68. package/dist/components/nav-secondary.d.ts.map +1 -1
  69. package/dist/components/router/dialog.delete.d.ts +8 -0
  70. package/dist/components/router/dialog.delete.d.ts.map +1 -1
  71. package/dist/components/router/gradient-button-link.d.ts +6 -0
  72. package/dist/components/router/gradient-button-link.d.ts.map +1 -1
  73. package/dist/components/router/index.d.ts +0 -11
  74. package/dist/components/router/index.d.ts.map +1 -1
  75. package/dist/components/router/loading-provider.d.ts +4 -0
  76. package/dist/components/router/loading-provider.d.ts.map +1 -1
  77. package/dist/components/startup-first/startup-first.d.ts +29 -0
  78. package/dist/components/startup-first/startup-first.d.ts.map +1 -1
  79. package/dist/components/ui/accordion.d.ts +17 -0
  80. package/dist/components/ui/accordion.d.ts.map +1 -1
  81. package/dist/components/ui/avatar.d.ts +14 -0
  82. package/dist/components/ui/avatar.d.ts.map +1 -1
  83. package/dist/components/ui/badge.d.ts +6 -0
  84. package/dist/components/ui/badge.d.ts.map +1 -1
  85. package/dist/components/ui/breadcrumb.d.ts +25 -0
  86. package/dist/components/ui/breadcrumb.d.ts.map +1 -1
  87. package/dist/components/ui/button.d.ts +15 -1
  88. package/dist/components/ui/button.d.ts.map +1 -1
  89. package/dist/components/ui/card.d.ts +27 -0
  90. package/dist/components/ui/card.d.ts.map +1 -1
  91. package/dist/components/ui/checkbox.d.ts +4 -0
  92. package/dist/components/ui/checkbox.d.ts.map +1 -1
  93. package/dist/components/ui/dialog.d.ts +36 -0
  94. package/dist/components/ui/dialog.d.ts.map +1 -1
  95. package/dist/components/ui/drawer.d.ts +35 -0
  96. package/dist/components/ui/drawer.d.ts.map +1 -1
  97. package/dist/components/ui/dropdown-menu.d.ts +53 -0
  98. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  99. package/dist/components/ui/floating-formulas-bg.d.ts +8 -0
  100. package/dist/components/ui/floating-formulas-bg.d.ts.map +1 -1
  101. package/dist/components/ui/form-actions.d.ts +7 -0
  102. package/dist/components/ui/form-actions.d.ts.map +1 -1
  103. package/dist/components/ui/form-item-two-columns.d.ts +7 -0
  104. package/dist/components/ui/form-item-two-columns.d.ts.map +1 -1
  105. package/dist/components/ui/form.d.ts +25 -0
  106. package/dist/components/ui/form.d.ts.map +1 -1
  107. package/dist/components/ui/glass-card.d.ts +18 -14
  108. package/dist/components/ui/glass-card.d.ts.map +1 -1
  109. package/dist/components/ui/gradient-button.d.ts +15 -1
  110. package/dist/components/ui/gradient-button.d.ts.map +1 -1
  111. package/dist/components/ui/gradient-switch.d.ts +6 -0
  112. package/dist/components/ui/gradient-switch.d.ts.map +1 -1
  113. package/dist/components/ui/hover-card.d.ts +14 -0
  114. package/dist/components/ui/hover-card.d.ts.map +1 -1
  115. package/dist/components/ui/icon-picker-dialog.d.ts.map +1 -1
  116. package/dist/components/ui/index.d.ts +6 -4
  117. package/dist/components/ui/index.d.ts.map +1 -1
  118. package/dist/components/ui/input.d.ts +6 -0
  119. package/dist/components/ui/input.d.ts.map +1 -1
  120. package/dist/components/ui/label.d.ts +4 -0
  121. package/dist/components/ui/label.d.ts.map +1 -1
  122. package/dist/components/ui/navigation-menu.d.ts +41 -0
  123. package/dist/components/ui/navigation-menu.d.ts.map +1 -1
  124. package/dist/components/ui/pagination.d.ts +25 -0
  125. package/dist/components/ui/pagination.d.ts.map +1 -1
  126. package/dist/components/ui/popover.d.ts +24 -0
  127. package/dist/components/ui/popover.d.ts.map +1 -1
  128. package/dist/components/ui/progress.d.ts +4 -0
  129. package/dist/components/ui/progress.d.ts.map +1 -1
  130. package/dist/components/ui/pulse-ring.d.ts +9 -11
  131. package/dist/components/ui/pulse-ring.d.ts.map +1 -1
  132. package/dist/components/ui/resizable.d.ts +11 -0
  133. package/dist/components/ui/resizable.d.ts.map +1 -1
  134. package/dist/components/ui/section-card.d.ts +9 -0
  135. package/dist/components/ui/section-card.d.ts.map +1 -1
  136. package/dist/components/ui/select.d.ts +36 -0
  137. package/dist/components/ui/select.d.ts.map +1 -1
  138. package/dist/components/ui/separator.d.ts +4 -0
  139. package/dist/components/ui/separator.d.ts.map +1 -1
  140. package/dist/components/ui/sheet.d.ts +29 -0
  141. package/dist/components/ui/sheet.d.ts.map +1 -1
  142. package/dist/components/ui/sidebar.d.ts +83 -0
  143. package/dist/components/ui/sidebar.d.ts.map +1 -1
  144. package/dist/components/ui/skeleton.d.ts +4 -0
  145. package/dist/components/ui/skeleton.d.ts.map +1 -1
  146. package/dist/components/ui/slider.d.ts +4 -0
  147. package/dist/components/ui/slider.d.ts.map +1 -1
  148. package/dist/components/ui/sonner.d.ts +16 -0
  149. package/dist/components/ui/sonner.d.ts.map +1 -1
  150. package/dist/components/ui/stepper.d.ts +8 -0
  151. package/dist/components/ui/stepper.d.ts.map +1 -1
  152. package/dist/components/ui/sticky-save-bar.d.ts +7 -0
  153. package/dist/components/ui/sticky-save-bar.d.ts.map +1 -1
  154. package/dist/components/ui/switch.d.ts +4 -0
  155. package/dist/components/ui/switch.d.ts.map +1 -1
  156. package/dist/components/ui/table.d.ts +31 -0
  157. package/dist/components/ui/table.d.ts.map +1 -1
  158. package/dist/components/ui/tabs.d.ts +16 -0
  159. package/dist/components/ui/tabs.d.ts.map +1 -1
  160. package/dist/components/ui/textarea.d.ts +4 -0
  161. package/dist/components/ui/textarea.d.ts.map +1 -1
  162. package/dist/components/ui/toggle-group.d.ts +7 -0
  163. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  164. package/dist/components/ui/toggle.d.ts +4 -0
  165. package/dist/components/ui/toggle.d.ts.map +1 -1
  166. package/dist/components/ui/tooltip.d.ts +18 -0
  167. package/dist/components/ui/tooltip.d.ts.map +1 -1
  168. package/dist/components/ui/user-avatar.d.ts +2 -1
  169. package/dist/components/ui/user-avatar.d.ts.map +1 -1
  170. package/dist/components/ui/viglet-assistant.d.ts +103 -0
  171. package/dist/components/ui/viglet-assistant.d.ts.map +1 -0
  172. package/dist/components/ui/viglet-avatar.d.ts +44 -0
  173. package/dist/components/ui/viglet-avatar.d.ts.map +1 -0
  174. package/dist/contexts/breadcrumb.context.d.ts +7 -0
  175. package/dist/contexts/breadcrumb.context.d.ts.map +1 -1
  176. package/dist/contexts/user.context.d.ts +8 -0
  177. package/dist/contexts/user.context.d.ts.map +1 -1
  178. package/dist/exports.json +359 -30
  179. package/dist/floating-formulas-bg.cjs +1 -1
  180. package/dist/floating-formulas-bg.es.js +56 -55
  181. package/dist/hooks/index.d.ts +1 -0
  182. package/dist/hooks/index.d.ts.map +1 -1
  183. package/dist/hooks/use-assistant-notifications.d.ts +15 -0
  184. package/dist/hooks/use-assistant-notifications.d.ts.map +1 -0
  185. package/dist/i18n/index.d.ts +168 -0
  186. package/dist/i18n/index.d.ts.map +1 -1
  187. package/dist/i18n/locales/en/assistant.json.d.ts +25 -0
  188. package/dist/i18n/locales/en/bento.json.d.ts +48 -2
  189. package/dist/i18n/locales/en/common.json.d.ts +6 -1
  190. package/dist/i18n/locales/en/language.json.d.ts +8 -0
  191. package/dist/i18n/locales/pt/assistant.json.d.ts +25 -0
  192. package/dist/i18n/locales/pt/bento.json.d.ts +48 -2
  193. package/dist/i18n/locales/pt/common.json.d.ts +6 -1
  194. package/dist/i18n/locales/pt/language.json.d.ts +8 -0
  195. package/dist/i18n.cjs +1 -1
  196. package/dist/i18n.es.js +572 -2
  197. package/dist/index.cjs +1 -1
  198. package/dist/index.d.ts +0 -1
  199. package/dist/index.d.ts.map +1 -1
  200. package/dist/index.es.js +2280 -733
  201. package/dist/lib/busy.d.ts +26 -0
  202. package/dist/lib/busy.d.ts.map +1 -0
  203. package/dist/lib/corner-slot.d.ts +15 -0
  204. package/dist/lib/corner-slot.d.ts.map +1 -0
  205. package/dist/{loading-provider-BWWhhfEn.js → loading-provider-ClDkFPuv.js} +84 -81
  206. package/dist/loading-provider-DGuCfsl9.cjs +1 -0
  207. package/dist/preset.css +7 -10
  208. package/dist/router.cjs +1 -1
  209. package/dist/router.es.js +2 -776
  210. package/dist/user.context-Cr3GAQ_r.js +762 -0
  211. package/dist/user.context-hbdvmXOn.cjs +1 -0
  212. package/dist/utils-Fk5jmzzb.js +41 -0
  213. package/dist/utils-Oagwe_oa.cjs +1 -0
  214. package/dist/viglet-design-system.css +1 -1
  215. package/dist/vite/boot-loader.d.ts +33 -2
  216. package/dist/vite/boot-loader.d.ts.map +1 -1
  217. package/dist/vite.cjs +16 -5
  218. package/dist/vite.es.js +27 -14
  219. package/docs/BENTO-AUTHORING.md +81 -11
  220. package/docs/reference/grounds.dc.html +9 -9
  221. package/package.json +16 -4
  222. package/scripts/consumer-entries.mjs +209 -0
  223. package/scripts/lib/contrast.mjs +301 -0
  224. package/scripts/mcp.mjs +300 -0
  225. package/scripts/page-lint.mjs +428 -0
  226. package/scripts/page-reference.mjs +54 -10
  227. package/dist/badge-locale-CoEEv7s5.js +0 -581
  228. package/dist/badge-locale-hQyWhAKe.cjs +0 -7
  229. package/dist/breadcrumb.context-KQut7j8U.js +0 -917
  230. package/dist/breadcrumb.context-Vwc3lAlf.cjs +0 -1
  231. package/dist/components/router/blank-slate.d.ts +0 -21
  232. package/dist/components/router/blank-slate.d.ts.map +0 -1
  233. package/dist/components/router/grid.list.d.ts +0 -39
  234. package/dist/components/router/grid.list.d.ts.map +0 -1
  235. package/dist/components/router/internal.sidebar.d.ts +0 -61
  236. package/dist/components/router/internal.sidebar.d.ts.map +0 -1
  237. package/dist/components/router/nav-main.d.ts +0 -25
  238. package/dist/components/router/nav-main.d.ts.map +0 -1
  239. package/dist/components/router/nav-user.d.ts +0 -24
  240. package/dist/components/router/nav-user.d.ts.map +0 -1
  241. package/dist/components/router/page-content.d.ts +0 -12
  242. package/dist/components/router/page-content.d.ts.map +0 -1
  243. package/dist/components/router/page-header.d.ts +0 -23
  244. package/dist/components/router/page-header.d.ts.map +0 -1
  245. package/dist/components/router/page.d.ts +0 -22
  246. package/dist/components/router/page.d.ts.map +0 -1
  247. package/dist/components/router/sticky.page.header.d.ts +0 -85
  248. package/dist/components/router/sticky.page.header.d.ts.map +0 -1
  249. package/dist/components/router/sub.page.d.ts +0 -50
  250. package/dist/components/router/sub.page.d.ts.map +0 -1
  251. package/dist/components/router/sub.page.header.d.ts +0 -37
  252. package/dist/components/router/sub.page.header.d.ts.map +0 -1
  253. package/dist/i18n-DD3rQOWi.js +0 -426
  254. package/dist/i18n-DrBq-qJJ.cjs +0 -1
  255. package/dist/lib/react-markers.d.ts +0 -17
  256. package/dist/lib/react-markers.d.ts.map +0 -1
  257. package/dist/loading-provider-CmeMqLGk.cjs +0 -1
  258. package/dist/user.context-B9vIBzY5.cjs +0 -1
  259. package/dist/user.context-D1PxP4D4.js +0 -512
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ // VDS129 — measure which entries a consumer imports and hold that against what
3
+ // consumers.json declares for it, in both directions.
4
+ //
5
+ // Run it from a product checkout, in that product's own test or CI step:
6
+ //
7
+ // viglet-ds-consumer-entries # match this package.json's name
8
+ // viglet-ds-consumer-entries --consumer shio # name the register entry
9
+ // viglet-ds-consumer-entries --root <dir> # measure a checkout elsewhere
10
+ // viglet-ds-consumer-entries --json # machine-readable result
11
+ // viglet-ds-consumer-entries --register <path> # read a register other than the installed one
12
+ //
13
+ // The register used to be checked in one direction only: every entry it declared
14
+ // had to be a real subpath. Nothing asked the converse, so Shio's entry listed
15
+ // three of the six subpaths its source takes and kept a chrome it had left two
16
+ // blocks earlier, and every guard reading the register read that. Nothing could
17
+ // ask from the product's side either, because the register was not published.
18
+ //
19
+ // Where to look is declared, not guessed: each consumer names its `sourceRoots`,
20
+ // directories or single files relative to its package root. A test or a story
21
+ // inside them is not the product taking a subpath — a census test that greps for
22
+ // `./bento` would otherwise record the product importing what it only counts —
23
+ // so those files are skipped, and so is anything inside a comment.
24
+
25
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
26
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
27
+ import { fileURLToPath } from "node:url"
28
+
29
+ const PACKAGE_NAME = "@viglet/viglet-design-system"
30
+ const EXTENSIONS = new Set([
31
+ ".ts",
32
+ ".tsx",
33
+ ".mts",
34
+ ".cts",
35
+ ".js",
36
+ ".jsx",
37
+ ".mjs",
38
+ ".cjs",
39
+ ".mdx",
40
+ ".css",
41
+ ".scss",
42
+ ])
43
+ const SKIP_DIRS = new Set([
44
+ "node_modules",
45
+ "dist",
46
+ "build",
47
+ "out",
48
+ "coverage",
49
+ "storybook-static",
50
+ "__tests__",
51
+ "test",
52
+ "tests",
53
+ ])
54
+ const NOT_PRODUCT = /\.(test|spec|stories)\.[^.]+$/
55
+
56
+ const args = process.argv.slice(2)
57
+ const asJson = args.includes("--json")
58
+
59
+ /** The value after `--flag`, or null when the flag is absent. */
60
+ function option(flag) {
61
+ const at = args.indexOf(flag)
62
+ return at === -1 ? null : (args[at + 1] ?? null)
63
+ }
64
+
65
+ function fail(message) {
66
+ console.error(`consumer-entries: ${message}`)
67
+ process.exit(1)
68
+ }
69
+
70
+ const root = resolve(option("--root") ?? process.cwd())
71
+ const registerPath = resolve(
72
+ option("--register") ?? join(dirname(fileURLToPath(import.meta.url)), "..", "consumers.json"),
73
+ )
74
+
75
+ if (!existsSync(registerPath)) fail(`no register at ${registerPath}`)
76
+ const register = JSON.parse(readFileSync(registerPath, "utf8"))
77
+
78
+ function findConsumer() {
79
+ const named = option("--consumer")
80
+ if (named) {
81
+ const byId = register.consumers.find((c) => c.id === named)
82
+ if (!byId) {
83
+ fail(`no consumer "${named}" in ${registerPath}; it declares ${register.consumers.map((c) => c.id).join(", ")}`)
84
+ }
85
+ return byId
86
+ }
87
+ const manifest = join(root, "package.json")
88
+ if (!existsSync(manifest)) fail(`no package.json in ${root}; pass --consumer <id>`)
89
+ const name = JSON.parse(readFileSync(manifest, "utf8")).name
90
+ const byPackage = register.consumers.find((c) => c.package === name)
91
+ if (!byPackage) {
92
+ fail(
93
+ `${name} is not a consumer in ${registerPath}. Declare it there, or pass --consumer <id> ` +
94
+ `(one of ${register.consumers.map((c) => c.id).join(", ")})`,
95
+ )
96
+ }
97
+ return byPackage
98
+ }
99
+
100
+ const consumer = findConsumer()
101
+ const sourceRoots = consumer.sourceRoots ?? []
102
+ if (sourceRoots.length === 0) {
103
+ fail(`${consumer.id} declares no sourceRoots, so there is nothing to measure its entries against`)
104
+ }
105
+
106
+ function collect(path, files = []) {
107
+ const stat = statSync(path)
108
+ if (stat.isFile()) {
109
+ files.push(path)
110
+ return files
111
+ }
112
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
113
+ const full = join(path, entry.name)
114
+ if (entry.isDirectory()) {
115
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) collect(full, files)
116
+ } else if (EXTENSIONS.has(entry.name.slice(entry.name.lastIndexOf("."))) && !NOT_PRODUCT.test(entry.name)) {
117
+ files.push(full)
118
+ }
119
+ }
120
+ return files
121
+ }
122
+
123
+ const missing = sourceRoots.filter((r) => isAbsolute(r) || !existsSync(join(root, r)))
124
+ if (missing.length > 0) {
125
+ fail(`${consumer.id} declares sourceRoots that are not under ${root}: ${missing.join(", ")}`)
126
+ }
127
+
128
+ // Blanked rather than removed, so an import's line number survives the strip.
129
+ // Only a comment that opens its line: a glob such as "src/**" in a config file
130
+ // opens a block anywhere else and would swallow the imports after it.
131
+ const blank = (text) => text.replace(/[^\n]/g, " ")
132
+ const withoutComments = (source) =>
133
+ source.replace(/^[ \t]*\/\*[\s\S]*?\*\//gm, blank).replace(/^[ \t]*\/\/.*$/gm, blank)
134
+
135
+ // `from "x"`, `import "x"`, `import("x")`, `require("x")` and CSS `@import "x"` or
136
+ // `@import url("x")`. The specifier is the package name alone or a subpath of it.
137
+ const SPECIFIER = new RegExp(
138
+ String.raw`(?:\bfrom|\bimport|\brequire|@import)\s*(?:\(\s*|url\(\s*)?["'](${PACKAGE_NAME}(?:/[^"'\s]*)?)["']`,
139
+ "g",
140
+ )
141
+
142
+ /** `@viglet/viglet-design-system/bento` -> `./bento`; the bare name -> `.`. */
143
+ const entryOf = (specifier) =>
144
+ specifier === PACKAGE_NAME ? "." : `.${specifier.slice(PACKAGE_NAME.length)}`
145
+
146
+ const imported = new Map()
147
+ let scanned = 0
148
+ for (const sourceRoot of sourceRoots) {
149
+ for (const file of collect(join(root, sourceRoot))) {
150
+ scanned++
151
+ const source = withoutComments(readFileSync(file, "utf8"))
152
+ for (const match of source.matchAll(SPECIFIER)) {
153
+ const entry = entryOf(match[1])
154
+ if (imported.has(entry)) continue
155
+ imported.set(entry, {
156
+ file: relative(root, file).replaceAll("\\", "/"),
157
+ line: source.slice(0, match.index).split("\n").length,
158
+ })
159
+ }
160
+ }
161
+ }
162
+
163
+ if (scanned === 0) {
164
+ fail(`${consumer.id}'s sourceRoots (${sourceRoots.join(", ")}) hold no source file, so this would pass vacuously`)
165
+ }
166
+
167
+ const declared = new Set(consumer.entries)
168
+ const undeclared = [...imported.entries()]
169
+ .filter(([entry]) => !declared.has(entry))
170
+ .map(([entry, at]) => ({ entry, ...at }))
171
+ .sort((a, b) => a.entry.localeCompare(b.entry))
172
+ const unused = [...declared].filter((entry) => !imported.has(entry)).sort()
173
+
174
+ if (asJson) {
175
+ console.log(
176
+ JSON.stringify(
177
+ {
178
+ consumer: consumer.id,
179
+ scanned,
180
+ declared: [...declared].sort(),
181
+ imported: [...imported.keys()].sort(),
182
+ undeclared,
183
+ unused,
184
+ },
185
+ null,
186
+ 2,
187
+ ),
188
+ )
189
+ } else if (undeclared.length === 0 && unused.length === 0) {
190
+ console.log(
191
+ `consumer-entries: ${consumer.id} imports the ${declared.size} entries consumers.json declares for it (${scanned} file(s) scanned).`,
192
+ )
193
+ } else {
194
+ console.error(`\nconsumer-entries: ${consumer.id}'s source and its entry in consumers.json disagree\n`)
195
+ if (undeclared.length > 0) {
196
+ console.error(" imported, and not declared:")
197
+ for (const { entry, file, line } of undeclared) console.error(` ${entry} first at ${file}:${line}`)
198
+ }
199
+ if (unused.length > 0) {
200
+ console.error(" declared, and imported nowhere:")
201
+ for (const entry of unused) console.error(` ${entry}`)
202
+ }
203
+ console.error(
204
+ `\nThe register is what this package's guards read, so a consumer it misdescribes is\n` +
205
+ `checked against a product that does not exist. Correct the entry in consumers.json.\n`,
206
+ )
207
+ }
208
+
209
+ process.exit(undeclared.length > 0 || unused.length > 0 ? 1 : 0)
@@ -0,0 +1,301 @@
1
+ /**
2
+ * VDS137 — the token pairs a product has to keep legible, measured on whatever
3
+ * stylesheets it ships, not only on the preset.
4
+ *
5
+ * The contrast gate measured the preset's own pairs and nothing else. A product
6
+ * claims its accent and its `--primary` in its own stylesheet, which no gate
7
+ * read, so a re-keyed orange could sit near 3.6:1 as text with every check green.
8
+ * This is that gate's arithmetic, lifted out so the package's test and a
9
+ * consumer's `viglet-ds-page-lint --contrast` measure the same way.
10
+ *
11
+ * **The cascade, modelled.** The stylesheets are read in order, as a product
12
+ * imports them: the preset first, then the product's. A ground's tokens are every
13
+ * `:root` block, with the dark blocks laid over them for the dark ground. A
14
+ * product's own `:root` comes after the preset's dark block at the same
15
+ * specificity, so it wins on both grounds, which is exactly why setting
16
+ * `--vg-primary` there keys the dark ground to the light value, and exactly what
17
+ * this measures.
18
+ *
19
+ * **Arithmetic, not a browser.** Colours are converted to OKLab (Ottosson's
20
+ * matrices), mixed there when a token is a `color-mix(in oklab, …)`, and taken to
21
+ * linear sRGB for WCAG's relative luminance. A value it cannot read is reported
22
+ * as unread, never measured as black.
23
+ */
24
+
25
+ /** WCAG 2 AA for body text. */
26
+ export const AA_TEXT = 4.5
27
+
28
+ const DARK_SELECTOR = /(^|[\s,])(\.dark|\[data-theme=["']?dark["']?\]|:root\.dark|html\.dark)(?=$|[\s,{:])/
29
+ const ROOT_SELECTOR = /^(:root|html)$/
30
+
31
+ /**
32
+ * Every innermost rule block in a stylesheet: its selector list, the at-rules
33
+ * around it, and its custom-property declarations, in source order.
34
+ */
35
+ export function ruleBlocks(css) {
36
+ const text = css.replace(/\/\*[\s\S]*?\*\//g, "")
37
+ const blocks = []
38
+ const stack = []
39
+ let start = 0
40
+ for (let i = 0; i < text.length; i++) {
41
+ const char = text[i]
42
+ if (char === "{") {
43
+ stack.push({ prelude: text.slice(start, i).trim(), body: i + 1, nested: false })
44
+ if (stack.length > 1) stack[stack.length - 2].nested = true
45
+ start = i + 1
46
+ } else if (char === "}") {
47
+ const open = stack.pop()
48
+ if (open && !open.nested) {
49
+ const declarations = new Map()
50
+ for (const part of text.slice(open.body, i).split(";")) {
51
+ const at = part.indexOf(":")
52
+ if (at === -1) continue
53
+ const name = part.slice(0, at).trim()
54
+ if (name.startsWith("--")) declarations.set(name, part.slice(at + 1).trim())
55
+ }
56
+ blocks.push({
57
+ selectors: open.prelude.split(",").map((s) => s.trim()),
58
+ atRules: stack.map((s) => s.prelude),
59
+ declarations,
60
+ })
61
+ }
62
+ start = i + 1
63
+ } else if (char === ";" && stack.length === 0) {
64
+ start = i + 1
65
+ }
66
+ }
67
+ return blocks
68
+ }
69
+
70
+ function groundOf(block) {
71
+ if (block.atRules.some((rule) => /^@theme/.test(rule))) return null
72
+ const darkMedia = block.atRules.some((rule) => /prefers-color-scheme:\s*dark/.test(rule))
73
+ if (block.selectors.some((s) => DARK_SELECTOR.test(s))) return "dark"
74
+ if (block.selectors.some((s) => ROOT_SELECTOR.test(s))) return darkMedia ? "dark" : "light"
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * One ground's tokens across the stylesheets, in cascade order, with where each
80
+ * was last declared, so a finding can name the product's line and not the preset's.
81
+ */
82
+ export function groundTokens(sheets, ground) {
83
+ const tokens = new Map()
84
+ const origin = new Map()
85
+ for (const { name, css } of sheets) {
86
+ for (const block of ruleBlocks(css)) {
87
+ const which = groundOf(block)
88
+ if (which === "light" || (ground === "dark" && which === "dark")) {
89
+ for (const [token, value] of block.declarations) {
90
+ tokens.set(token, value)
91
+ origin.set(token, name)
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return { tokens, origin }
97
+ }
98
+
99
+ /** Follow `var(--x)` and `var(--x, fallback)` to the value it names. */
100
+ export function resolveToken(tokens, value, depth = 8) {
101
+ let current = value.trim()
102
+ for (let hop = 0; hop < depth; hop++) {
103
+ const points = /^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/.exec(current)
104
+ if (!points) return current
105
+ const next = tokens.get(points[1])
106
+ if (next === undefined) {
107
+ if (points[2] === undefined) return current
108
+ current = points[2].trim()
109
+ } else {
110
+ current = next.trim()
111
+ }
112
+ }
113
+ return current
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Colour.
118
+
119
+ const srgbToLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)
120
+
121
+ function linearToOklab([r, g, b]) {
122
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
123
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
124
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
125
+ return {
126
+ L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
127
+ a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
128
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
129
+ }
130
+ }
131
+
132
+ function oklabToLinear({ L, a, b }) {
133
+ const long = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
134
+ const medium = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
135
+ const short = (L - 0.0894841775 * a - 1.291485548 * b) ** 3
136
+ const clamp = (x) => Math.min(1, Math.max(0, x))
137
+ return [
138
+ clamp(4.0767416621 * long - 3.3077115913 * medium + 0.2309699292 * short),
139
+ clamp(-1.2684380046 * long + 2.6097574011 * medium - 0.3413193965 * short),
140
+ clamp(-0.0041960863 * long - 0.7034186147 * medium + 1.707614701 * short),
141
+ ]
142
+ }
143
+
144
+ function hslToSrgb(h, s, l) {
145
+ const k = (n) => (n + h / 30) % 12
146
+ const f = (n) => l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))
147
+ return [f(0), f(8), f(4)]
148
+ }
149
+
150
+ const number = (text, scale = 1) => (text.endsWith("%") ? Number(text.slice(0, -1)) / 100 : Number(text) / scale)
151
+
152
+ /** A colour literal as OKLab with alpha, or null where it is not one this reads. */
153
+ export function parseColour(value, tokens = new Map()) {
154
+ const text = resolveToken(tokens, value).toLowerCase()
155
+ if (text === "white") return { L: 1, a: 0, b: 0, alpha: 1 }
156
+ if (text === "black") return { L: 0, a: 0, b: 0, alpha: 1 }
157
+ if (text === "transparent") return { L: 0, a: 0, b: 0, alpha: 0 }
158
+
159
+ const hex = /^#([0-9a-f]{3,8})$/.exec(text)
160
+ if (hex) {
161
+ let digits = hex[1]
162
+ if (digits.length <= 4) digits = [...digits].map((d) => d + d).join("")
163
+ const bytes = digits.match(/../g).map((pair) => Number.parseInt(pair, 16) / 255)
164
+ return { ...linearToOklab(bytes.slice(0, 3).map(srgbToLinear)), alpha: bytes[3] ?? 1 }
165
+ }
166
+
167
+ const fn = /^(oklch|oklab|rgba?|hsla?)\(\s*([^)]*)\)$/.exec(text)
168
+ if (fn) {
169
+ const parts = fn[2].split(/[\s,/]+/).filter(Boolean)
170
+ const alpha = parts[3] === undefined ? 1 : number(parts[3])
171
+ if (fn[1] === "oklch") {
172
+ const L = number(parts[0])
173
+ const C = Number(parts[1])
174
+ const H = (Number.parseFloat(parts[2]) * Math.PI) / 180
175
+ return { L, a: C * Math.cos(H), b: C * Math.sin(H), alpha }
176
+ }
177
+ if (fn[1] === "oklab") return { L: number(parts[0]), a: Number(parts[1]), b: Number(parts[2]), alpha }
178
+ if (fn[1].startsWith("rgb")) {
179
+ const channels = parts.slice(0, 3).map((p) => number(p, 255))
180
+ return { ...linearToOklab(channels.map(srgbToLinear)), alpha }
181
+ }
182
+ const [h, s, l] = [Number.parseFloat(parts[0]), number(parts[1]), number(parts[2])]
183
+ return { ...linearToOklab(hslToSrgb(h, s, l).map(srgbToLinear)), alpha }
184
+ }
185
+
186
+ // color-mix(in oklab, <colour> [p%], <colour> [p%]): the one space the preset mixes in.
187
+ const mix = /^color-mix\(\s*in\s+oklab\s*,\s*(.+)\)$/.exec(text)
188
+ if (mix) {
189
+ const args = splitTopLevel(mix[1])
190
+ if (args.length !== 2) return null
191
+ const [first, second] = args.map((arg) => {
192
+ const weighted = /^(.*?)\s+([\d.]+)%$/.exec(arg.trim())
193
+ return weighted ? { colour: weighted[1], weight: Number(weighted[2]) / 100 } : { colour: arg.trim(), weight: null }
194
+ })
195
+ const p1 = first.weight ?? (second.weight === null ? 0.5 : 1 - second.weight)
196
+ const a = parseColour(first.colour, tokens)
197
+ const b = parseColour(second.colour, tokens)
198
+ if (!a || !b) return null
199
+ const lerp = (x, y) => x * p1 + y * (1 - p1)
200
+ return { L: lerp(a.L, b.L), a: lerp(a.a, b.a), b: lerp(a.b, b.b), alpha: lerp(a.alpha, b.alpha) }
201
+ }
202
+ return null
203
+ }
204
+
205
+ function splitTopLevel(text) {
206
+ const out = []
207
+ let depth = 0
208
+ let from = 0
209
+ for (let i = 0; i < text.length; i++) {
210
+ if (text[i] === "(") depth++
211
+ else if (text[i] === ")") depth--
212
+ else if (text[i] === "," && depth === 0) {
213
+ out.push(text.slice(from, i))
214
+ from = i + 1
215
+ }
216
+ }
217
+ out.push(text.slice(from))
218
+ return out
219
+ }
220
+
221
+ /** WCAG relative luminance of an opaque colour; a translucent one has no single answer. */
222
+ export function luminance(colour) {
223
+ if (!colour || colour.alpha < 1) return null
224
+ const [r, g, b] = oklabToLinear(colour)
225
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
226
+ }
227
+
228
+ export function ratio(front, behind) {
229
+ return (Math.max(front, behind) + 0.05) / (Math.min(front, behind) + 0.05)
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // The pairs.
234
+
235
+ /**
236
+ * Pairs a name alone cannot derive: the page and muted text on it, the accented
237
+ * label on the page, and the white label the gradient button and the checked
238
+ * switch draw on the accent fill.
239
+ */
240
+ const NAMED_PAIRS = [
241
+ ["--vg-background", "--vg-foreground"],
242
+ ["--vg-background", "--vg-muted-foreground"],
243
+ ["--vg-background", "--vg-accent-fg"],
244
+ ["--vg-accent-fill-from", "white"],
245
+ ["--vg-accent-fill-to", "white"],
246
+ ]
247
+
248
+ /** Every `--vg-X` with a `--vg-X-foreground`, and the named pairs, where both sides are declared. */
249
+ export function pairsOf(tokens) {
250
+ const declared = (name) => name === "white" || tokens.has(name)
251
+ const derived = [...tokens.keys()]
252
+ .filter((name) => name.startsWith("--vg-") && name.endsWith("-foreground"))
253
+ .map((foreground) => [foreground.slice(0, -"-foreground".length), foreground])
254
+ .filter(([surface]) => tokens.has(surface))
255
+ const named = NAMED_PAIRS.filter(([surface, foreground]) => declared(surface) && declared(foreground))
256
+ const seen = new Set()
257
+ return [...named, ...derived].filter(([s, f]) => {
258
+ const key = `${s} ${f}`
259
+ if (seen.has(key)) return false
260
+ seen.add(key)
261
+ return true
262
+ })
263
+ }
264
+
265
+ /**
266
+ * Every pair on one ground, measured: `ratio` is null where either side does not
267
+ * resolve to an opaque colour this reads, and `unread` names which.
268
+ */
269
+ export function measurePairs(sheets, ground) {
270
+ const { tokens, origin } = groundTokens(sheets, ground)
271
+
272
+ /**
273
+ * Every stylesheet a token's value passes through on its way to a colour.
274
+ * `--vg-primary` is the preset's, and `--vg-primary-base` it points at is the
275
+ * product's: a finding has to name the second.
276
+ */
277
+ const through = (name) => {
278
+ const found = []
279
+ let current = name
280
+ for (let hop = 0; hop < 8 && current; hop++) {
281
+ if (origin.has(current)) found.push({ token: current, sheet: origin.get(current) })
282
+ const points = /var\(\s*(--[\w-]+)/.exec(tokens.get(current) ?? "")
283
+ current = points?.[1]
284
+ }
285
+ return found
286
+ }
287
+ return pairsOf(tokens).map(([surface, foreground]) => {
288
+ const behind = luminance(parseColour(tokens.get(surface) ?? "", tokens))
289
+ const front = luminance(parseColour(foreground === "white" ? "white" : (tokens.get(foreground) ?? ""), tokens))
290
+ const unread = [behind === null ? surface : null, front === null ? foreground : null].filter(Boolean)
291
+ return {
292
+ ground,
293
+ surface,
294
+ foreground,
295
+ ratio: unread.length === 0 ? ratio(front, behind) : null,
296
+ unread,
297
+ origin: [...new Set([...through(surface), ...through(foreground)].map((d) => d.sheet))],
298
+ declared: [...through(surface), ...through(foreground)],
299
+ }
300
+ })
301
+ }