@erclx/canon 5.14.1 → 5.16.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.
Files changed (77) hide show
  1. package/README.md +3 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
  4. package/claude/skills/draft-screencast/SKILL.md +14 -9
  5. package/claude/skills/draft-wiki/SKILL.md +1 -1
  6. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  7. package/claude/skills/read-frames/SKILL.md +3 -2
  8. package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
  9. package/claude/skills/record-screencast/SKILL.md +28 -5
  10. package/claude/skills/search-craft/REQUIREMENT.md +57 -0
  11. package/claude/skills/search-craft/SKILL.md +83 -0
  12. package/docs/agents/canvas.md +36 -0
  13. package/docs/agents/commands.md +3 -0
  14. package/docs/agents/demo.md +34 -7
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/migrate.md +1 -1
  17. package/docs/agents/sandbox.md +25 -0
  18. package/docs/target/migrations.md +1 -1
  19. package/docs/workflow/ai-workflow.md +2 -3
  20. package/docs/workflow/skill-map.md +2 -1
  21. package/governance/renames.toml +7 -5
  22. package/governance/rules/canon/630-requirements.md +1 -1
  23. package/package.json +5 -2
  24. package/scripts/lib/sandbox-git.sh +2 -2
  25. package/src/canvas/address.ts +184 -0
  26. package/src/canvas/capture.ts +172 -0
  27. package/src/canvas/client/app.tsx +87 -0
  28. package/src/canvas/client/inspector.tsx +121 -0
  29. package/src/canvas/client/layers.tsx +118 -0
  30. package/src/canvas/client/main.tsx +62 -0
  31. package/src/canvas/client/pages-panel.tsx +157 -0
  32. package/src/canvas/client/shell.css +449 -0
  33. package/src/canvas/client/state.ts +372 -0
  34. package/src/canvas/client/surface.tsx +465 -0
  35. package/src/canvas/content.ts +617 -0
  36. package/src/canvas/index.html +14 -0
  37. package/src/canvas/server.ts +471 -0
  38. package/src/canvas/tokens.ts +71 -0
  39. package/src/claude/cases/misc.ts +5 -0
  40. package/src/claude/seeds.ts +0 -1
  41. package/src/cli.ts +2 -0
  42. package/src/commands/canvas.ts +522 -0
  43. package/src/commands/claude.ts +9 -14
  44. package/src/commands/demo.ts +6 -6
  45. package/src/commands/migrate.ts +1 -1
  46. package/src/commands/sandbox.ts +108 -0
  47. package/src/context/classify/prompts.ts +1 -1
  48. package/src/context/folders.ts +0 -1
  49. package/src/demo/compile.ts +8 -4
  50. package/src/demo/container.ts +1 -1
  51. package/src/gov/renames.ts +3 -2
  52. package/src/help.ts +1 -0
  53. package/src/migrate/rename.ts +5 -4
  54. package/src/record-root.ts +1 -0
  55. package/src/sandbox/equivalence.ts +847 -0
  56. package/src/serve/static.ts +25 -11
  57. package/src/surface-root.ts +0 -1
  58. package/src/teach/browser/compile.ts +44 -0
  59. package/src/teach/browser/dismiss.ts +76 -0
  60. package/src/teach/browser/focus-line.ts +19 -0
  61. package/src/teach/browser/glossary-filter.ts +44 -0
  62. package/src/teach/browser/head.ts +30 -0
  63. package/src/teach/browser/quiz.ts +19 -0
  64. package/src/teach/browser/sidebar.ts +233 -0
  65. package/src/teach/browser/theme.ts +18 -0
  66. package/src/teach/nav.ts +19 -374
  67. package/standards/architecture.md +1 -1
  68. package/standards/context.md +1 -1
  69. package/standards/design.md +1 -1
  70. package/standards/index.md +0 -1
  71. package/standards/requirements.md +4 -6
  72. package/standards/wireframes.md +2 -2
  73. package/tooling/claude/reference.md +0 -1
  74. package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
  75. package/governance/rules/canon/645-decisions.md +0 -12
  76. package/standards/decisions.md +0 -105
  77. package/tooling/claude/seeds/canon/decisions/index.md +0 -8
