@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/governance/rules/ui/440-surface-capture.md +2 -0
- package/package.json +4 -3
- package/scripts/core/regen-web-favicon.ts +67 -0
- package/src/design/base.css +8 -3
- package/src/design/tokens.ts +23 -0
- package/src/gate/stages.ts +27 -4
- package/tooling/web/reference.md +1 -1
|
@@ -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.
|
|
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": "
|
|
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:
|
|
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
|
+
)
|
package/src/design/base.css
CHANGED
|
@@ -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
|
|
package/src/design/tokens.ts
CHANGED
|
@@ -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',
|
package/src/gate/stages.ts
CHANGED
|
@@ -220,10 +220,11 @@ export const STAGES: readonly Stage[] = [
|
|
|
220
220
|
success: 'Tooling paths clean',
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
|
-
// `.claude/DESIGN.md
|
|
224
|
-
// `src/design/tokens.ts` and
|
|
225
|
-
// one source is the cost of the token move,
|
|
226
|
-
// run is only safe while something fails
|
|
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
|
},
|
package/tooling/web/reference.md
CHANGED
|
@@ -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
|
|
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
|
|