@erclx/canon 4.64.0 → 4.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.64.0",
4
+ "version": "4.65.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -4,6 +4,7 @@ paths:
4
4
  - '**/routes/**/*.{tsx,jsx,vue,svelte,astro}'
5
5
  - '**/pages/**/*.{tsx,jsx,vue,svelte,astro}'
6
6
  - '**/app/**/page.{tsx,jsx}'
7
+ - '**/components/**/*.{tsx,jsx,vue,svelte,astro}'
7
8
  - '**/*.html'
8
9
  ---
9
10
 
@@ -13,6 +14,7 @@ paths:
13
14
 
14
15
  - A surface is anything the project renders for a person to look at. A framework route is one. A page the project generates is another, whoever generates it.
15
16
  - Judge a generated page by the same rule as a route. Reading its markup reports nothing about how it composes.
17
+ - A component the production build strips out is not a surface, such as a dev-only scenario switcher gated behind a build flag. The build removes it before anything renders, so no comparison exists to capture.
16
18
 
17
19
  ## When to capture
18
20
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.64.0",
4
+ "version": "4.65.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -41,9 +41,10 @@
41
41
  "bootstrap": "./scripts/core/bootstrap.sh",
42
42
  "canon:sandbox": "canon sandbox",
43
43
  "canon:sandbox:reset": "canon sandbox reset",
44
- "web:tokens": "canon design css --no-components > web/src/styles/tokens.css.new && (echo '/* Generated by `canon design css --no-components`. Regenerate with `bun run web:tokens`. Do not hand-edit. */'; cat web/src/styles/tokens.css.new) > web/src/styles/tokens.css && rm web/src/styles/tokens.css.new",
44
+ "web:tokens": "bun src/cli.ts design css --no-components > web/src/styles/tokens.css.new && (echo '/* Generated by `canon design css --no-components`. Regenerate with `bun run web:tokens`. Do not hand-edit. */'; cat web/src/styles/tokens.css.new) > web/src/styles/tokens.css && rm web/src/styles/tokens.css.new",
45
45
  "web:dev": "cd web && astro dev",
46
- "web:build": "bun run web:tokens && cd web && astro check && astro build",
46
+ "web:favicon": "bun scripts/core/regen-web-favicon.ts",
47
+ "web:build": "bun run web:tokens && bun run web:favicon && cd web && astro check && astro build",
47
48
  "web:preview": "cd web && astro preview",
48
49
  "web:e2e": "cd web && playwright test"
