@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.
- package/README.md +528 -44
- package/consumers.json +186 -0
- package/dist/badge-locale-CJkHDlPg.cjs +1 -0
- package/dist/badge-locale-DxDp8Q3S.js +397 -0
- package/dist/bento/bento-actions-menu.d.ts +8 -0
- package/dist/bento/bento-actions-menu.d.ts.map +1 -1
- package/dist/bento/bento-back-to-top.d.ts +9 -1
- package/dist/bento/bento-back-to-top.d.ts.map +1 -1
- package/dist/bento/bento-command-palette.d.ts +56 -9
- package/dist/bento/bento-command-palette.d.ts.map +1 -1
- package/dist/bento/bento-data-table.d.ts +81 -0
- package/dist/bento/bento-data-table.d.ts.map +1 -0
- package/dist/bento/bento-diff.d.ts +41 -0
- package/dist/bento/bento-diff.d.ts.map +1 -0
- package/dist/bento/bento-empty-state.d.ts +8 -1
- package/dist/bento/bento-empty-state.d.ts.map +1 -1
- package/dist/bento/bento-entity-shell.d.ts.map +1 -1
- package/dist/bento/bento-filter-bar.d.ts +60 -0
- package/dist/bento/bento-filter-bar.d.ts.map +1 -0
- package/dist/bento/bento-form-section.d.ts +14 -1
- package/dist/bento/bento-form-section.d.ts.map +1 -1
- package/dist/bento/bento-inline-edit.d.ts.map +1 -1
- package/dist/bento/bento-nav-rail.d.ts +13 -13
- package/dist/bento/bento-nav-rail.d.ts.map +1 -1
- package/dist/bento/bento-nav.d.ts +10 -2
- package/dist/bento/bento-nav.d.ts.map +1 -1
- package/dist/bento/bento-palette-trigger.d.ts +18 -0
- package/dist/bento/bento-palette-trigger.d.ts.map +1 -0
- package/dist/bento/bento-section-chrome.d.ts +19 -39
- package/dist/bento/bento-section-chrome.d.ts.map +1 -1
- package/dist/bento/bento-shell-shortcuts.d.ts +67 -0
- package/dist/bento/bento-shell-shortcuts.d.ts.map +1 -0
- package/dist/bento/bento-shell.d.ts +53 -0
- package/dist/bento/bento-shell.d.ts.map +1 -0
- package/dist/bento/bento-shortcuts-dialog.d.ts +9 -7
- package/dist/bento/bento-shortcuts-dialog.d.ts.map +1 -1
- package/dist/bento/bento-status-marker.d.ts +5 -2
- package/dist/bento/bento-status-marker.d.ts.map +1 -1
- package/dist/bento/bento-tile.d.ts.map +1 -1
- package/dist/bento/bento-user-menu.d.ts +9 -8
- package/dist/bento/bento-user-menu.d.ts.map +1 -1
- package/dist/bento/bento-version-rail.d.ts +31 -0
- package/dist/bento/bento-version-rail.d.ts.map +1 -0
- package/dist/bento/index.d.ts +15 -6
- package/dist/bento/index.d.ts.map +1 -1
- package/dist/bento.cjs +5 -1
- package/dist/bento.css +70 -4
- package/dist/bento.es.js +1778 -701
- package/dist/catalogue.json +5283 -0
- package/dist/components/app-footer.d.ts +7 -0
- package/dist/components/app-footer.d.ts.map +1 -1
- package/dist/components/app-switcher.d.ts.map +1 -1
- package/dist/components/backend-status.d.ts +19 -11
- package/dist/components/backend-status.d.ts.map +1 -1
- package/dist/components/badge-colorful.d.ts +13 -0
- package/dist/components/badge-colorful.d.ts.map +1 -1
- package/dist/components/badge-locale.d.ts +7 -0
- package/dist/components/badge-locale.d.ts.map +1 -1
- package/dist/components/error-boundary.d.ts +15 -13
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/language-select.d.ts +8 -0
- package/dist/components/language-select.d.ts.map +1 -1
- package/dist/components/login/login.d.ts +27 -0
- package/dist/components/login/login.d.ts.map +1 -1
- package/dist/components/mode-toggle.d.ts +9 -0
- package/dist/components/mode-toggle.d.ts.map +1 -1
- package/dist/components/nav-secondary.d.ts +7 -0
- package/dist/components/nav-secondary.d.ts.map +1 -1
- package/dist/components/router/dialog.delete.d.ts +8 -0
- package/dist/components/router/dialog.delete.d.ts.map +1 -1
- package/dist/components/router/gradient-button-link.d.ts +6 -0
- package/dist/components/router/gradient-button-link.d.ts.map +1 -1
- package/dist/components/router/index.d.ts +0 -11
- package/dist/components/router/index.d.ts.map +1 -1
- package/dist/components/router/loading-provider.d.ts +4 -0
- package/dist/components/router/loading-provider.d.ts.map +1 -1
- package/dist/components/startup-first/startup-first.d.ts +29 -0
- package/dist/components/startup-first/startup-first.d.ts.map +1 -1
- package/dist/components/ui/accordion.d.ts +17 -0
- package/dist/components/ui/accordion.d.ts.map +1 -1
- package/dist/components/ui/avatar.d.ts +14 -0
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/badge.d.ts +6 -0
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +25 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/button.d.ts +15 -1
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/card.d.ts +27 -0
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -1
- package/dist/components/ui/dialog.d.ts +36 -0
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/drawer.d.ts +35 -0
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +53 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/floating-formulas-bg.d.ts +8 -0
- package/dist/components/ui/floating-formulas-bg.d.ts.map +1 -1
- package/dist/components/ui/form-actions.d.ts +7 -0
- package/dist/components/ui/form-actions.d.ts.map +1 -1
- package/dist/components/ui/form-item-two-columns.d.ts +7 -0
- package/dist/components/ui/form-item-two-columns.d.ts.map +1 -1
- package/dist/components/ui/form.d.ts +25 -0
- package/dist/components/ui/form.d.ts.map +1 -1
- package/dist/components/ui/glass-card.d.ts +18 -14
- package/dist/components/ui/glass-card.d.ts.map +1 -1
- package/dist/components/ui/gradient-button.d.ts +15 -1
- package/dist/components/ui/gradient-button.d.ts.map +1 -1
- package/dist/components/ui/gradient-switch.d.ts +6 -0
- package/dist/components/ui/gradient-switch.d.ts.map +1 -1
- package/dist/components/ui/hover-card.d.ts +14 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -1
- package/dist/components/ui/icon-picker-dialog.d.ts.map +1 -1
- package/dist/components/ui/index.d.ts +6 -4
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/input.d.ts +6 -0
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu.d.ts +41 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/pagination.d.ts +25 -0
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/popover.d.ts +24 -0
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/progress.d.ts.map +1 -1
- package/dist/components/ui/pulse-ring.d.ts +9 -11
- package/dist/components/ui/pulse-ring.d.ts.map +1 -1
- package/dist/components/ui/resizable.d.ts +11 -0
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/section-card.d.ts +9 -0
- package/dist/components/ui/section-card.d.ts.map +1 -1
- package/dist/components/ui/select.d.ts +36 -0
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.d.ts.map +1 -1
- package/dist/components/ui/sheet.d.ts +29 -0
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sidebar.d.ts +83 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -1
- package/dist/components/ui/skeleton.d.ts +4 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/slider.d.ts +4 -0
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/sonner.d.ts +16 -0
- package/dist/components/ui/sonner.d.ts.map +1 -1
- package/dist/components/ui/stepper.d.ts +8 -0
- package/dist/components/ui/stepper.d.ts.map +1 -1
- package/dist/components/ui/sticky-save-bar.d.ts +7 -0
- package/dist/components/ui/sticky-save-bar.d.ts.map +1 -1
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/components/ui/table.d.ts +31 -0
- package/dist/components/ui/table.d.ts.map +1 -1
- package/dist/components/ui/tabs.d.ts +16 -0
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts +4 -0
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/toggle-group.d.ts +7 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -1
- package/dist/components/ui/toggle.d.ts +4 -0
- package/dist/components/ui/toggle.d.ts.map +1 -1
- package/dist/components/ui/tooltip.d.ts +18 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/components/ui/user-avatar.d.ts +2 -1
- package/dist/components/ui/user-avatar.d.ts.map +1 -1
- package/dist/components/ui/viglet-assistant.d.ts +103 -0
- package/dist/components/ui/viglet-assistant.d.ts.map +1 -0
- package/dist/components/ui/viglet-avatar.d.ts +44 -0
- package/dist/components/ui/viglet-avatar.d.ts.map +1 -0
- package/dist/contexts/breadcrumb.context.d.ts +7 -0
- package/dist/contexts/breadcrumb.context.d.ts.map +1 -1
- package/dist/contexts/user.context.d.ts +8 -0
- package/dist/contexts/user.context.d.ts.map +1 -1
- package/dist/exports.json +359 -30
- package/dist/floating-formulas-bg.cjs +1 -1
- package/dist/floating-formulas-bg.es.js +56 -55
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/use-assistant-notifications.d.ts +15 -0
- package/dist/hooks/use-assistant-notifications.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +168 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/locales/en/assistant.json.d.ts +25 -0
- package/dist/i18n/locales/en/bento.json.d.ts +48 -2
- package/dist/i18n/locales/en/common.json.d.ts +6 -1
- package/dist/i18n/locales/en/language.json.d.ts +8 -0
- package/dist/i18n/locales/pt/assistant.json.d.ts +25 -0
- package/dist/i18n/locales/pt/bento.json.d.ts +48 -2
- package/dist/i18n/locales/pt/common.json.d.ts +6 -1
- package/dist/i18n/locales/pt/language.json.d.ts +8 -0
- package/dist/i18n.cjs +1 -1
- package/dist/i18n.es.js +572 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +2280 -733
- package/dist/lib/busy.d.ts +26 -0
- package/dist/lib/busy.d.ts.map +1 -0
- package/dist/lib/corner-slot.d.ts +15 -0
- package/dist/lib/corner-slot.d.ts.map +1 -0
- package/dist/{loading-provider-BWWhhfEn.js → loading-provider-ClDkFPuv.js} +84 -81
- package/dist/loading-provider-DGuCfsl9.cjs +1 -0
- package/dist/preset.css +7 -10
- package/dist/router.cjs +1 -1
- package/dist/router.es.js +2 -776
- package/dist/user.context-Cr3GAQ_r.js +762 -0
- package/dist/user.context-hbdvmXOn.cjs +1 -0
- package/dist/utils-Fk5jmzzb.js +41 -0
- package/dist/utils-Oagwe_oa.cjs +1 -0
- package/dist/viglet-design-system.css +1 -1
- package/dist/vite/boot-loader.d.ts +33 -2
- package/dist/vite/boot-loader.d.ts.map +1 -1
- package/dist/vite.cjs +16 -5
- package/dist/vite.es.js +27 -14
- package/docs/BENTO-AUTHORING.md +81 -11
- package/docs/reference/grounds.dc.html +9 -9
- package/package.json +16 -4
- package/scripts/consumer-entries.mjs +209 -0
- package/scripts/lib/contrast.mjs +301 -0
- package/scripts/mcp.mjs +300 -0
- package/scripts/page-lint.mjs +428 -0
- package/scripts/page-reference.mjs +54 -10
- package/dist/badge-locale-CoEEv7s5.js +0 -581
- package/dist/badge-locale-hQyWhAKe.cjs +0 -7
- package/dist/breadcrumb.context-KQut7j8U.js +0 -917
- package/dist/breadcrumb.context-Vwc3lAlf.cjs +0 -1
- package/dist/components/router/blank-slate.d.ts +0 -21
- package/dist/components/router/blank-slate.d.ts.map +0 -1
- package/dist/components/router/grid.list.d.ts +0 -39
- package/dist/components/router/grid.list.d.ts.map +0 -1
- package/dist/components/router/internal.sidebar.d.ts +0 -61
- package/dist/components/router/internal.sidebar.d.ts.map +0 -1
- package/dist/components/router/nav-main.d.ts +0 -25
- package/dist/components/router/nav-main.d.ts.map +0 -1
- package/dist/components/router/nav-user.d.ts +0 -24
- package/dist/components/router/nav-user.d.ts.map +0 -1
- package/dist/components/router/page-content.d.ts +0 -12
- package/dist/components/router/page-content.d.ts.map +0 -1
- package/dist/components/router/page-header.d.ts +0 -23
- package/dist/components/router/page-header.d.ts.map +0 -1
- package/dist/components/router/page.d.ts +0 -22
- package/dist/components/router/page.d.ts.map +0 -1
- package/dist/components/router/sticky.page.header.d.ts +0 -85
- package/dist/components/router/sticky.page.header.d.ts.map +0 -1
- package/dist/components/router/sub.page.d.ts +0 -50
- package/dist/components/router/sub.page.d.ts.map +0 -1
- package/dist/components/router/sub.page.header.d.ts +0 -37
- package/dist/components/router/sub.page.header.d.ts.map +0 -1
- package/dist/i18n-DD3rQOWi.js +0 -426
- package/dist/i18n-DrBq-qJJ.cjs +0 -1
- package/dist/lib/react-markers.d.ts +0 -17
- package/dist/lib/react-markers.d.ts.map +0 -1
- package/dist/loading-provider-CmeMqLGk.cjs +0 -1
- package/dist/user.context-B9vIBzY5.cjs +0 -1
- 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
|
+
}
|