@erclx/canon 5.15.0 → 5.17.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 (55) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/draft-figure/REQUIREMENT.md +8 -2
  3. package/claude/skills/draft-figure/SKILL.md +19 -4
  4. package/claude/skills/draft-wiki/SKILL.md +1 -1
  5. package/claude/skills/draft-wireframes/SKILL.md +1 -1
  6. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  7. package/claude/skills/search-craft/REQUIREMENT.md +57 -0
  8. package/claude/skills/search-craft/SKILL.md +83 -0
  9. package/claude/skills/teach-workspace/SKILL.md +1 -1
  10. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  11. package/docs/agents/canvas.md +36 -0
  12. package/docs/agents/commands.md +3 -3
  13. package/docs/agents/demo.md +3 -1
  14. package/docs/agents/index.md +1 -0
  15. package/docs/agents/teach.md +2 -2
  16. package/docs/workflow/skill-map.md +1 -0
  17. package/docs/workflow/visual-design-workflow.md +8 -15
  18. package/governance/rules/claude/566-output.md +3 -5
  19. package/governance/rules/claude/575-hooks.md +1 -1
  20. package/package.json +1 -1
  21. package/scripts/lib/sandbox-git.sh +2 -2
  22. package/src/canvas/address.ts +184 -0
  23. package/src/canvas/capture.ts +172 -0
  24. package/src/canvas/client/app.tsx +2 -0
  25. package/src/canvas/client/inspector.tsx +121 -0
  26. package/src/canvas/client/layers.tsx +118 -0
  27. package/src/canvas/client/pages-panel.tsx +47 -13
  28. package/src/canvas/client/shell.css +131 -1
  29. package/src/canvas/client/state.ts +186 -0
  30. package/src/canvas/client/surface.tsx +272 -20
  31. package/src/canvas/content.ts +307 -16
  32. package/src/canvas/server.ts +182 -13
  33. package/src/claude/cases/misc.ts +5 -0
  34. package/src/commands/canvas.ts +200 -3
  35. package/src/commands/demo.ts +2 -0
  36. package/src/commands/migrate.ts +1 -1
  37. package/src/commands/teach.ts +3 -1
  38. package/src/demo/drive.ts +36 -7
  39. package/src/demo/timeline.ts +64 -0
  40. package/src/migrate/rename.ts +5 -4
  41. package/src/teach/browser/compile.ts +44 -0
  42. package/src/teach/browser/dismiss.ts +76 -0
  43. package/src/teach/browser/focus-line.ts +19 -0
  44. package/src/teach/browser/glossary-filter.ts +44 -0
  45. package/src/teach/browser/head.ts +30 -0
  46. package/src/teach/browser/quiz.ts +19 -0
  47. package/src/teach/browser/sidebar.ts +233 -0
  48. package/src/teach/browser/theme.ts +18 -0
  49. package/src/teach/nav.ts +19 -374
  50. package/src/teach/workspace.ts +61 -6
  51. package/standards/figures.md +1 -0
  52. package/tooling/claude/reference.md +1 -1
  53. package/tooling/claude/seeds/.claude/hooks/path-form.sh +2 -4
  54. package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +35 -0
  55. package/tooling/claude/seeds/.claude/settings.json +9 -0