49
50
  },
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Writes web/public/favicon.svg from assets/brand/mark.svg and the design
3
+ * source's two accent values.
4
+ *
5
+ * The page is the fourth surface to carry the mark as a favicon and the only
6
+ * one that can answer for itself. `regen-hero.sh` and `src/design/render.ts`
7
+ * each bake a single literal, because both embed the mark as a data URI and a
8
+ * data URI has no CSS context. A file served at its own URL does have one, so
9
+ * this copy carries a `prefers-color-scheme` branch and tracks the reader's
10
+ * theme rather than picking one accent for everybody.
11
+ *
12
+ * `web/public/favicon.svg` was a symlink to the source before this, and the
13
+ * source fills `currentColor`, which resolves to black with no CSS context.
14
+ * That is what painted the tab icon black on every surface the page reaches.
15
+ * The source keeps `currentColor`, since the hero topbar embeds the same file
16
+ * inline and wants it to inherit.
17
+ */
18
+ import { writeFileSync } from 'node:fs'
19
+
20
+ const root = new URL('../..', import.meta.url).pathname
21
+
22
+ const mark = (await Bun.file(`${root}assets/brand/mark.svg`).text()).trim()
23
+ if (!mark) {
24
+ console.error('regen-web-favicon: assets/brand/mark.svg read empty')
25
+ process.exit(1)
26
+ }
27
+
28
+ const tokenCss = await Bun.file(`${root}web/src/styles/tokens.css`).text()
29
+ const read = (name: string): string => {
30
+ const match = tokenCss.match(
31
+ new RegExp(`--color-${name}:\\s*(#[0-9a-fA-F]{3,8})`),
32
+ )
33
+ if (!match) {
34
+ console.error(
35
+ `regen-web-favicon: tokens.css carries no --color-${name}, refusing to write a colorless favicon`,
36
+ )
37
+ process.exit(1)
38
+ }
39
+ return match[1] as string
40
+ }
41
+
42
+ const dark = read('accent')
43
+ const light = read('light-accent')
44
+
45
+ // The shapes only, with the source's authoring comment dropped. Both fills are
46
+ // replaced by a rule rather than an attribute so one branch can flip both.
47
+ const shapes = mark
48
+ .replace(/<!--[\s\S]*?-->/, '')
49
+ .trim()
50
+ .replace(/^<svg[^>]*>/, '')
51
+ .replace(/<\/svg>$/, '')
52
+ .replaceAll(' fill="currentColor"', '')
53
+ .trim()
54
+
55
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
56
+ <style>
57
+ path, rect { fill: ${light}; }
58
+ @media (prefers-color-scheme: dark) { path, rect { fill: ${dark}; } }
59
+ </style>
60
+ ${shapes.replace(/\n\s*/g, '\n ')}
61
+ </svg>
62
+ `
63
+
64
+ writeFileSync(`${root}web/public/favicon.svg`, svg)
65
+ console.log(
66
+ `regen-web-favicon: wrote web/public/favicon.svg (${light} / ${dark})`,
67
+ )
@@ -16,9 +16,13 @@
16
16
  --color-success: #61c454;
17
17
  --color-light-background: #faf7f2;
18
18
  --color-light-surface: #f4efe6;
19
+ --color-light-chrome: #ede4d6;
19
20
  --color-light-text: #1a1815;
21
+ --color-light-text-body: #3d3630;
22
+ --color-light-text-secondary: #5c544b;
20
23
  --color-light-muted: #726b62;
21
24
  --color-light-accent: #a4471c;
25
+ --color-light-success: #2d6b22;
22
26
  --color-light-border: #e4dcd0;
23
27
  --space-xs: 6px;
24
28
  --space-sm: 12px;
@@ -53,15 +57,16 @@
53
57
  --radius-marker: 999px;
54
58
  }
55
59
 
56
- /* The record declares no light counterpart for chrome, text-body, text-secondary, success, so
57
- a light-ground surface using one is reading a dark value. Declare the
58
- counterpart in src/design/tokens.ts rather than overriding it here. */
59
60
  [data-theme='light'] {
60
61
  --color-background: var(--color-light-background);
61
62
  --color-surface: var(--color-light-surface);
63
+ --color-chrome: var(--color-light-chrome);
62
64
  --color-text: var(--color-light-text);
65
+ --color-text-body: var(--color-light-text-body);
66
+ --color-text-secondary: var(--color-light-text-secondary);
63
67
  --color-muted: var(--color-light-muted);
64
68
  --color-accent: var(--color-light-accent);
69
+ --color-success: var(--color-light-success);
65
70
  --color-border: var(--color-light-border);
66
71
  }
67
72
 
@@ -180,12 +180,29 @@ export const TOKENS: DesignTokens = {
180
180
  intent: 'cards and panels on a light ground',
181
181
  value: '#f4efe6',
182
182
  },
183
+ {
184
+ role: 'light-chrome',
185
+ intent: 'the window titlebar, one step above the canvas',
186
+ value: '#ede4d6',
187
+ },
183
188
  {
184
189
  role: 'light-text',
185
190
  intent: 'primary text on a light ground',
186
191
  value: '#1a1815',
187
192
  grounds: LIGHT_GROUNDS,
188
193
  },
194
+ {
195
+ role: 'light-text-body',
196
+ intent: 'default body copy on a light ground',
197
+ value: '#3d3630',
198
+ grounds: LIGHT_GROUNDS,
199
+ },
200
+ {
201
+ role: 'light-text-secondary',
202
+ intent: 'labels, captions, supporting copy on a light ground',
203
+ value: '#5c544b',
204
+ grounds: LIGHT_GROUNDS,
205
+ },
189
206
  {
190
207
  role: 'light-muted',
191
208
  intent: 'secondary text on a light ground',
@@ -198,6 +215,12 @@ export const TOKENS: DesignTokens = {
198
215
  value: '#a4471c',
199
216
  grounds: LIGHT_GROUNDS,
200
217
  },
218
+ {
219
+ role: 'light-success',
220
+ intent: 'confirmations, rendered and in the terminal, on light',
221
+ value: '#2d6b22',
222
+ grounds: ['light-background'],
223
+ },
201
224
  {
202
225
  role: 'light-border',
203
226
  intent: 'rules and panel edges on light',
@@ -220,10 +220,11 @@ export const STAGES: readonly Stage[] = [
220
220
  success: 'Tooling paths clean',
221
221
  },
222
222
  {
223
- // `.claude/DESIGN.md` and the base stylesheet are both written from
224
- // `src/design/tokens.ts` and neither is edited by hand. Two artifacts from
225
- // one source is the cost of the token move, and a render step that has to
226
- // run is only safe while something fails when it did not, which is this.
223
+ // `.claude/DESIGN.md`, the base stylesheet, the web stylesheet, and the
224
+ // tab icon are all written from `src/design/tokens.ts` and none is edited
225
+ // by hand. Four artifacts from one source is the cost of the token move,
226
+ // and a render step that has to run is only safe while something fails
227
+ // when it did not, which is this.
227
228
  id: 'design',
228
229
  label: 'Design',
229
230
  checks: [
@@ -244,6 +245,28 @@ export const STAGES: readonly Stage[] = [
244
245
  failure:
245
246
  'The base stylesheet drifted from the token source. Run bun run check and commit src/design/base.css.',
246
247
  },
248
+ {
249
+ kind: 'command',
250
+ argv: ['bun', 'run', 'web:tokens'],
251
+ failure: 'Web token regen failed',
252
+ },
253
+ {
254
+ kind: 'drift',
255
+ pathspec: 'web/src/styles/tokens.css',
256
+ failure:
257
+ 'The web stylesheet drifted from the token source. Run bun run check and commit web/src/styles/tokens.css.',
258
+ },
259
+ {
260
+ kind: 'command',
261
+ argv: ['bun', 'run', 'web:favicon'],
262
+ failure: 'Web favicon regen failed',
263
+ },
264
+ {
265
+ kind: 'drift',
266
+ pathspec: 'web/public/favicon.svg',
267
+ failure:
268
+ 'The tab icon drifted from the token source. Run bun run check and commit web/public/favicon.svg.',
269
+ },
247
270
  ],
248
271
  success: 'Design source clean',
249
272
  },
@@ -110,7 +110,7 @@ Append rows:
110
110
 
111
111
  `canon tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
112
112
 
113
- `governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a route changes. It fires on route and page files rather than on every component, so a shared component changing every screen fires nothing and the operator runs the capture by hand.
113
+ `governance/rules/ui/440-surface-capture.md` is what asks a session to run the capture after a surface changes. It now fires on every component file too, reversing the route-and-page-only scope this reference once described, after a shipped batch of components carried no capture and a defect went unseen. The rule body states its own exemption for a component the production build strips out, so a reader chasing that case reads it there rather than here.
114
114
 
115
115
  The sweep under `screenshots/` is ignored again, and only a flagged case's `evidence/` output tracks in git, so the first capture a scaffolded target runs after this change is the baseline it commits there.
116
116