@erclx/canon 5.53.0 → 5.55.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/CHANGELOG.md +18 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +6 -6
- package/claude/skills/document-repair/SKILL.md +5 -5
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/target-check/SKILL.md +6 -8
- package/claude/skills/target-check/references/domains.md +1 -4
- package/claude/skills/target-setup/SKILL.md +8 -8
- package/claude/skills/target-update/SKILL.md +5 -5
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +2 -10
- package/docs/agents/output-shape.md +11 -10
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -5
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/ui.sh +12 -65
- package/src/audits/run.ts +1 -1
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -2
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +20 -324
- package/src/commands/gate.ts +5 -3
- package/src/commands/gov.ts +11 -21
- package/src/commands/labels.ts +1 -1
- package/src/commands/pr.ts +1 -1
- package/src/commands/sync.ts +5 -6
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +4 -20
- package/src/gov/audit.ts +1 -6
- package/src/hooks/post-merge.ts +2 -5
- package/src/markdown/scope.ts +2 -4
- package/src/process/harness.ts +3 -4
- package/src/sandbox/equivalence.ts +0 -1
- package/src/sandbox/provision.ts +8 -13
- package/src/sandbox/scenario.ts +6 -12
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/engine.ts +3 -8
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/src/sync/workflow.ts +3 -5
- package/src/tooling/verify-stack.ts +1 -1
- package/tooling/base/configs/.husky/post-merge +6 -10
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/governance/rules/ui/canvas.md +0 -13
- package/scripts/core/check-color-source.sh +0 -51
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: canvas-board
|
|
3
|
-
description: Scope boundary for driving the local design canvas, and the line between a drafted frame and the design it feeds
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Canvas board requirement
|
|
7
|
-
|
|
8
|
-
## Gap
|
|
9
|
-
|
|
10
|
-
Without this skill, a session asked to draw on the canvas writes frames nothing is serving and reports them drawn. It trusts a passing capture, though a blank shell passes every capture while the operator sees an empty tab. From a linked worktree it writes a copy the canvas never serves, and it captures after every edit.
|
|
11
|
-
|
|
12
|
-
It reads "this one" from the conversation rather than the operator's selection, or acts on a stale element pick and edits the wrong element. It restyles through a class while the operator's inspector edit sits inline, so the change never shows. A picked frame stays on the canvas as the only record, and the project's design record never learns it.
|
|
13
|
-
|
|
14
|
-
## Must
|
|
15
|
-
|
|
16
|
-
- The canvas is serving and its live shell loads at the address the server reported before any frame is written, and a refusal or a blank shell is reported rather than drawn into
|
|
17
|
-
- Pages and frames go through the verbs, so the session and the operator's drags share one writer, and a frame from a linked worktree takes the main-root route
|
|
18
|
-
- The operator's selection decides what "this one" means, and a stale element pick reads as unknown
|
|
19
|
-
- A frame's inline styles are read before a restyle, the run names which operator edits it kept and dropped, and capture runs once per finished frame or page
|
|
20
|
-
- A picked frame is handed to `design-record` by page and frame, while a picked layout stays on the canvas for the plan that builds it
|
|
21
|
-
|
|
22
|
-
## Must not
|
|
23
|
-
|
|
24
|
-
- Present the canvas as the project's source of truth for design, or promise it survives a lost checkout
|
|
25
|
-
- Write frames when `canon` or its canvas command is missing
|
|
26
|
-
- Name any third-party design tool, or the canvas's predecessor surface
|
|
27
|
-
- Be reached only by the author typing its name, since a request to draw, select, or capture on the canvas should reach it by description
|
|
28
|
-
|
|
29
|
-
## Out of scope
|
|
30
|
-
|
|
31
|
-
- Drafting candidates for a taste decision and taking the pick, which `pick-by-eye` owns
|
|
32
|
-
- What the canvas verbs do internally, which the CLI's own contract owns
|
|
33
|
-
- The content format a frame and a layout take, which the canvas standard owns
|
|
34
|
-
- Landing a pick in the reference folder and carrying its values into the design document, which `design-record` owns
|
|
35
|
-
- Tracing reference images into design values and backing up the canvas folder, which no skill owns
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: canvas-board
|
|
3
|
-
description: Drives the local design canvas, where pages hold HTML frames the operator sees, drags, selects, and edits in the browser. Starts the canvas when it is not running, confirms the live shell loads, makes pages and frames, edits a frame through its file, reads what the operator selected or restyled, captures finished frames, and hands a picked frame to `design-record`. Use when asked to "open the canvas", "put this on the canvas", "draw a frame for X", "change the one I selected", "what did I pick", "capture this frame", or "move that frame on the canvas". Do NOT use to draft candidates for a taste decision, which is `pick-by-eye`.
|
|
4
|
-
metadata:
|
|
5
|
-
family: decide
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Canvas board
|
|
9
|
-
|
|
10
|
-
Drive the canvas the operator is looking at. A page is a folder of HTML frames and a frame is one file, so a session draws by writing files and the operator sees each write land in the browser. The content format is fixed by `${CLAUDE_SKILL_DIR}/references/canvas.md`. Read it before writing a frame.
|
|
11
|
-
|
|
12
|
-
Every verb below is `canon canvas`. Run `canon canvas --help` for the verbs and their refusals, and read `${CLAUDE_SKILL_DIR}/references/canvas.md` for the content format.
|
|
13
|
-
|
|
14
|
-
## Guards
|
|
15
|
-
|
|
16
|
-
- If `canon` is not on PATH, stop: `❌ canon CLI not found.`
|
|
17
|
-
- If `canon canvas --help` reports no such command, stop: `❌ The installed canon carries no canvas command. Update the CLI and re-run.`
|
|
18
|
-
- Write no frame before the canvas is serving and the shell has loaded. A frame nobody can see is a draft reported as delivered.
|
|
19
|
-
- Never present a frame as the project's design. The canvas is a drafting surface and the project's design record holds what was decided.
|
|
20
|
-
- Never promise the canvas survives a lost checkout. Its folder is gitignored and nothing backs it up.
|
|
21
|
-
|
|
22
|
-
## Step 1: start the canvas and confirm the shell
|
|
23
|
-
|
|
24
|
-
Read the pages first, which also proves the verb resolves:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
canon canvas list --json
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The record carries `content`, the folder every page lives in. It sits at the main worktree root whichever worktree the session runs in, so write through that path rather than one composed from the current directory.
|
|
31
|
-
|
|
32
|
-
Start the server in the background unless the operator named an address that already answers, since it runs until interrupted. No verb reports a running server, and a second one is harmless, since it walks to the next free port and serves the same folder:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
canon canvas serve --json
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Read `url` off the record rather than composing one, since the server walks past a port already in use. Report the refusal and its `reason` when `ok` is false, and stop there.
|
|
39
|
-
|
|
40
|
-
### Check the shell
|
|
41
|
-
|
|
42
|
-
Fetch the printed address and check the shell itself, not a capture, since every capture goes through the frame route alone and passes on a blank shell.
|
|
43
|
-
|
|
44
|
-
- The body is non-empty and carries a `<script` tag. An empty `200` means the shell failed to build, usually a missing dependency, so report a blank shell and stop.
|
|
45
|
-
- `<url>/api/pages` answers with JSON listing the pages.
|
|
46
|
-
|
|
47
|
-
Hand the operator the address as a markdown link carrying the URL as its text and its target.
|
|
48
|
-
|
|
49
|
-
## Step 2: make pages and frames
|
|
50
|
-
|
|
51
|
-
- Add a page for each decision being drafted: `canon canvas page add <page> --json`.
|
|
52
|
-
- Follow the house style in `${CLAUDE_SKILL_DIR}/references/canvas.md` before drafting an explanation frame.
|
|
53
|
-
- Add each frame with `canon canvas frame add <page> <frame> --width <px> --height <px> --json`, which writes the file and its box in one locked step
|
|
54
|
-
|
|
55
|
-
Give each frame the width it should render at, since a capture and the canvas both render at it. The verb places the frame after the last one, and a position the operator asks for goes through `frame move`.
|
|
56
|
-
|
|
57
|
-
### Layout and naming
|
|
58
|
-
|
|
59
|
-
Lay frames out through the verbs rather than by hand, since the operator's drags write the same `layout.json` under a lock.
|
|
60
|
-
|
|
61
|
-
- Move a frame with `canon canvas frame move <page> <frame> --x <px> --y <px> --json`
|
|
62
|
-
- Name pages and frames for what they hold, such as `pricing` and `pricing-dense`, so the operator can say which one by name.
|
|
63
|
-
- After an insert or a resize, re-lay the page with `canon canvas arrange <page> --json`. It keeps dragged rows, sets the standard gap, and drops entries whose file is gone.
|
|
64
|
-
- Run `arrange --check` to report without writing
|
|
65
|
-
|
|
66
|
-
## Step 3: edit a frame through its file
|
|
67
|
-
|
|
68
|
-
Write the frame's whole document to the `path` the add verb reported. The open canvas reloads the frame when its file changes.
|
|
69
|
-
|
|
70
|
-
- From a linked worktree, `Edit` and `Write` refuse a main-root path. Write the whole file through one plain `Bash` heredoc, the route `session-worktree` states, never a worktree copy.
|
|
71
|
-
- Change one property with `canon canvas edit <page>/<frame> --element <index> --set <property>=<value> --json` when its set covers the change, since it leaves every other byte as it was
|
|
72
|
-
- Use the project's custom properties for every value the token stylesheet defines. The `serve` record names the source under `tokens`, and `none` means frames render unstyled, so say so.
|
|
73
|
-
|
|
74
|
-
### Fit the box
|
|
75
|
-
|
|
76
|
-
Fit the box to the content once the write lands, with `canon canvas fit <page>/<frame> --json`, and read each row's `wide` and `font`.
|
|
77
|
-
|
|
78
|
-
- A `wide` frame needs its box widened, which fit never does
|
|
79
|
-
- A `font` frame names a family the canvas does not serve, so set its text to a served face and fit again
|
|
80
|
-
- A fit that grew a frame leaves the rows below overlapped until `canvas arrange` runs
|
|
81
|
-
|
|
82
|
-
### Mark the frame
|
|
83
|
-
|
|
84
|
-
Mark the frame before the first write, so the operator sees which frame is being worked on before anything lands, and clear the mark once the edits to it are done:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
canon canvas editing <page>/<frame> --json
|
|
88
|
-
canon canvas editing <page>/<frame> --done --json
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
- A mark lapses after five minutes, so mark the frame again during a longer edit.
|
|
92
|
-
- Clear it on every exit from the edit, including one that stopped on a refusal. Clearing a frame that holds no mark succeeds.
|
|
93
|
-
- On `unknown command 'editing'`, the CLI predates the mark, so go on without it. Never run `canvas edit` in its place, since that verb writes the frame.
|
|
94
|
-
|
|
95
|
-
## Step 4: read what the operator pointed at
|
|
96
|
-
|
|
97
|
-
Read the selection when the operator says "this one", "the selected frame", or "that button":
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
canon canvas selection --json
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- `selection: null` means nothing is selected or the selected frame is gone. Ask which one rather than guessing.
|
|
104
|
-
- An `element` carrying `stale: true` was picked against an earlier file, so its `index` may land elsewhere. Ask the operator to pick again rather than matching its `tag` and `text` by eye.
|
|
105
|
-
- A fresh `element` gives the `index` that `canvas edit` takes.
|
|
106
|
-
|
|
107
|
-
## Step 5: respect the operator's inspector edits
|
|
108
|
-
|
|
109
|
-
The operator restyles elements from the inspector, and each edit lands as inline `style` on the element. Inline style beats a class, so a class change made afterwards does not show.
|
|
110
|
-
|
|
111
|
-
- Read the frame's inline `style` attributes before restyling anything in it.
|
|
112
|
-
- Keep an inline property the operator set unless the change replaces it, and drop it, through `canvas edit` with an empty value or by rewriting the file, when it does.
|
|
113
|
-
- Say which operator edits were kept and which were dropped.
|
|
114
|
-
|
|
115
|
-
## Step 6: capture finished work
|
|
116
|
-
|
|
117
|
-
Capture once per finished frame or page, not after every edit. The live canvas is the working view, and a capture is the record handed on.
|
|
118
|
-
|
|
119
|
-
- Capture a frame with `canon canvas capture <page>/<frame> --json`. Each PNG is the whole document, so its height follows the content rather than the box.
|
|
120
|
-
- Capture a page with `canon canvas capture <page> --composite --json`, which shows each frame clipped to its box at its layout position
|
|
121
|
-
- Read each `path` off the record rather than composing one.
|
|
122
|
-
|
|
123
|
-
A capture refused because a font is not installed names the family it read off the frame's text. Set that text's `font-family` to a named family the machine has, never a generic keyword such as `sans-serif`, and capture again rather than reporting the frame as captured.
|
|
124
|
-
|
|
125
|
-
## Step 7: hand a picked frame on
|
|
126
|
-
|
|
127
|
-
When the operator picks a frame, the decision leaves the canvas. Hand it to `canon:design-record`, which lands the frame in the project's reference folder and carries its values into the design document. Report it rather than proceeding silently when that skill does not resolve.
|
|
128
|
-
|
|
129
|
-
- Name the page and frame the decision came from in the handoff and the reply, and leave the frame on the canvas until the operator clears it
|
|
130
|
-
- Name the frame in the plan that builds a picked layout, since the layout reaches code through that plan
|
|
131
|
-
- Never cite a frame path from a tracked file. The folder is gitignored, so the citation resolves on this machine alone.
|
|
132
|
-
|
|
133
|
-
## Output
|
|
134
|
-
|
|
135
|
-
```plaintext
|
|
136
|
-
✅ Canvas: <url the serve verb reported>
|
|
137
|
-
Page: <page>, <n> frames
|
|
138
|
-
Selected: <page>/<frame>, element <index>, or none, or stale
|
|
139
|
-
Capture: <path of each capture written>
|
|
140
|
-
Handed: <the frame handed to design-record, the plan that cites it, or not yet>
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Write the canvas line's URL as a markdown link. Omit the capture line when nothing was captured and the handed line when no frame was picked. Emit every path from the project root.
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Canvas reference
|
|
3
|
-
description: Page folders, frame files, the layout.json shape, token resolution, and what a frame must not assume on a local design canvas
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Canvas reference
|
|
7
|
-
|
|
8
|
-
Applies to the canvas content under `.canon/canvas/`, where each page is a folder of HTML frames and a layout file placing them. A frame is a draft drawn to be looked at and picked from, so it changes whenever someone tries a direction, and the operator clears it once the direction lands where the project keeps its design.
|
|
9
|
-
|
|
10
|
-
The folder is gitignored, so nothing in a repository checks its shape and nothing backs it up by default.
|
|
11
|
-
|
|
12
|
-
## Contents
|
|
13
|
-
|
|
14
|
-
- [Scope](#scope)
|
|
15
|
-
- [What a working canvas looks like](#what-a-working-canvas-looks-like)
|
|
16
|
-
- [Pages](#pages)
|
|
17
|
-
- [Frames](#frames)
|
|
18
|
-
- [House style](#house-style)
|
|
19
|
-
- [Layout](#layout)
|
|
20
|
-
- [Tokens](#tokens)
|
|
21
|
-
- [What a frame must not assume](#what-a-frame-must-not-assume)
|
|
22
|
-
- [Template](#template)
|
|
23
|
-
|
|
24
|
-
## Scope
|
|
25
|
-
|
|
26
|
-
Governs the canvas content under `.canon/canvas/`: page folder names, frame files, the `layout.json` shape, how tokens reach a frame, and what a frame must not assume about where it renders.
|
|
27
|
-
|
|
28
|
-
- Also governs the house style of an explanation frame, being a diagram, a board of notes, or a comparison
|
|
29
|
-
- A mockup of the project's own interface is not an explanation frame, and follows the project's design document for type, color, and layout
|
|
30
|
-
|
|
31
|
-
Does not govern:
|
|
32
|
-
|
|
33
|
-
- The design decisions a mockup frame is drafting, such as type, color, and layout taste: the project's own design document
|
|
34
|
-
- The project's design document and the token module it renders from, which a picked direction is carried into: `design.md`
|
|
35
|
-
- The procedure that starts the canvas, reads the operator's selection, and carries a pick back, which belongs to the surface driving the canvas
|
|
36
|
-
- Voice, rhythm, and sentence construction in any text a frame shows: the `write-human` skill
|
|
37
|
-
|
|
38
|
-
## What a working canvas looks like
|
|
39
|
-
|
|
40
|
-
A canvas works when the operator and a session can point at the same thing and see the same thing:
|
|
41
|
-
|
|
42
|
-
- Which pages exist, and which frames sit on each?
|
|
43
|
-
- Where does each frame sit on its page, and at what width does it render?
|
|
44
|
-
- Does each frame show the project's own tokens rather than values copied into it?
|
|
45
|
-
- Does a frame render the same in the live canvas as in a capture?
|
|
46
|
-
|
|
47
|
-
A canvas failing these is non-conforming even when it satisfies every shape rule below.
|
|
48
|
-
|
|
49
|
-
## Pages
|
|
50
|
-
|
|
51
|
-
- Make one folder per page directly under `.canon/canvas/`. A page groups the frames compared side by side, such as the candidates for one decision.
|
|
52
|
-
- Name a page as one path segment of letters, digits, dots, dashes, and underscores, starting with a letter or a digit. A name opening with a dot is hidden from every listing.
|
|
53
|
-
- Never write a folder whose name opens with a dot. Those names are reserved for files the canvas generates and removes itself.
|
|
54
|
-
- Rename a page rather than copying it under a new name, so its layout and frames stay together.
|
|
55
|
-
|
|
56
|
-
## Frames
|
|
57
|
-
|
|
58
|
-
- Write one frame as one `.html` file inside its page folder. The filename without its extension is the frame's name, and the name follows the page naming rule.
|
|
59
|
-
- Write a frame as a whole document or as a fragment. The server writes in the `html`, `head`, and `body` a fragment leaves out, marked so a pick still matches the file.
|
|
60
|
-
- State a `body` whenever the frame states a `head`. The browser builds one the file leaves out, and an address the operator picks inside the frame can no longer be matched to the file.
|
|
61
|
-
- Write every table with its `tbody` stated, for the same reason. A browser adds one the file leaves out.
|
|
62
|
-
|
|
63
|
-
### Content and edits
|
|
64
|
-
|
|
65
|
-
A frame is the only copy of its markup, so it carries everything it needs.
|
|
66
|
-
|
|
67
|
-
- Keep a frame self-contained. Inline its own styles and scripts, or link a file inside the same page folder, since a frame is served from its page and nothing outside the canvas resolves.
|
|
68
|
-
- Use the project's custom properties, `var(--<name>)`, for every value the token stylesheet defines. A literal copied from it shows the right color today and the wrong one after the next token change.
|
|
69
|
-
- Edit a frame by rewriting its file. The file is the frame, and nothing else holds a copy of its markup.
|
|
70
|
-
|
|
71
|
-
## House style
|
|
72
|
-
|
|
73
|
-
Applies to an explanation frame only. A mockup of the project's interface renders at the app's own sizes and follows the project's design document instead.
|
|
74
|
-
|
|
75
|
-
- Write text in sentence case. Use no all-caps text and no `text-transform: uppercase`.
|
|
76
|
-
- Keep lines short, one fact a line.
|
|
77
|
-
- Set body text at 20px or larger and headings at 44px or larger. Set nothing under 16px.
|
|
78
|
-
- Render an explanation frame about 1100px wide.
|
|
79
|
-
|
|
80
|
-
### Colors, faces, and fit
|
|
81
|
-
|
|
82
|
-
Take colors from the project's custom properties. A frame defining colors of its own sets the dark values on `:root` and the light ones under `[data-theme="light"]`, since the shell sets `data-theme` on each frame's root.
|
|
83
|
-
|
|
84
|
-
- Name only the faces the canvas serves into every frame, being `Geist Variable` for text, `Noto Sans Mono` for code, and `Excalifont` for a hand-drawn look
|
|
85
|
-
- Those render the same in a capture and in any browser, so a height fitted against them holds
|
|
86
|
-
- Make each box tall enough to show all its content, with no scrollbar and no clipped line
|
|
87
|
-
- Run `canon canvas fit <page>/<frame>` rather than estimating, since it measures the content in the capture browser and sets the height
|
|
88
|
-
- Read frames left to right in reading order. Let `frame add` place each frame after the last with the standard gap, and move one only when the operator asks for a position.
|
|
89
|
-
|
|
90
|
-
## Layout
|
|
91
|
-
|
|
92
|
-
- Keep one `layout.json` per page, beside its frames. It holds each frame's box and nothing else, so moving a frame never rewrites its markup.
|
|
93
|
-
- Key each box by the frame's name, under one top-level `frames` object.
|
|
94
|
-
- Give every box four finite numbers in pixels: `x` and `y` for its top-left corner on the page, `width` for the width the frame renders at, and `height` for the height it shows on the canvas.
|
|
95
|
-
|
|
96
|
-
### Missing boxes and writes
|
|
97
|
-
|
|
98
|
-
The layout file is shared state, so a write never discards a box another writer or reader holds.
|
|
99
|
-
|
|
100
|
-
- Leave a frame out of the layout only when its position does not matter yet. A reader places an unnamed frame in a row after the others, at the default size of 1440 by 900.
|
|
101
|
-
- Never write the file while a writer holds its lock, which is the same name with `.lock` appended. Two writers each replacing the whole file lose one of the two edits.
|
|
102
|
-
- Treat a layout that does not parse as a defect to repair, never as an empty one to overwrite. Writing over it erases every box it held.
|
|
103
|
-
|
|
104
|
-
The keys below are the ones a reader parses. The frame names are placeholders.
|
|
105
|
-
|
|
106
|
-
```json
|
|
107
|
-
{
|
|
108
|
-
"frames": {
|
|
109
|
-
"hero-a": { "x": 0, "y": 0, "width": 1440, "height": 900 },
|
|
110
|
-
"hero-b": { "x": 1520, "y": 0, "width": 1440, "height": 900 }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Tokens
|
|
116
|
-
|
|
117
|
-
- Expect the token stylesheet first in every frame's `head` when it is served, ahead of anything the frame links. A stylesheet the frame links itself still wins the cascade.
|
|
118
|
-
- Read the tokens from the canvas tool's own module inside its own checkout, else the project's installed base stylesheet plus its project override folder, else none
|
|
119
|
-
- A canvas with no token source renders unstyled and says so
|
|
120
|
-
- Never paste the token stylesheet into a frame. The injected copy follows the project, and a pasted one stops following it the moment it is written.
|
|
121
|
-
|
|
122
|
-
## What a frame must not assume
|
|
123
|
-
|
|
124
|
-
- Do not assume a viewport. A frame renders at its layout width on the canvas and in a capture alike, so size it with media queries against that width rather than against the operator's window.
|
|
125
|
-
- Do not assume the theme. The canvas theme toggle is the operator's own state, and a capture shows each frame in its default theme.
|
|
126
|
-
- Do not assume a network. Name a served face, or ship the font beside the frame, since a capture refuses a frame naming a font it cannot find.
|
|
127
|
-
|
|
128
|
-
### Font, source, and checkout
|
|
129
|
-
|
|
130
|
-
Do not assume a default font. Set `font-family` with a served face first on an element the frame's text inherits from, since a capture checks the first family of the text it renders and the browser's default serif is absent from many machines.
|
|
131
|
-
|
|
132
|
-
- A frame naming a machine family such as `DejaVu Sans` renders at another height in another browser, which `canon canvas fit` reports
|
|
133
|
-
- Do not assume the frame is the source. A picked direction is carried into the project's design document or a plan, and the operator clears the frame once it has been carried.
|
|
134
|
-
- Do not assume the canvas survives the checkout. The folder is gitignored, so a lost checkout loses every page in it.
|
|
135
|
-
|
|
136
|
-
## Template
|
|
137
|
-
|
|
138
|
-
A new frame starts from this skeleton. The names in it are placeholders.
|
|
139
|
-
|
|
140
|
-
```html
|
|
141
|
-
<!doctype html>
|
|
142
|
-
<html lang="en">
|
|
143
|
-
<head>
|
|
144
|
-
<meta charset="utf-8" />
|
|
145
|
-
<title>frame-name</title>
|
|
146
|
-
<style>
|
|
147
|
-
html {
|
|
148
|
-
font-family: installed-family, sans-serif;
|
|
149
|
-
}
|
|
150
|
-
main {
|
|
151
|
-
padding: var(--space-lg);
|
|
152
|
-
color: var(--color-text);
|
|
153
|
-
background: var(--color-background);
|
|
154
|
-
}
|
|
155
|
-
/* only a color the frame defines itself: dark on :root, light here */
|
|
156
|
-
:root {
|
|
157
|
-
--frame-accent: #7aa2ff;
|
|
158
|
-
}
|
|
159
|
-
[data-theme='light'] {
|
|
160
|
-
--frame-accent: #1f4fd8;
|
|
161
|
-
}
|
|
162
|
-
</style>
|
|
163
|
-
</head>
|
|
164
|
-
<body>
|
|
165
|
-
<main>
|
|
166
|
-
<h1>frame-name</h1>
|
|
167
|
-
</main>
|
|
168
|
-
</body>
|
|
169
|
-
</html>
|
|
170
|
-
```
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Route edits to canvas frames under .canon/canvas/ to the canvas-board skill for the house style, and keep frame text and box height readable
|
|
3
|
-
paths:
|
|
4
|
-
- '.canon/canvas/**'
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Canvas standards
|
|
8
|
-
|
|
9
|
-
## Frames
|
|
10
|
-
|
|
11
|
-
- Load the `canon:canvas-board` skill before drawing or editing a frame. It carries the house style for an explanation frame and the content format. Report it rather than proceeding silently when the skill does not resolve.
|
|
12
|
-
- Write explanation frame text in sentence case. A mockup of the project's interface follows its design document instead.
|
|
13
|
-
- Size each explanation frame's box to show all its content with no scrollbar.
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
SCRIPTS_ROOT="$PROJECT_ROOT/scripts"
|
|
9
|
-
SANDBOX_ROOT="$PROJECT_ROOT/sandbox"
|
|
10
|
-
PALETTE_SOURCE="$SCRIPTS_ROOT/lib/ui.sh"
|
|
11
|
-
|
|
12
|
-
# `find` writes to stderr and returns non-zero for a missing root, inside a
|
|
13
|
-
# process substitution whose status nothing reads. Without this the walk covers
|
|
14
|
-
# nothing and the check reports the tree clean having never measured it.
|
|
15
|
-
if [ ! -f "$PALETTE_SOURCE" ]; then
|
|
16
|
-
echo "No palette source at ${PALETTE_SOURCE#"$PROJECT_ROOT/"}, color sourcing unverifiable." >&2
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
# The same silence follows a root that moved, so a walked root that is gone
|
|
21
|
-
# fails here rather than shrinking the corpus with no message.
|
|
22
|
-
for root in "$SCRIPTS_ROOT" "$SANDBOX_ROOT"; do
|
|
23
|
-
if [ ! -d "$root" ]; then
|
|
24
|
-
echo "No shell root at ${root#"$PROJECT_ROOT/"}, color sourcing unverifiable." >&2
|
|
25
|
-
exit 1
|
|
26
|
-
fi
|
|
27
|
-
done
|
|
28
|
-
|
|
29
|
-
# Any SGR sequence in either spelling. The cursor and erase sequences an
|
|
30
|
-
# interactive prompt writes end in a letter instead, and they run only where a
|
|
31
|
-
# terminal already exists, so they are not what this measures.
|
|
32
|
-
SGR='\\(033|e|x1b)\[[0-9;]*m'
|
|
33
|
-
|
|
34
|
-
# Sixteen copies of one decision is what made color impossible to turn off on
|
|
35
|
-
# the TypeScript side. A rule nothing enforces rebuilds them the next time a
|
|
36
|
-
# script wants grey, so the escape lives in the shared library and every other
|
|
37
|
-
# script reads the palette that library derives for its stream.
|
|
38
|
-
offenders=""
|
|
39
|
-
while IFS= read -r file; do
|
|
40
|
-
[ "$file" = "$PALETTE_SOURCE" ] && continue
|
|
41
|
-
if grep -qE "$SGR" "$file"; then
|
|
42
|
-
offenders="$offenders ${file#"$PROJECT_ROOT/"}"$'\n'
|
|
43
|
-
fi
|
|
44
|
-
done < <(find "$SCRIPTS_ROOT" "$SANDBOX_ROOT" -type f -name '*.sh')
|
|
45
|
-
|
|
46
|
-
if [ -n "$offenders" ]; then
|
|
47
|
-
echo "A color escape is defined outside the shared library:" >&2
|
|
48
|
-
printf '%s' "$offenders" >&2
|
|
49
|
-
echo "Source scripts/lib/ui.sh and read its palette, so one answer covers every writer." >&2
|
|
50
|
-
exit 1
|
|
51
|
-
fi
|
package/src/canvas/address.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The element address: a frame plus the element's index in document order.
|
|
3
|
-
* The shell counts it over the frame's parsed DOM and the server counts it over
|
|
4
|
-
* the file through `HTMLRewriter`, so the two have to count the same elements.
|
|
5
|
-
* This module imports nothing, which keeps it bundling into the shell, and the
|
|
6
|
-
* server half reaches `HTMLRewriter` as a Bun global only when called.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Marks the token stylesheet the server injects, which the file never holds. */
|
|
10
|
-
export const TOKENS_ATTRIBUTE = 'data-canvas-tokens'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Marks a document wrapper the server writes because the file leaves it to the
|
|
14
|
-
* browser to imply, so the shell counts only what the file states.
|
|
15
|
-
*/
|
|
16
|
-
export const IMPLIED_ATTRIBUTE = 'data-canvas-implied'
|
|
17
|
-
|
|
18
|
-
/** On that same element, the hash of the file the frame was served from. */
|
|
19
|
-
export const HASH_ATTRIBUTE = 'data-canvas-hash'
|
|
20
|
-
|
|
21
|
-
const EXCERPT_LENGTH = 80
|
|
22
|
-
|
|
23
|
-
/** Elements whose content is raw text, which a browser never parses as markup. */
|
|
24
|
-
const RAW_TEXT = new Set(['script', 'style', 'template', 'textarea', 'title'])
|
|
25
|
-
|
|
26
|
-
export interface ElementAddress {
|
|
27
|
-
readonly index: number
|
|
28
|
-
readonly tag: string
|
|
29
|
-
/**
|
|
30
|
-
* Every element the shell counted. A browser implies elements the file never
|
|
31
|
-
* states, such as a `tbody`, and each one shifts the count, so a total that
|
|
32
|
-
* differs from the server's is what exposes it. The server marks an implied
|
|
33
|
-
* document wrapper itself, so only the marked ones leave the count.
|
|
34
|
-
*/
|
|
35
|
-
readonly count: number
|
|
36
|
-
/**
|
|
37
|
-
* The served file's hash, so the server can refuse a pick made against a
|
|
38
|
-
* version of the frame the file has since moved past.
|
|
39
|
-
*/
|
|
40
|
-
readonly hash?: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface SourceElement {
|
|
44
|
-
readonly tag: string
|
|
45
|
-
readonly classes: readonly string[]
|
|
46
|
-
readonly text: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type AddressCheck =
|
|
50
|
-
| { readonly ok: true; readonly element: SourceElement }
|
|
51
|
-
| {
|
|
52
|
-
readonly ok: false
|
|
53
|
-
readonly reason: 'address-mismatch'
|
|
54
|
-
readonly detail: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function excerpt(text: string): string {
|
|
58
|
-
return text.replace(/\s+/g, ' ').trim().slice(0, EXCERPT_LENGTH)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function documentElements(doc: Document): Element[] {
|
|
62
|
-
return [...doc.querySelectorAll('*')].filter(
|
|
63
|
-
(element) =>
|
|
64
|
-
!element.hasAttribute(TOKENS_ATTRIBUTE) &&
|
|
65
|
-
!element.hasAttribute(IMPLIED_ATTRIBUTE),
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function addressOf(
|
|
70
|
-
doc: Document,
|
|
71
|
-
element: Element,
|
|
72
|
-
): ElementAddress | undefined {
|
|
73
|
-
const all = documentElements(doc)
|
|
74
|
-
const index = all.indexOf(element)
|
|
75
|
-
if (index === -1) return undefined
|
|
76
|
-
const address = {
|
|
77
|
-
index,
|
|
78
|
-
tag: element.tagName.toLowerCase(),
|
|
79
|
-
count: all.length,
|
|
80
|
-
}
|
|
81
|
-
const hash = doc
|
|
82
|
-
.querySelector(`[${TOKENS_ATTRIBUTE}]`)
|
|
83
|
-
?.getAttribute(HASH_ATTRIBUTE)
|
|
84
|
-
return hash ? { ...address, hash } : address
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** The element an address names, or nothing once the tag there differs. */
|
|
88
|
-
export function elementAt(
|
|
89
|
-
doc: Document,
|
|
90
|
-
address: Pick<ElementAddress, 'index' | 'tag'>,
|
|
91
|
-
): Element | undefined {
|
|
92
|
-
const element = documentElements(doc)[address.index]
|
|
93
|
-
return element?.tagName.toLowerCase() === address.tag ? element : undefined
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface Collected {
|
|
97
|
-
readonly tag: string
|
|
98
|
-
readonly classes: readonly string[]
|
|
99
|
-
text: string
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Called on each element the address counts, with its index. What it returns
|
|
104
|
-
* runs at the element's end tag, since a second `onEndTag` on one element
|
|
105
|
-
* replaces the first rather than adding to it.
|
|
106
|
-
*/
|
|
107
|
-
export type ElementVisitor = (
|
|
108
|
-
element: HTMLRewriterTypes.Element,
|
|
109
|
-
index: number,
|
|
110
|
-
) => (() => void) | undefined
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Registers the one counting rule both the reader and the edit writer follow,
|
|
114
|
-
* so an index means the same element to each.
|
|
115
|
-
*/
|
|
116
|
-
export function countElements(
|
|
117
|
-
rewriter: HTMLRewriter,
|
|
118
|
-
visit: ElementVisitor,
|
|
119
|
-
): HTMLRewriter {
|
|
120
|
-
let count = 0
|
|
121
|
-
/* A template's content is a fragment `querySelectorAll` never reaches. */
|
|
122
|
-
let templateDepth = 0
|
|
123
|
-
|
|
124
|
-
return rewriter.on('*', {
|
|
125
|
-
element(element) {
|
|
126
|
-
const tag = element.tagName.toLowerCase()
|
|
127
|
-
if (templateDepth > 0) {
|
|
128
|
-
if (tag === 'template') {
|
|
129
|
-
templateDepth += 1
|
|
130
|
-
element.onEndTag(() => {
|
|
131
|
-
templateDepth -= 1
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
const end = visit(element, count)
|
|
137
|
-
count += 1
|
|
138
|
-
if (!element.canHaveContent || element.selfClosing) return
|
|
139
|
-
const isTemplate = tag === 'template'
|
|
140
|
-
if (isTemplate) templateDepth += 1
|
|
141
|
-
element.onEndTag(() => {
|
|
142
|
-
if (isTemplate) templateDepth -= 1
|
|
143
|
-
end?.()
|
|
144
|
-
})
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Whether an element's content is raw text rather than parsed markup. */
|
|
150
|
-
export function isRawText(tag: string): boolean {
|
|
151
|
-
return RAW_TEXT.has(tag)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Every element the file states, in document order. Text is gathered into
|
|
156
|
-
* each open element, which an element closed only by implication keeps open to
|
|
157
|
-
* the end, so its excerpt can run past where a browser would end it.
|
|
158
|
-
*/
|
|
159
|
-
export function sourceElements(html: string): SourceElement[] {
|
|
160
|
-
const elements: Collected[] = []
|
|
161
|
-
const open: number[] = []
|
|
162
|
-
let rawDepth = 0
|
|
163
|
-
|
|
164
|
-
const appendText = (text: string) => {
|
|
165
|
-
for (const index of open) {
|
|
166
|
-
const element = elements[index]
|
|
167
|
-
if (element && element.text.length < EXCERPT_LENGTH * 4) {
|
|
168
|
-
element.text += text
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
countElements(new HTMLRewriter(), (element, index) => {
|
|
174
|
-
const tag = element.tagName.toLowerCase()
|
|
175
|
-
/* A tag boundary separates words the way a rendered block would. */
|
|
176
|
-
appendText(' ')
|
|
177
|
-
elements.push({
|
|
178
|
-
tag,
|
|
179
|
-
classes: (element.getAttribute('class') ?? '')
|
|
180
|
-
.split(/\s+/)
|
|
181
|
-
.filter(Boolean),
|
|
182
|
-
text: '',
|
|
183
|
-
})
|
|
184
|
-
if (!element.canHaveContent || element.selfClosing) return undefined
|
|
185
|
-
const isRaw = isRawText(tag)
|
|
186
|
-
open.push(index)
|
|
187
|
-
if (isRaw) rawDepth += 1
|
|
188
|
-
return () => {
|
|
189
|
-
const at = open.lastIndexOf(index)
|
|
190
|
-
if (at !== -1) open.splice(at, 1)
|
|
191
|
-
if (isRaw) rawDepth -= 1
|
|
192
|
-
appendText(' ')
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
.onDocument({
|
|
196
|
-
text(chunk) {
|
|
197
|
-
if (rawDepth === 0) appendText(chunk.text)
|
|
198
|
-
},
|
|
199
|
-
})
|
|
200
|
-
.transform(html)
|
|
201
|
-
|
|
202
|
-
return elements.map(({ tag, classes, text }) => ({
|
|
203
|
-
tag,
|
|
204
|
-
classes,
|
|
205
|
-
text: excerpt(text),
|
|
206
|
-
}))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Finds the element the shell addressed in the file, and refuses when the two
|
|
211
|
-
* counts disagree rather than naming whatever sits at that index on disk.
|
|
212
|
-
*/
|
|
213
|
-
export function resolveAddress(
|
|
214
|
-
html: string,
|
|
215
|
-
address: ElementAddress,
|
|
216
|
-
): AddressCheck {
|
|
217
|
-
const elements = sourceElements(html)
|
|
218
|
-
const element = elements[address.index]
|
|
219
|
-
if (elements.length !== address.count || element?.tag !== address.tag) {
|
|
220
|
-
return {
|
|
221
|
-
ok: false,
|
|
222
|
-
reason: 'address-mismatch',
|
|
223
|
-
detail: `the browser counted ${address.count} elements and the file states ${elements.length}, so element ${address.index} cannot be matched`,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return { ok: true, element }
|
|
227
|
-
}
|