@@ -0,0 +1,44 @@
1
+ export function glossaryFilter(): void {
2
+ var input = document.getElementById('gfilter') as HTMLInputElement | null
3
+ var list = document.getElementById('gloss')
4
+ var count = document.getElementById('gloss-count')
5
+ if (!input || !list) return
6
+ var field = input
7
+ var terms = list
8
+ function updateGroups() {
9
+ terms.querySelectorAll('.gloss-group').forEach(function (heading) {
10
+ var el = heading.nextElementSibling as HTMLElement | null
11
+ var any = false
12
+ while (el && !el.classList.contains('gloss-group')) {
13
+ if (el.style.display !== 'none') any = true
14
+ el = el.nextElementSibling as HTMLElement | null
15
+ }
16
+ ;(heading as HTMLElement).style.display = any ? '' : 'none'
17
+ })
18
+ }
19
+ field.addEventListener('input', function () {
20
+ var q = field.value.toLowerCase()
21
+ var n = 0
22
+ terms.querySelectorAll<HTMLElement>('.gterm').forEach(function (entry) {
23
+ var match = (entry.textContent as string).toLowerCase().includes(q)
24
+ entry.style.display = match ? '' : 'none'
25
+ if (match) n++
26
+ })
27
+ terms.classList.toggle('none', n === 0)
28
+ updateGroups()
29
+ if (count) {
30
+ var total = terms.querySelectorAll('.gterm').length
31
+ count.textContent =
32
+ (n === total ? total : n + ' of ' + total) +
33
+ (total === 1 ? ' term' : ' terms')
34
+ }
35
+ })
36
+ var clear = terms.querySelector('.clear')
37
+ if (clear) {
38
+ clear.addEventListener('click', function () {
39
+ field.value = ''
40
+ field.dispatchEvent(new Event('input'))
41
+ field.focus()
42
+ })
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The panel's state, settled before the first paint rather than after it. An
3
+ * arm that set the narrow default afterwards slid the panel in and back out on
4
+ * every load, and a restored custom width animated in from the default.
5
+ *
6
+ * A listing page starts shut because the body already lists what the panel
7
+ * would, and a lesson starts open because there the panel is the only
8
+ * cross-lesson navigation on the page. A stored preference beats both. A count
9
+ * threshold on its own was built and reverted, since it hides the panel on a
10
+ * lesson too.
11
+ */
12
+ export function head(page: 'index' | 'lesson', breakpoint: number): void {
13
+ var r = document.documentElement
14
+ var s: string | null = null
15
+ r.dataset.page = page
16
+ if (matchMedia('(max-width: ' + breakpoint + 'px)').matches) {
17
+ r.classList.add('sb-shut')
18
+ return
19
+ }
20
+ try {
21
+ s = localStorage.getItem('teach-sb')
22
+ } catch (e) {}
23
+ var idx = r.dataset.page === 'index'
24
+ if (s === 'shut' || (s === null && idx)) r.classList.add('sb-shut')
25
+ var w: string | null = null
26
+ try {
27
+ w = localStorage.getItem('teach-sb-w')
28
+ } catch (e) {}
29
+ if (w) r.style.setProperty('--sb-w', w + 'px')
30
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What reveals feedback in a lesson written with button options. The radio
3
+ * shape needs none of it.
4
+ */
5
+ export function quiz(): void {
6
+ document.querySelectorAll<HTMLElement>('.q').forEach(function (q) {
7
+ var f = q.querySelector('.fb') as HTMLElement
8
+ q.querySelectorAll<HTMLElement>('.opt').forEach(function (b) {
9
+ b.addEventListener('click', function () {
10
+ if (f.classList.contains('show')) return
11
+ q.querySelectorAll<HTMLElement>('.opt').forEach(function (o) {
12
+ o.dataset.state =
13
+ o.dataset.a === '1' ? 'right' : o === b ? 'chosen' : 'wrong'
14
+ })
15
+ f.classList.add('show')
16
+ })
17
+ })
18
+ })
19
+ }
@@ -0,0 +1,233 @@
1
+ import { focusLine } from '@/teach/browser/focus-line'
2
+
3
+ /**
4
+ * `focusLine` is declared in the page ahead of this function by `compile`, so
5
+ * the call below reaches the one source the unit test exercises.
6
+ */
7
+ export function sidebar(breakpoint: number): void {
8
+ var root = document.documentElement
9
+ var found = document.querySelector<HTMLElement>('.sb')
10
+ if (!found) return
11
+ var panel = found
12
+
13
+ var fold = document.querySelector<HTMLElement>('.sb-fold')
14
+ var narrow = matchMedia('(max-width: ' + breakpoint + 'px)')
15
+
16
+ var FOCUSABLE =
17
+ "a[href], button:not([disabled]), summary, input, [tabindex]:not([tabindex='-1'])"
18
+
19
+ function panelStops() {
20
+ return Array.prototype.slice
21
+ .call(panel.querySelectorAll(FOCUSABLE))
22
+ .filter(function (el: HTMLElement) {
23
+ return el.offsetParent !== null || el === document.activeElement
24
+ }) as HTMLElement[]
25
+ }
26
+
27
+ /* The focus return belongs here rather than on the close control, because all
28
+ three routes out land here and only one of them used to move focus. The
29
+ shut panel takes visibility: hidden 180ms later, so a reader who pressed
30
+ Escape kept focus on a control that then disappeared under them and the
31
+ browser dropped them at the top of the document. */
32
+ function shut() {
33
+ var inside = panel.contains(document.activeElement)
34
+ root.classList.add('sb-shut')
35
+ try {
36
+ localStorage.setItem('teach-sb', 'shut')
37
+ } catch (e) {}
38
+ if (inside && fold) fold.focus()
39
+ }
40
+
41
+ if (fold)
42
+ fold.addEventListener('click', function () {
43
+ root.classList.toggle('sb-shut')
44
+ try {
45
+ localStorage.setItem(
46
+ 'teach-sb',
47
+ root.classList.contains('sb-shut') ? 'shut' : 'open',
48
+ )
49
+ } catch (e) {}
50
+ /* An overlay takes focus with it. Without this a keyboard reader opens the
51
+ panel and goes on tabbing through the lesson behind the scrim. */
52
+ if (!narrow.matches || root.classList.contains('sb-shut')) return
53
+ var first = panel.querySelector<HTMLElement>('a, button, summary, input')
54
+ if (first) first.focus()
55
+ })
56
+
57
+ var scrim = document.createElement('div')
58
+ scrim.className = 'sb-scrim'
59
+ document.body.appendChild(scrim)
60
+ scrim.addEventListener('click', shut)
61
+
62
+ document.addEventListener('keydown', function (e) {
63
+ if (e.key === 'Escape' && narrow.matches) {
64
+ shut()
65
+ return
66
+ }
67
+
68
+ /* The scrim says the lesson is unavailable, so Tab must not reach it.
69
+ Moving focus into the panel on open only defers this by however many
70
+ controls the panel holds. */
71
+ if (e.key !== 'Tab') return
72
+ if (!narrow.matches || root.classList.contains('sb-shut')) return
73
+
74
+ var stops = panelStops()
75
+ if (!stops.length) return
76
+
77
+ var first = stops[0]
78
+ var last = stops[stops.length - 1]
79
+
80
+ if (!panel.contains(document.activeElement)) {
81
+ ;(e.shiftKey ? last : first).focus()
82
+ e.preventDefault()
83
+ } else if (e.shiftKey && document.activeElement === first) {
84
+ last.focus()
85
+ e.preventDefault()
86
+ } else if (!e.shiftKey && document.activeElement === last) {
87
+ first.focus()
88
+ e.preventDefault()
89
+ }
90
+ })
91
+
92
+ /* The panel covers the masthead and the toggle that opened it, so it carries
93
+ its own way out rather than leaving the scrim as the only route. */
94
+ var close = document.createElement('button')
95
+ close.type = 'button'
96
+ close.className = 'sb-close'
97
+ close.setAttribute('aria-label', 'Close the course panel')
98
+ close.textContent = '×'
99
+ close.addEventListener('click', shut)
100
+ panel.appendChild(close)
101
+
102
+ var MIN = 208,
103
+ MAX = 296,
104
+ DEF = 256
105
+ var grip = document.querySelector<HTMLElement>('.sb-grip')
106
+ var lastWidth = parseInt(root.style.getPropertyValue('--sb-w'), 10) || DEF
107
+
108
+ function setWidth(px: number, save: boolean) {
109
+ var w = Math.max(MIN, Math.min(MAX, Math.round(px)))
110
+ root.style.setProperty('--sb-w', w + 'px')
111
+ lastWidth = w
112
+ if (save) {
113
+ try {
114
+ localStorage.setItem('teach-sb-w', String(w))
115
+ } catch (e) {}
116
+ }
117
+ }
118
+
119
+ if (grip) {
120
+ var handle = grip
121
+ var dragging = false
122
+ handle.addEventListener('pointerdown', function (e) {
123
+ dragging = true
124
+ handle.setPointerCapture(e.pointerId)
125
+ root.classList.add('sb-drag')
126
+ e.preventDefault()
127
+ })
128
+ handle.addEventListener('pointermove', function (e) {
129
+ if (dragging) setWidth(e.clientX, false)
130
+ })
131
+ handle.addEventListener('pointerup', function (e) {
132
+ if (!dragging) return
133
+ dragging = false
134
+ root.classList.remove('sb-drag')
135
+ handle.releasePointerCapture(e.pointerId)
136
+ setWidth(lastWidth, true)
137
+ })
138
+ handle.addEventListener('dblclick', function () {
139
+ setWidth(DEF, true)
140
+ })
141
+ handle.addEventListener('keydown', function (e) {
142
+ var step = e.shiftKey ? 32 : 8
143
+ if (e.key === 'ArrowLeft') {
144
+ setWidth(lastWidth - step, true)
145
+ e.preventDefault()
146
+ } else if (e.key === 'ArrowRight') {
147
+ setWidth(lastWidth + step, true)
148
+ e.preventDefault()
149
+ } else if (e.key === 'Home') {
150
+ setWidth(DEF, true)
151
+ e.preventDefault()
152
+ }
153
+ })
154
+ }
155
+
156
+ var filter = document.querySelector<HTMLInputElement>('.sb-filter input')
157
+ if (filter) {
158
+ var box = filter
159
+ box.addEventListener('input', function () {
160
+ var q = box.value.trim().toLowerCase()
161
+ document
162
+ .querySelectorAll<HTMLElement>('.sb-list > li')
163
+ .forEach(function (li) {
164
+ li.classList.toggle(
165
+ 'hide',
166
+ q !== '' &&
167
+ (li.textContent as string).toLowerCase().indexOf(q) === -1,
168
+ )
169
+ })
170
+ })
171
+ }
172
+
173
+ var slot = document.querySelector('.sb-out-slot')
174
+ var hs = Array.prototype.slice.call(
175
+ document.querySelectorAll('main h2'),
176
+ ) as HTMLElement[]
177
+ var links: HTMLElement[] = []
178
+
179
+ if (slot && hs.length) {
180
+ var list = document.createElement('ul')
181
+ list.className = 'sb-out'
182
+ hs.forEach(function (h, i) {
183
+ if (!h.id) h.id = 's' + i
184
+ var li = document.createElement('li')
185
+ var a = document.createElement('a')
186
+ a.href = '#' + h.id
187
+ a.textContent = (h.textContent as string).trim()
188
+ li.appendChild(a)
189
+ list.appendChild(li)
190
+ })
191
+ ;(slot.parentNode as Node).replaceChild(list, slot)
192
+ links = Array.prototype.slice.call(list.querySelectorAll('a'))
193
+ }
194
+
195
+ /* The bar reports position inside the lesson, which nothing reported before
196
+ the segmented track retired. The breadcrumb's last segment carries the
197
+ lesson title once the real one has scrolled off. */
198
+ var bar = document.querySelector<HTMLElement>('.bar')
199
+ var here = document.querySelector('.crumb-here')
200
+ var h1 = document.querySelector('main h1')
201
+ var counter = here ? (here.textContent as string).trim() : ''
202
+ var title = h1 ? (h1.textContent as string).trim() : ''
203
+
204
+ function sync() {
205
+ var max = document.documentElement.scrollHeight - innerHeight
206
+ if (bar) {
207
+ var pct = max > 0 ? Math.min(100, Math.max(0, (scrollY / max) * 100)) : 0
208
+ bar.style.setProperty('--read', pct.toFixed(1) + '%')
209
+ }
210
+
211
+ if (here && h1 && title && counter) {
212
+ var want = h1.getBoundingClientRect().bottom < 56 ? title : counter
213
+ if (here.textContent !== want) here.textContent = want
214
+ }
215
+
216
+ if (links.length) {
217
+ /* Nothing is marked until a heading has actually passed the line, so the
218
+ first section is not reported as current while the title is on screen. */
219
+ var best = -1
220
+ var line = focusLine(scrollY, max, innerHeight)
221
+ for (var i = 0; i < hs.length; i++) {
222
+ if (hs[i].getBoundingClientRect().top <= line) best = i
223
+ }
224
+ links.forEach(function (l, j) {
225
+ l.classList.toggle('on', j === best)
226
+ })
227
+ }
228
+ }
229
+
230
+ addEventListener('scroll', sync, { passive: true })
231
+ addEventListener('resize', sync)
232
+ sync()
233
+ }
@@ -0,0 +1,18 @@
1
+ export function theme(): void {
2
+ var r = document.documentElement
3
+ try {
4
+ var s = localStorage.getItem('course-theme')
5
+ if (s) r.dataset.theme = s
6
+ } catch (e) {}
7
+ document.addEventListener('click', function (e) {
8
+ var b = (e.target as Element).closest('.theme')
9
+ if (!b) return
10
+ var d =
11
+ r.dataset.theme === 'dark' ||
12
+ (!r.dataset.theme && matchMedia('(prefers-color-scheme: dark)').matches)
13
+ r.dataset.theme = d ? 'light' : 'dark'
14
+ try {
15
+ localStorage.setItem('course-theme', r.dataset.theme)
16
+ } catch (e) {}
17
+ })
18
+ }