@@ -0,0 +1,118 @@
1
+ /** @jsxImportSource preact */
2
+ import type { JSX } from 'preact'
3
+ import { addressOf, documentElements, excerpt } from '@/canvas/address'
4
+ import {
5
+ type FrameRef,
6
+ frameDocuments,
7
+ hoveredElement,
8
+ selectElement,
9
+ selection,
10
+ } from '@/canvas/client/state'
11
+
12
+ /** Elements that never render, so a layer for one points at nothing. */
13
+ const HIDDEN = new Set(['script', 'style', 'template', 'noscript'])
14
+
15
+ const LABEL_TEXT = 24
16
+
17
+ export interface LayersProps {
18
+ readonly frame: FrameRef
19
+ /** The frame's key, which its loaded document is registered under. */
20
+ readonly frameKey: string
21
+ }
22
+
23
+ /** `tag.first-class`, then a short excerpt of a leaf's own text. */
24
+ function layerLabel(element: Element): { name: string; text: string } {
25
+ const tag = element.tagName.toLowerCase()
26
+ const first = element.classList[0]
27
+ const text =
28
+ element.children.length === 0
29
+ ? excerpt(element.textContent ?? '').slice(0, LABEL_TEXT)
30
+ : ''
31
+ return { name: first ? `${tag}.${first}` : tag, text }
32
+ }
33
+
34
+ function LayerNode({
35
+ doc,
36
+ element,
37
+ indexes,
38
+ props,
39
+ }: {
40
+ readonly doc: Document
41
+ readonly element: Element
42
+ readonly indexes: ReadonlyMap<Element, number>
43
+ readonly props: LayersProps
44
+ }): JSX.Element {
45
+ const index = indexes.get(element) ?? -1
46
+ const chosen = selection.value
47
+ const isCurrent =
48
+ chosen?.page === props.frame.page &&
49
+ chosen.frame === props.frame.frame &&
50
+ chosen.element?.index === index &&
51
+ !chosen.element.stale
52
+ const { name, text } = layerLabel(element)
53
+ const children = [...element.children].filter(
54
+ (child) => !HIDDEN.has(child.tagName.toLowerCase()),
55
+ )
56
+
57
+ return (
58
+ <li>
59
+ <button
60
+ type="button"
61
+ class="row layer"
62
+ title={text ? `${name} ${text}` : name}
63
+ aria-current={isCurrent ? 'true' : undefined}
64
+ onClick={() => {
65
+ const address = addressOf(doc, element)
66
+ if (address) void selectElement(props.frame, props.frameKey, address)
67
+ }}
68
+ onMouseEnter={() => {
69
+ hoveredElement.value = { key: props.frameKey, index }
70
+ }}
71
+ onMouseLeave={() => {
72
+ hoveredElement.value = undefined
73
+ }}
74
+ >
75
+ <span class="row-label">{name}</span>
76
+ {text ? <span class="row-meta layer-text">{text}</span> : null}
77
+ </button>
78
+ {children.length > 0 ? (
79
+ <ul class="layers">
80
+ {children.map((child, at) => (
81
+ <LayerNode
82
+ key={at}
83
+ doc={doc}
84
+ element={child}
85
+ indexes={indexes}
86
+ props={props}
87
+ />
88
+ ))}
89
+ </ul>
90
+ ) : null}
91
+ </li>
92
+ )
93
+ }
94
+
95
+ /**
96
+ * The element tree of one frame, read from its loaded document, so it shows
97
+ * what the browser built rather than what the file states. Each row selects by
98
+ * the same address a click on the surface records.
99
+ */
100
+ export function Layers(props: LayersProps): JSX.Element {
101
+ const doc = frameDocuments.value.get(props.frameKey)
102
+ const label = `Layers of ${props.frame.frame}`
103
+ if (!doc?.body) {
104
+ return (
105
+ <p class="empty" aria-label={label}>
106
+ Loading layers
107
+ </p>
108
+ )
109
+ }
110
+ const indexes = new Map(
111
+ documentElements(doc).map((element, index) => [element, index]),
112
+ )
113
+ return (
114
+ <ul class="layers layers-root" aria-label={label}>
115
+ <LayerNode doc={doc} element={doc.body} indexes={indexes} props={props} />
116
+ </ul>
117
+ )
118
+ }
@@ -0,0 +1,62 @@
1
+ /** @jsxImportSource preact */
2
+ import { effect } from '@preact/signals'
3
+ import { render } from 'preact'
4
+ import { App } from '@/canvas/client/app'
5
+ import {
6
+ applyChange,
7
+ type ChangeEvent,
8
+ loadPages,
9
+ systemTheme,
10
+ theme,
11
+ themeChoice,
12
+ type Theme,
13
+ } from '@/canvas/client/state'
14
+
15
+ const THEME_KEY = 'canon-canvas-theme'
16
+
17
+ function readStoredTheme(): Theme | undefined {
18
+ try {
19
+ const stored = localStorage.getItem(THEME_KEY)
20
+ return stored === 'light' || stored === 'dark' ? stored : undefined
21
+ } catch {
22
+ return undefined
23
+ }
24
+ }
25
+
26
+ function storeTheme(value: Theme | undefined): void {
27
+ try {
28
+ if (value === undefined) localStorage.removeItem(THEME_KEY)
29
+ else localStorage.setItem(THEME_KEY, value)
30
+ } catch {
31
+ /* A blocked store costs the remembered pick, nothing else. */
32
+ }
33
+ }
34
+
35
+ const darkQuery = matchMedia('(prefers-color-scheme: dark)')
36
+ systemTheme.value = darkQuery.matches ? 'dark' : 'light'
37
+ darkQuery.addEventListener('change', (event) => {
38
+ systemTheme.value = event.matches ? 'dark' : 'light'
39
+ })
40
+ themeChoice.value = readStoredTheme()
41
+
42
+ effect(() => {
43
+ document.documentElement.dataset.theme = theme.value
44
+ document.documentElement.style.colorScheme = theme.value
45
+ })
46
+ effect(() => storeTheme(themeChoice.value))
47
+
48
+ /* The chrome reads this toolkit's own tokens, served beside the shell. */
49
+ const chrome = document.createElement('link')
50
+ chrome.rel = 'stylesheet'
51
+ chrome.href = '/api/chrome.css'
52
+ document.head.prepend(chrome)
53
+
54
+ const events = new EventSource('/api/events')
55
+ events.addEventListener('message', (message) => {
56
+ void applyChange(JSON.parse(message.data) as ChangeEvent)
57
+ })
58
+
59
+ void loadPages()
60
+
61
+ const mount = document.getElementById('app')
62
+ if (mount) render(<App />, mount)
@@ -0,0 +1,157 @@
1
+ /** @jsxImportSource preact */
2
+ import type { JSX } from 'preact'
3
+ import { Layers } from '@/canvas/client/layers'
4
+ import type { Frame } from '@/canvas/content'
5
+ import {
6
+ currentPage,
7
+ expandedFrames,
8
+ frameKey,
9
+ isLoaded,
10
+ loadError,
11
+ pages,
12
+ selectedFrame,
13
+ selectedPage,
14
+ selectFrame,
15
+ theme,
16
+ toggleLayers,
17
+ toggleTheme,
18
+ } from '@/canvas/client/state'
19
+
20
+ export interface PagesPanelProps {
21
+ /** Brings a frame into view on the surface. */
22
+ readonly onFocusFrame: (frame: Frame) => void
23
+ }
24
+
25
+ function ThemeToggle(): JSX.Element {
26
+ const next = theme.value === 'dark' ? 'light' : 'dark'
27
+ return (
28
+ <button
29
+ type="button"
30
+ class="icon-button"
31
+ aria-label={`Switch to ${next} theme`}
32
+ title={`Switch to ${next} theme`}
33
+ onClick={toggleTheme}
34
+ >
35
+ <span aria-hidden="true">{theme.value === 'dark' ? '☾' : '☀'}</span>
36
+ </button>
37
+ )
38
+ }
39
+
40
+ function PageList(): JSX.Element {
41
+ if (pages.value.length === 0) {
42
+ return (
43
+ <p class="empty">
44
+ No pages yet. Run <code>canon canvas page add &lt;name&gt;</code> to add
45
+ one.
46
+ </p>
47
+ )
48
+ }
49
+ const current = currentPage.value?.name
50
+ return (
51
+ <ul class="list" aria-label="Pages">
52
+ {pages.value.map((page) => (
53
+ <li key={page.name}>
54
+ <button
55
+ type="button"
56
+ class="row"
57
+ aria-current={page.name === current ? 'page' : undefined}
58
+ title={page.name}
59
+ onClick={() => {
60
+ selectedPage.value = page.name
61
+ }}
62
+ >
63
+ <span class="row-label">{page.name}</span>
64
+ <span class="row-meta">{page.frames.length}</span>
65
+ </button>
66
+ </li>
67
+ ))}
68
+ </ul>
69
+ )
70
+ }
71
+
72
+ function FrameList({ onFocusFrame }: PagesPanelProps): JSX.Element | null {
73
+ const page = currentPage.value
74
+ if (!page) return null
75
+ if (page.frames.length === 0) {
76
+ return (
77
+ <p class="empty">
78
+ No frames on this page. Run{' '}
79
+ <code>canon canvas frame add {page.name} &lt;name&gt;</code> to add one.
80
+ </p>
81
+ )
82
+ }
83
+ return (
84
+ <ul class="list" aria-label="Frames">
85
+ {page.frames.map((frame) => {
86
+ const key = frameKey(page.name, frame)
87
+ const isOpen = expandedFrames.value.has(key)
88
+ return (
89
+ <li key={frame.name}>
90
+ <div class="frame-row">
91
+ <button
92
+ type="button"
93
+ class="disclosure"
94
+ aria-expanded={isOpen}
95
+ aria-label={`${isOpen ? 'Hide' : 'Show'} layers of ${frame.name}`}
96
+ title={`${isOpen ? 'Hide' : 'Show'} layers`}
97
+ onClick={() => toggleLayers(key)}
98
+ >
99
+ <span aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
100
+ </button>
101
+ <button
102
+ type="button"
103
+ class="row"
104
+ title={frame.name}
105
+ aria-current={
106
+ frame.name === selectedFrame.value?.name ? 'true' : undefined
107
+ }
108
+ onClick={() => {
109
+ onFocusFrame(frame)
110
+ void selectFrame({ page: page.name, frame: frame.name })
111
+ }}
112
+ >
113
+ <span class="row-label">{frame.name}</span>
114
+ <span class="row-meta">{frame.width}</span>
115
+ </button>
116
+ </div>
117
+ {isOpen ? (
118
+ <Layers
119
+ frame={{ page: page.name, frame: frame.name }}
120
+ frameKey={key}
121
+ />
122
+ ) : null}
123
+ </li>
124
+ )
125
+ })}
126
+ </ul>
127
+ )
128
+ }
129
+
130
+ export function PagesPanel(props: PagesPanelProps): JSX.Element {
131
+ return (
132
+ <nav class="panel panel-left" aria-label="Pages and frames">
133
+ <header class="panel-head">
134
+ <span class="brand">Canvas</span>
135
+ <ThemeToggle />
136
+ </header>
137
+ {loadError.value ? (
138
+ <p class="empty" role="alert">
139
+ {loadError.value}
140
+ </p>
141
+ ) : !isLoaded.value ? (
142
+ <p class="empty">Loading</p>
143
+ ) : (
144
+ <>
145
+ <h2 class="section-label">Pages</h2>
146
+ <PageList />
147
+ {currentPage.value ? (
148
+ <>
149
+ <h2 class="section-label">Frames</h2>
150
+ <FrameList {...props} />
151
+ </>
152
+ ) : null}
153
+ </>
154
+ )}
155
+ </nav>
156
+ )
157
+ }