@viglet/viglet-design-system 2026.3.5 → 2026.3.8

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.
@@ -0,0 +1,133 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script src="./support.js"></script>
6
+ </head>
7
+ <body>
8
+ <x-dc>
9
+ <helmet>
10
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700&display=swap">
11
+ <style>
12
+ body { margin: 0; background: #ffffff; }
13
+ /* Opened straight from the file tree, `x-dc` is an unknown element and so
14
+ lays out inline, and `helmet` would render its own contents as text. The
15
+ canvas runtime replaces both; these two rules only matter for the raw
16
+ open, which is how a reader in this repository will look at it. */
17
+ x-dc { display: block; }
18
+ helmet { display: none; }
19
+ a { color: #1447e6; text-decoration: none; }
20
+ a:hover { color: #193cb8; }
21
+ .vg { font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
22
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
23
+ .eyebrow { font-size: 10.5px; font-weight: 600; letter-spacing: 0.09em; color: #1447e6; }
24
+ .pin { display: inline-flex; align-items: center; justify-content: center; width: 17px; height: 17px; border-radius: 999px; background: #1447e6; color: #ffffff; font-size: 10.5px; font-weight: 700; flex: 0 0 auto; }
25
+ .req { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 5px; background: #171717; color: #fafafa; font-size: 9.5px; font-weight: 600; letter-spacing: 0.04em; }
26
+ .opt { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 5px; border: 1px solid #e5e5e5; color: #737373; font-size: 9.5px; font-weight: 600; letter-spacing: 0.04em; }
27
+ </style>
28
+ </helmet>
29
+
30
+ <div class="vg" style="position:relative;width:1280px;height:760px;background:#fafafa;color:#0a0a0a;overflow:hidden;display:flex;flex-direction:column">
31
+
32
+ <div style="padding:16px 24px 0">
33
+ <div class="eyebrow">REFERENCE</div>
34
+ <div style="display:flex;align-items:baseline;gap:12px;margin-top:4px">
35
+ <div style="font-size:21px;font-weight:600;letter-spacing:-0.015em">The shell</div>
36
+ <div style="font-size:12.5px;color:#737373">Adopt it before the pages. Three pieces, no provider, no context between them.</div>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- the shell -->
41
+ <div style="margin:14px 24px 0;flex:1;min-height:0;position:relative;border-radius:14px;background:#ffffff;border:1px solid #e5e5e5;overflow:hidden;box-shadow:0 24px 50px -36px rgba(0,0,0,0.4)">
42
+
43
+ <!-- rail: w-16, hidden below md, items start below the header -->
44
+ <div style="position:absolute;left:0;top:0;bottom:0;width:64px;z-index:3;display:flex;flex-direction:column;align-items:center;gap:4px;padding:80px 0 16px;background:rgba(255,255,255,0.55);backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);border-right:1px solid rgba(229,229,229,0.4)">
45
+ <div style="display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:12px;border:1px solid rgba(23,23,23,0.4);background:rgba(23,23,23,0.09);color:#171717" title="aria-current=page">
46
+ <svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11.5 12 5l8 6.5"></path><path d="M6 10.5V19h12v-8.5"></path></svg>
47
+ </div>
48
+ <div style="display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:12px;color:#737373"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="2"></rect><rect x="13" y="4" width="7" height="7" rx="2"></rect><rect x="4" y="13" width="7" height="7" rx="2"></rect></svg></div>
49
+ <div style="display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:12px;color:#737373"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11M9 12h11M9 18h11"></path><path d="M4.5 6h.01M4.5 12h.01M4.5 18h.01"></path></svg></div>
50
+ <div style="flex:1"></div>
51
+ <div class="mono" style="font-size:9px;color:#a1a1a1;transform:rotate(180deg);writing-mode:vertical-rl">64px</div>
52
+ </div>
53
+
54
+ <!-- header -->
55
+ <div style="position:absolute;left:0;right:0;top:0;height:80px;z-index:4;display:flex;align-items:center;gap:10px;padding:0 20px 0 16px;background:rgba(255,255,255,0.55);backdrop-filter:blur(16px) saturate(150%);-webkit-backdrop-filter:blur(16px) saturate(150%);border-bottom:1px solid rgba(229,229,229,0.4)">
56
+ <span class="pin">1</span>
57
+ <div style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:9px;background-image:linear-gradient(to bottom right,#155dfc,#4f39f6);color:#ffffff"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M5 12h14M12 5v14"></path></svg></div>
58
+ <div style="font-family:'Plus Jakarta Sans',system-ui,sans-serif;font-size:16px;font-weight:600;letter-spacing:-0.015em">Product</div>
59
+
60
+ <span class="pin">2</span>
61
+ <div style="display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid #e5e5e5;border-radius:8px;color:#525252"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 6l-6 6 6 6"></path></svg></div>
62
+
63
+ <span class="pin">3</span>
64
+ <div style="display:flex;align-items:center;gap:8px;height:32px;padding:0 10px;border:1px solid #e5e5e5;border-radius:10px;background:#ffffff;min-width:210px">
65
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#737373" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.4-4.4"></path></svg>
66
+ <span style="font-size:12.5px;color:#737373;flex:1">Search</span>
67
+ <span class="mono" style="display:inline-flex;align-items:center;height:19px;padding:0 5px;border:1px solid #e5e5e5;border-radius:5px;font-size:10px;color:#737373">Ctrl K</span>
68
+ </div>
69
+
70
+ <div style="flex:1"></div>
71
+ <span class="pin">4</span>
72
+ <div style="display:inline-flex;align-items:center;height:28px;padding:0 9px;border:1px solid #e5e5e5;border-radius:8px;font-size:11.5px;color:#525252">EN</div>
73
+ <span class="pin">5</span>
74
+ <div style="display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid #e5e5e5;border-radius:8px;color:#525252"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 3v2M12 19v2M3 12h2M19 12h2"></path></svg></div>
75
+ <span class="pin">6</span>
76
+ <div style="width:28px;height:28px;border-radius:999px;background:#f5f5f5;border:1px solid #e5e5e5"></div>
77
+ </div>
78
+ <div class="mono" style="position:absolute;left:74px;top:58px;z-index:5;font-size:9px;color:#a1a1a1">header 80px, so the rail's items start at 80</div>
79
+
80
+ <!-- routed page, in the gutter the rail reserves -->
81
+ <div style="position:absolute;left:64px;right:0;top:80px;bottom:0;overflow:hidden">
82
+ <div style="max-width:1120px;margin:0 auto;padding:28px 32px">
83
+ <div style="display:flex;align-items:center;gap:9px;margin-bottom:16px">
84
+ <span class="pin">7</span>
85
+ <span class="mono" style="font-size:10.5px;color:#737373">main: the shell's column, set once</span>
86
+ </div>
87
+ <div style="height:12px;width:150px;border-radius:999px;background:#ededed;margin-bottom:12px"></div>
88
+ <div style="height:26px;width:340px;border-radius:8px;background:#e6e6e6;margin-bottom:9px"></div>
89
+ <div style="height:11px;width:470px;border-radius:999px;background:#f0f0f0;margin-bottom:22px"></div>
90
+ <div style="display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:14px">
91
+ <div style="height:104px;border-radius:24px;background:rgba(255,255,255,0.72);border:1px solid rgba(229,229,229,0.7);backdrop-filter:blur(18px) saturate(180%);-webkit-backdrop-filter:blur(18px) saturate(180%)"></div>
92
+ <div style="height:104px;border-radius:24px;background:rgba(255,255,255,0.72);border:1px solid rgba(229,229,229,0.7);backdrop-filter:blur(18px) saturate(180%);-webkit-backdrop-filter:blur(18px) saturate(180%)"></div>
93
+ <div style="height:104px;border-radius:24px;background:rgba(255,255,255,0.72);border:1px solid rgba(229,229,229,0.7);backdrop-filter:blur(18px) saturate(180%);-webkit-backdrop-filter:blur(18px) saturate(180%)"></div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- back to top -->
99
+ <div style="position:absolute;right:20px;bottom:20px;z-index:5;display:flex;align-items:center;gap:8px">
100
+ <span class="pin">8</span>
101
+ <div style="display:flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:999px;background:rgba(255,255,255,0.86);backdrop-filter:blur(18px);border:1px solid rgba(229,229,229,0.7);box-shadow:0 10px 22px -12px rgba(0,0,0,0.3);color:#404040"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M6 12l6-6 6 6"></path></svg></div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- the set -->
106
+ <div style="margin:14px 24px 18px;display:grid;grid-template-columns:1fr 1fr 300px;gap:18px;align-items:start">
107
+ <div>
108
+ <div class="eyebrow" style="color:#737373;margin-bottom:7px">THE HEADER, IN THIS ORDER</div>
109
+ <div style="display:flex;flex-direction:column;gap:5px;font-size:11.5px;color:#404040">
110
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">1</span>Mark and wordmark <span class="req">REQUIRED</span></div>
111
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">2</span>Back, by route hierarchy <span class="opt">WHERE THERE IS A PARENT</span></div>
112
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">3</span>Palette trigger, with the platform's own hint <span class="req">REQUIRED</span></div>
113
+ </div>
114
+ </div>
115
+ <div>
116
+ <div class="eyebrow" style="color:#737373;margin-bottom:7px">AND ON THE TRAILING EDGE</div>
117
+ <div style="display:flex;flex-direction:column;gap:5px;font-size:11.5px;color:#404040">
118
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">4</span>Locale <span class="opt">WHERE A SECOND ONE SHIPS</span></div>
119
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">5</span>Ground <span class="req">REQUIRED</span></div>
120
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">6</span>User menu <span class="opt">WHERE THERE IS AN ACCOUNT</span></div>
121
+ <div style="display:flex;align-items:center;gap:7px"><span class="pin">8</span>Back to top <span class="req">REQUIRED</span></div>
122
+ </div>
123
+ </div>
124
+ <div style="border:1px solid rgba(251,44,54,0.3);background:rgba(251,44,54,0.05);border-radius:12px;padding:10px 12px">
125
+ <div class="eyebrow" style="color:#c10007;margin-bottom:5px">TWO THINGS THE HEADER IS NOT</div>
126
+ <div style="font-size:11px;color:#404040;line-height:1.45">A second always-visible nav: the rail is the nav and the palette is the mobile one. And a surface's own controls -- a switcher, a pending count -- which belong to the surface that owns them, not to every page that renders under it.</div>
127
+ </div>
128
+ </div>
129
+
130
+ </div>
131
+ </x-dc>
132
+ </body>
133
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viglet/viglet-design-system",
3
- "version": "2026.3.5",
3
+ "version": "2026.3.8",
4
4
  "type": "module",
5
5
  "description": "Viglet Design System - Shared components, hooks, utilities and design tokens for Viglet products",
6
6
  "license": "Apache-2.0",
@@ -61,11 +61,16 @@
61
61
  }
62
62
  },
63
63
  "bin": {
64
- "viglet-ds-check-duplicates": "./scripts/check-duplicates.mjs"
64
+ "viglet-ds-check-duplicates": "./scripts/check-duplicates.mjs",
65
+ "viglet-ds-page-reference": "./scripts/page-reference.mjs"
65
66
  },
66
67
  "files": [
67
68
  "dist",
68
- "scripts/check-duplicates.mjs"
69
+ "scripts/check-duplicates.mjs",
70
+ "scripts/page-reference.mjs",
71
+ "docs/BENTO-AUTHORING.md",
72
+ "docs/BENTO-BOUNDARY.md",
73
+ "docs/reference"
69
74
  ],
70
75
  "sideEffects": [
71
76
  "**/*.css"
@@ -129,7 +134,7 @@
129
134
  "@storybook/addon-a11y": "^10.3.5",
130
135
  "@storybook/addon-docs": "^10.3.5",
131
136
  "@storybook/addon-themes": "^10.3.5",
132
- "@storybook/addon-vitest": "10.5.10",
137
+ "@storybook/addon-vitest": "10.6.0",
133
138
  "@storybook/react-vite": "^10.3.5",
134
139
  "@tailwindcss/typography": "^0.5.19",
135
140
  "@tailwindcss/vite": "^4.2.2",
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Vendor this package's page reference into a consuming repository.
4
+ *
5
+ * The catalogue answers "does this component exist". What it cannot answer is
6
+ * "does this page read as the same product", and the answer to that lives in
7
+ * two documents and nine artboards which are not in anybody's `node_modules`
8
+ * by reading them: a contract a product author has to browse a repository to
9
+ * find is one they read after the fifth screen, which is what BENTO-AUTHORING's
10
+ * own opening warns against.
11
+ *
12
+ * So the surfaces are written into the consumer, where an agent session and a
13
+ * person both trip over them. Two disciplines, borrowed rather than invented:
14
+ *
15
+ * copy rewritten on every run, and drift is a finding. Every byte is
16
+ * this package's statement; a vendored authority that has drifted
17
+ * is worse than none, because it is read with the same trust.
18
+ * declaration this project's entries re-derived inside a file the adopter
19
+ * also writes in. Compared as parsed JSON, never as bytes: their
20
+ * indentation is not drift, and a check that fails on whitespace
21
+ * is a check nobody leaves switched on.
22
+ *
23
+ * Usage:
24
+ * viglet-ds-page-reference [--root <dir>] [--canvas-dir <rel>]
25
+ * viglet-ds-page-reference --check # report, write nothing, exit 1 on drift
26
+ *
27
+ * --root <dir> the consuming repository (default: the working directory)
28
+ * --canvas-dir <rel> where the artboards go (default: docs/design)
29
+ * --check report what would change and write nothing
30
+ * --json machine-readable form
31
+ * --warn report drift and still exit 0
32
+ */
33
+
34
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
35
+ import { dirname, join, resolve } from "node:path"
36
+ import { fileURLToPath } from "node:url"
37
+
38
+ const here = dirname(fileURLToPath(import.meta.url))
39
+ /** The package root, whether that is this checkout or a `node_modules` copy. */
40
+ const payload = resolve(here, "..")
41
+
42
+ const argv = process.argv.slice(2)
43
+ function flag(name) {
44
+ return argv.includes(`--${name}`)
45
+ }
46
+ function value(name, fallback) {
47
+ const at = argv.indexOf(`--${name}`)
48
+ return at === -1 || at === argv.length - 1 ? fallback : argv[at + 1]
49
+ }
50
+
51
+ const root = resolve(value("root", process.cwd()))
52
+ const canvasDir = value("canvas-dir", "docs/design")
53
+ const checking = flag("check")
54
+ const asJson = flag("json")
55
+ const warnOnly = flag("warn")
56
+
57
+ const version = JSON.parse(readFileSync(join(payload, "package.json"), "utf8")).version
58
+
59
+ const SKILL_DIR = ".claude/skills/viglet-ds-pages"
60
+ const SKILL = `${SKILL_DIR}/SKILL.md`
61
+ const REFERENCE = join(payload, "docs", "reference")
62
+
63
+ /**
64
+ * The one file written new rather than copied, and it states no rule of its own
65
+ * -- it says which pages hold them and where the artboards landed in *this*
66
+ * repository, which is the one fact the package cannot know.
67
+ */
68
+ function orientation(dir, artboards) {
69
+ return `---
70
+ name: viglet-ds-pages
71
+ description: Building or changing a page with @viglet/viglet-design-system/bento -- the shell, the three page shapes, the panel, and which tokens a product claims. Load before the first screen, and before adding a region to one.
72
+ vds-version: ${version}
73
+ ---
74
+
75
+ # Pages in the Viglet design system
76
+
77
+ This is vendored from \`@viglet/viglet-design-system@${version}\` and refreshed by
78
+ \`viglet-ds-page-reference\`. Do not edit these files here: the next run rewrites them, and
79
+ a correction belongs in the package so every consumer gets it.
80
+
81
+ ## Read first
82
+
83
+ - **[authoring.md](authoring.md)** -- the contract. The shell and who owns each region, the
84
+ three page shapes, the panel, colour and the tokens a product claims, i18n, accessibility,
85
+ responsive, tests. Read it before the first screen, not after the fifth.
86
+ - **[boundary.md](boundary.md)** -- which components are the shared layer and which stay in
87
+ a product, and why.
88
+
89
+ ## And look at
90
+
91
+ \`${dir}/vds-*.dc.html\` -- ${artboards} artboards, the contract drawn: every region of a page
92
+ with an ownership key, the reading column owned two ways, what \`--primary\` reaches, the page
93
+ shapes, the panel, and both grounds with their ratios. They are laid out by
94
+ \`${dir}/canvas.json\`, whose \`vds-\` pages and artboards belong to the package; everything
95
+ else in that file is this repository's own and is preserved.
96
+
97
+ An artboard opens in a browser straight from the file tree.
98
+
99
+ ## Two rules that are easy to get wrong
100
+
101
+ - A page sets no max width, no gutters and no vertical rhythm. The shell sets them once.
102
+ - A product claims \`--primary\` through the four \`--vg-primary-*-base\` inputs at \`:root\`,
103
+ never by setting \`--vg-primary\` itself, which would key the dark ground to the light value.
104
+
105
+ ## Checking it is current
106
+
107
+ \`\`\`bash
108
+ viglet-ds-page-reference --check
109
+ \`\`\`
110
+ `
111
+ }
112
+
113
+ /** A verbatim copy, rewritten every run. Drift is the finding. */
114
+ function copied(to, text) {
115
+ const at = join(root, to)
116
+ const existed = existsSync(at)
117
+ const drifted = !existed || readFileSync(at, "utf8") !== text
118
+ return { to, kind: "copy", text, existed, drifted }
119
+ }
120
+
121
+ const artboards = existsSync(REFERENCE)
122
+ ? readdirSync(REFERENCE)
123
+ .filter((name) => name.endsWith(".dc.html"))
124
+ .sort()
125
+ : []
126
+
127
+ if (artboards.length === 0) {
128
+ console.error(`No artboards in ${REFERENCE}. This is not a copy of the package that ships them.`)
129
+ process.exit(1)
130
+ }
131
+
132
+ /**
133
+ * The consumer's own canvas, with this package's pages re-derived inside it.
134
+ *
135
+ * Their artboards, their pages, their notes and their launch view are carried
136
+ * through untouched. Ours are recognised by the `vds-` prefix and replaced, so
137
+ * an artboard the package drops stops being listed rather than lingering.
138
+ */
139
+ function declaration() {
140
+ const to = `${canvasDir}/canvas.json`
141
+ const at = join(root, to)
142
+ const ours = JSON.parse(readFileSync(join(REFERENCE, "canvas.json"), "utf8"))
143
+ const mine = (name) => name.startsWith("vds-")
144
+
145
+ let current = null
146
+ if (existsSync(at)) {
147
+ try {
148
+ current = JSON.parse(readFileSync(at, "utf8"))
149
+ } catch {
150
+ console.error(`${to} is not JSON this can merge into. Fix or move it; nothing was written.`)
151
+ process.exit(1)
152
+ }
153
+ if (current === null || typeof current !== "object" || Array.isArray(current)) {
154
+ console.error(`${to} is not a JSON object. Nothing was written.`)
155
+ process.exit(1)
156
+ }
157
+ }
158
+
159
+ const theirs = current ?? {}
160
+ const merged = {
161
+ ...theirs,
162
+ pages: [...(theirs.pages ?? []).filter((p) => !mine(p.id)), ...ours.pages],
163
+ artboards: [
164
+ ...(theirs.artboards ?? []).filter((a) => !mine(a.file)),
165
+ ...ours.artboards.map((a) => ({ ...a, file: `vds-${a.file}` })),
166
+ ],
167
+ annotations: [
168
+ ...(theirs.annotations ?? []).filter((n) => !mine(n.id)),
169
+ ...(ours.annotations ?? []),
170
+ ],
171
+ // Their opening view, or ours only where they have none.
172
+ launch: theirs.launch ?? ours.launch,
173
+ }
174
+
175
+ const text = `${JSON.stringify(merged, null, 2)}\n`
176
+ return {
177
+ to,
178
+ kind: "declaration",
179
+ text,
180
+ existed: current !== null,
181
+ // Parsed, not byte-compared: their formatter is not drift.
182
+ drifted: current === null || JSON.stringify(current) !== JSON.stringify(merged),
183
+ }
184
+ }
185
+
186
+ const surfaces = [
187
+ copied(SKILL, orientation(canvasDir, artboards.length)),
188
+ copied(`${SKILL_DIR}/authoring.md`, readFileSync(join(payload, "docs", "BENTO-AUTHORING.md"), "utf8")),
189
+ copied(`${SKILL_DIR}/boundary.md`, readFileSync(join(payload, "docs", "BENTO-BOUNDARY.md"), "utf8")),
190
+ ...artboards.map((name) =>
191
+ copied(`${canvasDir}/vds-${name}`, readFileSync(join(REFERENCE, name), "utf8")),
192
+ ),
193
+ declaration(),
194
+ ]
195
+
196
+ /**
197
+ * Which direction the surfaces are behind in.
198
+ *
199
+ * Where the vendored skill was written by a newer package than the one running,
200
+ * "refresh" means downgrade and every word of a drift report would be wrong. So
201
+ * it is named and refused instead.
202
+ */
203
+ const wired = existsSync(join(root, SKILL))
204
+ const stamped = wired
205
+ ? (/^vds-version:\s*(\S+)\s*$/m.exec(readFileSync(join(root, SKILL), "utf8"))?.[1] ?? null)
206
+ : null
207
+ const ahead =
208
+ stamped !== null &&
209
+ stamped !== version &&
210
+ stamped.localeCompare(version, undefined, { numeric: true }) > 0
211
+
212
+ const changing = surfaces.filter((s) => s.drifted)
213
+
214
+ /**
215
+ * A `vds-` artboard this package no longer ships.
216
+ *
217
+ * Part of the plan rather than a sweep at the end of a write: a reference that
218
+ * dropped an artboard and left it behind in every consumer is drift in the one
219
+ * direction a copy cannot fix, and it has to be reported by `--check` for the
220
+ * gate to mean what it says.
221
+ */
222
+ const shipped = new Set(artboards.map((name) => `vds-${name}`))
223
+ const orphans = existsSync(join(root, canvasDir))
224
+ ? readdirSync(join(root, canvasDir))
225
+ .filter((name) => name.startsWith("vds-") && name.endsWith(".dc.html") && !shipped.has(name))
226
+ .map((name) => `${canvasDir}/${name}`)
227
+ : []
228
+
229
+ /** created / updated, in the conditional under `--check` and the past tense after a write. */
230
+ function state(surface) {
231
+ if (!surface.drifted) return "unchanged"
232
+ if (!surface.existed) return checking ? "would create" : "created"
233
+ return checking ? "would update" : "updated"
234
+ }
235
+
236
+ function code() {
237
+ if (!wired) return "page-reference.absent"
238
+ return changing.length > 0 || orphans.length > 0 ? "page-reference.stale" : null
239
+ }
240
+
241
+ if (asJson) {
242
+ console.log(
243
+ JSON.stringify(
244
+ {
245
+ version,
246
+ root,
247
+ canvasDir,
248
+ wired,
249
+ stamped,
250
+ ahead,
251
+ code: code(),
252
+ surfaces: surfaces.map((s) => ({ to: s.to, kind: s.kind, state: state(s) })),
253
+ orphans,
254
+ },
255
+ null,
256
+ 2,
257
+ ),
258
+ )
259
+ } else if (ahead) {
260
+ console.error(
261
+ `\n${SKILL} was written by ${stamped} and this package is ${version}: the surfaces here are ` +
262
+ `ahead, not behind.\nRefreshing them would be a downgrade, so nothing was written. Upgrade ` +
263
+ `the package, or run this from the checkout that matches.`,
264
+ )
265
+ process.exit(1)
266
+ } else if (!wired && checking) {
267
+ // The vendored skill is the discriminator: an unwired project pays one stat
268
+ // and is told what it is missing, rather than reading nine lines of drift.
269
+ console.log(`\npage-reference.absent ${root}`)
270
+ console.log(` This repository has no vendored page reference.`)
271
+ console.log(` Without it a session has no contract for the shell, the page shapes or the`)
272
+ console.log(` tokens a product claims -- the four decisions that make two products built`)
273
+ console.log(` from this package stop looking like one.`)
274
+ console.log(`\n viglet-ds-page-reference writes ${surfaces.length} surface(s)`)
275
+ } else if (changing.length === 0 && orphans.length === 0) {
276
+ console.log(`\nThe page reference is current (${version}), ${surfaces.length} surface(s).`)
277
+ } else {
278
+ const heading = checking ? "page-reference.stale" : `Writing into ${root}`
279
+ console.log(`\n${heading} ${version}`)
280
+ for (const s of surfaces) {
281
+ console.log(` ${state(s).padEnd(13)} ${s.to}`)
282
+ }
283
+ for (const orphan of orphans) {
284
+ console.log(` ${(checking ? "would remove" : "removed").padEnd(13)} ${orphan}`)
285
+ }
286
+ if (!checking) {
287
+ for (const s of changing) {
288
+ const at = join(root, s.to)
289
+ mkdirSync(dirname(at), { recursive: true })
290
+ writeFileSync(at, s.text, "utf8")
291
+ }
292
+ for (const orphan of orphans) rmSync(join(root, orphan))
293
+ console.log(`\nDone. Wire \`viglet-ds-page-reference --check\` into this repository's lint.`)
294
+ }
295
+ }
296
+
297
+ const drifting = !wired || changing.length > 0 || orphans.length > 0
298
+ process.exit(checking && drifting && !warnOnly ? 1 : 0)