@erclx/canon 5.52.0 → 5.54.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 +17 -0
- package/README.md +11 -13
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -8
- package/claude/skills/context-fold/REQUIREMENT.md +0 -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/design-taste/references/kinds.md +0 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
- package/claude/skills/draw-figure/SKILL.md +5 -6
- package/claude/skills/draw-figure/references/figures.md +0 -1
- package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
- 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/teach-craft/REQUIREMENT.md +30 -0
- package/claude/skills/teach-craft/SKILL.md +70 -0
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
- package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
- package/docs/agents/audits.md +0 -1
- package/docs/agents/install-and-sync.md +0 -8
- package/docs/target/migrations.md +16 -2
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +10 -22
- package/docs/workflow/skill-map.md +11 -14
- 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 -8
- package/scripts/core/regen-web-previews.ts +2 -67
- package/src/audits/catalog.ts +0 -1
- package/src/browser/engine.ts +3 -6
- package/src/capture/render.ts +1 -1
- package/src/claude/cases/authoring.ts +0 -12
- package/src/claude/cases/misc.ts +0 -22
- package/src/claude/cases/workflow.ts +2 -38
- package/src/claude/skills-audit.ts +1 -1
- package/src/cli/help.ts +0 -10
- package/src/cli.ts +0 -8
- package/src/commands/design.ts +16 -315
- package/src/commands/records.ts +0 -1
- package/src/commands/serve.ts +2 -2
- package/src/commands/sync.ts +4 -5
- package/src/design/components.ts +8 -1514
- package/src/design/css.ts +5 -6
- package/src/design/fonts.ts +3 -5
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/driver/drive.ts +3 -4
- package/src/driver/steps.ts +2 -5
- package/src/gate/stages.ts +4 -10
- package/src/gov/audit.ts +1 -6
- package/src/inventory/walk.ts +1 -1
- package/src/markdown/scope.ts +6 -35
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- 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/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
- package/claude/skills/draft-and-pick/SKILL.md +0 -11
- package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
- package/claude/skills/draft-doc/SKILL.md +0 -11
- package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
- package/claude/skills/draft-figure/SKILL.md +0 -11
- package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
- package/claude/skills/draft-identity/SKILL.md +0 -11
- package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
- package/claude/skills/draft-ready/SKILL.md +0 -11
- package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
- package/claude/skills/draft-screencast/SKILL.md +0 -11
- package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
- package/claude/skills/draft-slides/SKILL.md +0 -11
- package/claude/skills/make-deck/REQUIREMENT.md +0 -36
- package/claude/skills/make-deck/SKILL.md +0 -80
- package/claude/skills/make-deck/references/deck.md +0 -183
- package/claude/skills/read-frames/REQUIREMENT.md +0 -30
- package/claude/skills/read-frames/SKILL.md +0 -55
- package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
- package/claude/skills/record-screencast/SKILL.md +0 -99
- package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
- package/claude/skills/script-screencast/SKILL.md +0 -70
- package/claude/skills/script-screencast/references/beats-template.md +0 -119
- package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
- package/claude/skills/teach-workspace/SKILL.md +0 -196
- package/claude/skills/teach-workspace/references/glossary.md +0 -90
- package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
- package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
- package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
- package/claude/skills/teach-workspace/references/promotion.md +0 -116
- package/claude/skills/teach-workspace/references/revisit.md +0 -28
- package/claude/skills/teach-workspace/references/teach.md +0 -211
- package/claude/skills/video-craft/REQUIREMENT.md +0 -35
- package/claude/skills/video-craft/SKILL.md +0 -96
- package/claude/skills/video-craft/references/adopted.md +0 -110
- package/governance/rules/ui/canvas.md +0 -13
- 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/commands/demo.ts +0 -542
- package/src/commands/slides.ts +0 -184
- package/src/commands/teach.ts +0 -1401
- package/src/demo/beats.ts +0 -135
- package/src/demo/compile.ts +0 -350
- package/src/demo/container.ts +0 -219
- package/src/demo/cursors.ts +0 -55
- package/src/demo/drive.ts +0 -442
- package/src/demo/pointer.ts +0 -178
- package/src/demo/theme.ts +0 -112
- package/src/demo/timeline.ts +0 -64
- 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
- package/src/slides/convert/chart.ts +0 -165
- package/src/slides/convert/deck.ts +0 -244
- package/src/slides/convert/export.ts +0 -567
- package/src/slides/convert/master.ts +0 -198
- package/src/slides/convert/shapes.ts +0 -804
- package/src/slides/convert/svg.ts +0 -38
- package/src/slides/convert/walk.ts +0 -671
- package/src/slides/decks.ts +0 -110
- package/src/slides/open.ts +0 -18
- package/src/slides/package/fonts.ts +0 -301
- package/src/slides/package/motion.ts +0 -316
- package/src/slides/package/test-face.ts +0 -113
- package/src/teach/browser/compile.ts +0 -44
- package/src/teach/browser/dismiss.ts +0 -76
- package/src/teach/browser/focus-line.ts +0 -19
- package/src/teach/browser/glossary-filter.ts +0 -44
- package/src/teach/browser/head.ts +0 -30
- package/src/teach/browser/quiz.ts +0 -19
- package/src/teach/browser/reload.ts +0 -9
- package/src/teach/browser/sidebar.ts +0 -233
- package/src/teach/browser/theme.ts +0 -18
- package/src/teach/components/code.tsx +0 -24
- package/src/teach/components/heading.tsx +0 -11
- package/src/teach/components/list.tsx +0 -17
- package/src/teach/components/paragraph.tsx +0 -29
- package/src/teach/components/refs.tsx +0 -28
- package/src/teach/cross-references.ts +0 -78
- package/src/teach/fonts.ts +0 -16
- package/src/teach/highlight.ts +0 -71
- package/src/teach/html/jsx-dev-runtime.ts +0 -13
- package/src/teach/html/jsx-runtime.ts +0 -113
- package/src/teach/html/raw.ts +0 -28
- package/src/teach/lesson.ts +0 -178
- package/src/teach/nav.ts +0 -1337
- package/src/teach/render.ts +0 -240
- package/src/teach/workspace.ts +0 -1164
- package/src/teach/write-if-changed.ts +0 -17
|
@@ -1,233 +0,0 @@
|
|
|
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 {}
|
|
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 {}
|
|
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 {}
|
|
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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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 {}
|
|
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 {}
|
|
17
|
-
})
|
|
18
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../html */
|
|
2
|
-
import { highlight } from '@/teach/highlight'
|
|
3
|
-
import type { Element } from '@/teach/html/jsx-runtime'
|
|
4
|
-
import { raw } from '@/teach/html/raw'
|
|
5
|
-
|
|
6
|
-
export interface CodeProps {
|
|
7
|
-
/** A highlight.js language name or alias. Unregistered ones render plain. */
|
|
8
|
-
readonly lang?: string
|
|
9
|
-
readonly text: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The one way code reaches a lesson body. The highlighter escapes every source
|
|
14
|
-
* character before wrapping tokens in spans, which is what makes passing its
|
|
15
|
-
* output through `raw` safe.
|
|
16
|
-
*/
|
|
17
|
-
export function Code({ lang, text }: CodeProps): Element {
|
|
18
|
-
const className = lang === undefined ? 'hljs' : `hljs language-${lang}`
|
|
19
|
-
return (
|
|
20
|
-
<pre>
|
|
21
|
-
<code class={className}>{raw(highlight(text, lang).html)}</code>
|
|
22
|
-
</pre>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../html */
|
|
2
|
-
import type { Child, Element } from '@/teach/html/jsx-runtime'
|
|
3
|
-
|
|
4
|
-
export interface HeadingProps {
|
|
5
|
-
readonly level: 1 | 2
|
|
6
|
-
readonly children: Child
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function Heading({ level, children }: HeadingProps): Element {
|
|
10
|
-
return level === 1 ? <h1>{children}</h1> : <h2>{children}</h2>
|
|
11
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../html */
|
|
2
|
-
import type { Child, Element } from '@/teach/html/jsx-runtime'
|
|
3
|
-
|
|
4
|
-
export interface ListProps {
|
|
5
|
-
readonly ordered?: boolean
|
|
6
|
-
readonly items: readonly Child[]
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Composes an item per entry rather than taking pre-built `<li>` children, so
|
|
11
|
-
* every list in a lesson escapes the same way regardless of what the caller
|
|
12
|
-
* hands in.
|
|
13
|
-
*/
|
|
14
|
-
export function List({ ordered, items }: ListProps): Element {
|
|
15
|
-
const rendered = items.map((item) => <li>{item}</li>)
|
|
16
|
-
return ordered ? <ol>{rendered}</ol> : <ul>{rendered}</ul>
|
|
17
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../html */
|
|
2
|
-
import type { Child, Element } from '@/teach/html/jsx-runtime'
|
|
3
|
-
|
|
4
|
-
export interface ParagraphProps {
|
|
5
|
-
/** The lesson's dek, read back by `extractLessonMeta` in `@/teach/nav`. */
|
|
6
|
-
readonly lede?: boolean
|
|
7
|
-
/** Reference numbers, each linking to the `Refs` item carrying that id. */
|
|
8
|
-
readonly cites?: readonly number[]
|
|
9
|
-
readonly children: Child
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function Paragraph({ lede, cites, children }: ParagraphProps): Element {
|
|
13
|
-
const markers = (cites ?? []).map((cite) => (
|
|
14
|
-
<sup class="cite">
|
|
15
|
-
<a href={`#r${cite}`}>{cite}</a>
|
|
16
|
-
</sup>
|
|
17
|
-
))
|
|
18
|
-
return lede ? (
|
|
19
|
-
<p class="lede">
|
|
20
|
-
{children}
|
|
21
|
-
{markers}
|
|
22
|
-
</p>
|
|
23
|
-
) : (
|
|
24
|
-
<p>
|
|
25
|
-
{children}
|
|
26
|
-
{markers}
|
|
27
|
-
</p>
|
|
28
|
-
)
|
|
29
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ../html */
|
|
2
|
-
import type { Element } from '@/teach/html/jsx-runtime'
|
|
3
|
-
|
|
4
|
-
export interface Reference {
|
|
5
|
-
readonly title: string
|
|
6
|
-
readonly url?: string
|
|
7
|
-
readonly note?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface RefsProps {
|
|
11
|
-
readonly items: readonly Reference[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Numbers each item from 1 so its `id` matches the `#r<n>` a cited
|
|
16
|
-
* `Paragraph` links to. The caller has already checked every `url` parses as
|
|
17
|
-
* http or https, since attribute escaping does not stop a `javascript:` href.
|
|
18
|
-
*/
|
|
19
|
-
export function Refs({ items }: RefsProps): Element {
|
|
20
|
-
const rendered = items.map(({ title, url, note }, index) => (
|
|
21
|
-
<li id={`r${index + 1}`}>
|
|
22
|
-
<cite>{title}</cite>
|
|
23
|
-
{note === undefined ? null : `. ${note}`}
|
|
24
|
-
{url === undefined ? null : [' ', <a href={url}>{new URL(url).host}</a>]}
|
|
25
|
-
</li>
|
|
26
|
-
))
|
|
27
|
-
return <ol class="refs">{rendered}</ol>
|
|
28
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export interface LessonReferencesLinked {
|
|
2
|
-
readonly html: string
|
|
3
|
-
/** Four-digit numbers naming no lesson, each once, in first-seen order. */
|
|
4
|
-
readonly unresolved: readonly string[]
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A mention a learner would read as pointing at another lesson. The whitespace
|
|
9
|
-
* is captured rather than normalized, since a prettier-wrapped lesson can break
|
|
10
|
-
* a mention across a line.
|
|
11
|
-
*/
|
|
12
|
-
const MENTION = /\b(lesson|Lesson)(\s+)(\d{4})\b/g
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A link nav wrote on an earlier run, and the only kind it may rewrite. The
|
|
16
|
-
* close tag allows whitespace before its `>`, which is how prettier wraps it.
|
|
17
|
-
*/
|
|
18
|
-
const OWNED_LINK = /<a\s[^>]*\bdata-lesson-ref\b[^>]*>([\s\S]*?)<\/a\s*>/g
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* A comment, a raw-text element taken whole so a `<` inside a script never
|
|
22
|
-
* reads as a tag, any other tag, or the text between tags.
|
|
23
|
-
*/
|
|
24
|
-
const TOKEN =
|
|
25
|
-
/<!--[\s\S]*?-->|<(script|style)\b[\s\S]*?<\/\1\s*>|<\/?[a-zA-Z][^>]*>|[^<]+|</g
|
|
26
|
-
|
|
27
|
-
const TAG_NAME = /^<(\/?)([a-zA-Z][a-zA-Z0-9-]*)/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Elements whose text stays plain. A link inside a quiz `label` navigates on
|
|
31
|
-
* the click meant to pick an option, and one inside `summary` fights the
|
|
32
|
-
* toggle.
|
|
33
|
-
*/
|
|
34
|
-
const SKIPPED = new Set(['a', 'code', 'pre', 'label', 'summary'])
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Links every "lesson NNNN" mention in a lesson's authored region to the
|
|
38
|
-
* sibling file carrying that number, and reports the numbers naming no lesson.
|
|
39
|
-
* A link carrying `data-lesson-ref` is nav's own, so it is unwrapped and
|
|
40
|
-
* resolved again from its text, which re-points it after a slug rename and
|
|
41
|
-
* drops it once its target is gone. An author's own `<a>` is never touched.
|
|
42
|
-
*/
|
|
43
|
-
export function linkLessonReferences(
|
|
44
|
-
html: string,
|
|
45
|
-
targets: ReadonlyMap<string, string>,
|
|
46
|
-
current: string,
|
|
47
|
-
): LessonReferencesLinked {
|
|
48
|
-
const unresolved: string[] = []
|
|
49
|
-
let depth = 0
|
|
50
|
-
|
|
51
|
-
const linkText = (text: string): string =>
|
|
52
|
-
text.replace(MENTION, (mention, word, space, number: string) => {
|
|
53
|
-
const target = targets.get(number)
|
|
54
|
-
if (target === current) return mention
|
|
55
|
-
if (target === undefined) {
|
|
56
|
-
if (!unresolved.includes(number)) unresolved.push(number)
|
|
57
|
-
return mention
|
|
58
|
-
}
|
|
59
|
-
return `<a href="${target}" data-lesson-ref>${word}${space}${number}</a>`
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const unwrapped = html.replace(OWNED_LINK, (_link, text: string) => text)
|
|
63
|
-
|
|
64
|
-
const rewritten = unwrapped.replace(TOKEN, (token) => {
|
|
65
|
-
if (!token.startsWith('<') || token === '<') {
|
|
66
|
-
return depth === 0 ? linkText(token) : token
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const tag = TAG_NAME.exec(token)
|
|
70
|
-
if (tag === null || !SKIPPED.has(tag[2].toLowerCase())) return token
|
|
71
|
-
|
|
72
|
-
if (tag[1] === '/') depth = Math.max(0, depth - 1)
|
|
73
|
-
else if (!token.endsWith('/>')) depth += 1
|
|
74
|
-
return token
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
return { html: rewritten, unresolved }
|
|
78
|
-
}
|
package/src/teach/fonts.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The faces a teach page embeds: the design module's own `FONT_FACES` for
|
|
3
|
-
* everything a lesson reads and for code, plus the hand-drawn pair teach
|
|
4
|
-
* needs for its figures, both from `@/design/fonts`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
FONT_FACES,
|
|
9
|
-
HAND_DRAWN_FONT_FACES,
|
|
10
|
-
type FontFace,
|
|
11
|
-
} from '@/design/fonts'
|
|
12
|
-
|
|
13
|
-
export const TEACH_FONT_FACES: readonly FontFace[] = [
|
|
14
|
-
...FONT_FACES,
|
|
15
|
-
...HAND_DRAWN_FONT_FACES,
|
|
16
|
-
]
|
package/src/teach/highlight.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import hljs from 'highlight.js/lib/core'
|
|
2
|
-
import bash from 'highlight.js/lib/languages/bash'
|
|
3
|
-
import css from 'highlight.js/lib/languages/css'
|
|
4
|
-
import diff from 'highlight.js/lib/languages/diff'
|
|
5
|
-
import dockerfile from 'highlight.js/lib/languages/dockerfile'
|
|
6
|
-
import go from 'highlight.js/lib/languages/go'
|
|
7
|
-
import javascript from 'highlight.js/lib/languages/javascript'
|
|
8
|
-
import json from 'highlight.js/lib/languages/json'
|
|
9
|
-
import markdown from 'highlight.js/lib/languages/markdown'
|
|
10
|
-
import python from 'highlight.js/lib/languages/python'
|
|
11
|
-
import shell from 'highlight.js/lib/languages/shell'
|
|
12
|
-
import sql from 'highlight.js/lib/languages/sql'
|
|
13
|
-
import typescript from 'highlight.js/lib/languages/typescript'
|
|
14
|
-
import xml from 'highlight.js/lib/languages/xml'
|
|
15
|
-
import yaml from 'highlight.js/lib/languages/yaml'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A fixed set rather than all of highlight.js, since each grammar costs load
|
|
19
|
-
* time and a workspace rarely reaches past these. An unlisted language renders
|
|
20
|
-
* plain rather than refusing, so adding one here is the whole change.
|
|
21
|
-
*/
|
|
22
|
-
const LANGUAGES = {
|
|
23
|
-
bash,
|
|
24
|
-
css,
|
|
25
|
-
diff,
|
|
26
|
-
dockerfile,
|
|
27
|
-
go,
|
|
28
|
-
javascript,
|
|
29
|
-
json,
|
|
30
|
-
markdown,
|
|
31
|
-
python,
|
|
32
|
-
shell,
|
|
33
|
-
sql,
|
|
34
|
-
typescript,
|
|
35
|
-
xml,
|
|
36
|
-
yaml,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const [name, grammar] of Object.entries(LANGUAGES)) {
|
|
40
|
-
hljs.registerLanguage(name, grammar)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface Highlighted {
|
|
44
|
-
/** Markup safe to embed inside `<code>`: every source character escaped. */
|
|
45
|
-
readonly html: string
|
|
46
|
-
/** The language as asked for, or `undefined` when it rendered plain. */
|
|
47
|
-
readonly language: string | undefined
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function escapeCode(text: string): string {
|
|
51
|
-
return text
|
|
52
|
-
.replaceAll('&', '&')
|
|
53
|
-
.replaceAll('<', '<')
|
|
54
|
-
.replaceAll('>', '>')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Highlights code at build time into `hljs-*` class spans the teach
|
|
59
|
-
* stylesheet colors, so an emitted page needs no script. An absent or
|
|
60
|
-
* unregistered language falls back to escaped plain text rather than
|
|
61
|
-
* throwing or guessing.
|
|
62
|
-
*/
|
|
63
|
-
export function highlight(text: string, lang?: string): Highlighted {
|
|
64
|
-
if (lang === undefined || hljs.getLanguage(lang) === undefined) {
|
|
65
|
-
return { html: escapeCode(text), language: undefined }
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
html: hljs.highlight(text, { language: lang, ignoreIllegals: true }).value,
|
|
69
|
-
language: lang,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bun resolves this module rather than `jsx-runtime` at run time (spike 1,
|
|
3
|
-
* `.canon/groundwork/56-teach-render-layer/08-spikes.md`), so every export a
|
|
4
|
-
* caller might reach through either entry point has to exist here too.
|
|
5
|
-
*/
|
|
6
|
-
export {
|
|
7
|
-
Fragment,
|
|
8
|
-
jsx,
|
|
9
|
-
jsx as jsxDEV,
|
|
10
|
-
jsxs,
|
|
11
|
-
render,
|
|
12
|
-
type JSX,
|
|
13
|
-
} from './jsx-runtime'
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { escape, isRaw, raw, type RawMarkup } from './raw'
|
|
2
|
-
|
|
3
|
-
export type Child =
|
|
4
|
-
| string
|
|
5
|
-
| number
|
|
6
|
-
| boolean
|
|
7
|
-
| null
|
|
8
|
-
| undefined
|
|
9
|
-
| RawMarkup
|
|
10
|
-
| readonly Child[]
|
|
11
|
-
|
|
12
|
-
export interface Props {
|
|
13
|
-
readonly children?: Child
|
|
14
|
-
readonly [key: string]: unknown
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* What a component returns and what `jsx` produces: markup already rendered,
|
|
19
|
-
* carrying the same brand `raw` gives an author's own escape hatch. One brand
|
|
20
|
-
* for both is what lets a parent's `{children}` compose a child's output
|
|
21
|
-
* without escaping it a second time, since a plain string cannot say whether
|
|
22
|
-
* it is author text or already-rendered markup.
|
|
23
|
-
*/
|
|
24
|
-
export type Element = RawMarkup
|
|
25
|
-
|
|
26
|
-
export type Component<P extends Props = Props> = (props: P) => Element
|
|
27
|
-
|
|
28
|
-
export const Fragment = Symbol('Fragment')
|
|
29
|
-
|
|
30
|
-
const ARIA_PREFIX = 'aria-'
|
|
31
|
-
|
|
32
|
-
const VOID_ELEMENTS = new Set([
|
|
33
|
-
'area',
|
|
34
|
-
'base',
|
|
35
|
-
'br',
|
|
36
|
-
'col',
|
|
37
|
-
'embed',
|
|
38
|
-
'hr',
|
|
39
|
-
'img',
|
|
40
|
-
'input',
|
|
41
|
-
'link',
|
|
42
|
-
'meta',
|
|
43
|
-
'source',
|
|
44
|
-
'track',
|
|
45
|
-
'wbr',
|
|
46
|
-
])
|
|
47
|
-
|
|
48
|
-
function renderChild(child: Child): string {
|
|
49
|
-
if (child === null || child === undefined || typeof child === 'boolean') {
|
|
50
|
-
return ''
|
|
51
|
-
}
|
|
52
|
-
if (typeof child === 'number') return String(child)
|
|
53
|
-
if (typeof child === 'string') return escape(child)
|
|
54
|
-
if (isRaw(child)) return child.html
|
|
55
|
-
return child.map(renderChild).join('')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* ARIA takes the literal word for a boolean, where HTML boolean attributes
|
|
60
|
-
* take presence or absence. Spike 1's first draft rendered a bare
|
|
61
|
-
* `aria-disabled` for `true` and dropped `aria-disabled={false}` entirely,
|
|
62
|
-
* silently removing state a screen reader reads.
|
|
63
|
-
*/
|
|
64
|
-
function renderAttribute(key: string, value: unknown): string {
|
|
65
|
-
if (key.startsWith(ARIA_PREFIX) && typeof value === 'boolean') {
|
|
66
|
-
return ` ${key}="${value}"`
|
|
67
|
-
}
|
|
68
|
-
if (value === undefined || value === null || value === false) return ''
|
|
69
|
-
if (value === true) return ` ${key}`
|
|
70
|
-
if (isRaw(value)) return ` ${key}="${escape(value.html)}"`
|
|
71
|
-
return ` ${key}="${escape(String(value))}"`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderAttributes(props: Props): string {
|
|
75
|
-
return Object.entries(props)
|
|
76
|
-
.filter(([key]) => key !== 'children')
|
|
77
|
-
.map(([key, value]) => renderAttribute(key, value))
|
|
78
|
-
.join('')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function jsx(
|
|
82
|
-
type: string | Component | typeof Fragment,
|
|
83
|
-
props: Props,
|
|
84
|
-
): Element {
|
|
85
|
-
if (type === Fragment) return raw(renderChild(props.children ?? null))
|
|
86
|
-
if (typeof type === 'function') return type(props)
|
|
87
|
-
|
|
88
|
-
const attrs = renderAttributes(props)
|
|
89
|
-
if (VOID_ELEMENTS.has(type)) return raw(`<${type}${attrs}>`)
|
|
90
|
-
|
|
91
|
-
return raw(
|
|
92
|
-
`<${type}${attrs}>${renderChild(props.children ?? null)}</${type}>`,
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const jsxs = jsx
|
|
97
|
-
|
|
98
|
-
/** Unwraps a rendered element to the plain string a file writes to disk. */
|
|
99
|
-
export function render(element: Element): string {
|
|
100
|
-
return element.html
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export namespace JSX {
|
|
104
|
-
export type Element = RawMarkup
|
|
105
|
-
|
|
106
|
-
export interface ElementChildrenAttribute {
|
|
107
|
-
children: Record<string, never>
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface IntrinsicElements {
|
|
111
|
-
[name: string]: Props
|
|
112
|
-
}
|
|
113
|
-
}
|
package/src/teach/html/raw.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const RAW = Symbol('raw')
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Markup already rendered, exempt from the escaping every other string
|
|
5
|
-
* receives. `raw` is the only way to produce one, so a lesson has to opt in
|
|
6
|
-
* explicitly rather than an author text string accidentally passing through
|
|
7
|
-
* unescaped.
|
|
8
|
-
*/
|
|
9
|
-
export interface RawMarkup {
|
|
10
|
-
readonly [RAW]: true
|
|
11
|
-
readonly html: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function raw(html: string): RawMarkup {
|
|
15
|
-
return { [RAW]: true, html }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function isRaw(value: unknown): value is RawMarkup {
|
|
19
|
-
return typeof value === 'object' && value !== null && RAW in value
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function escape(value: string): string {
|
|
23
|
-
return value
|
|
24
|
-
.replace(/&/g, '&')
|
|
25
|
-
.replace(/</g, '<')
|
|
26
|
-
.replace(/>/g, '>')
|
|
27
|
-
.replace(/"/g, '"')
|
|
28
|
-
}
|