@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -1,71 +0,0 @@
1
- /**
2
- * The canvas key layer: one map from a key event to a surface action, which
3
- * the surface and every loaded frame document call. It sees only what the
4
- * surface forwards, so a key typed into a panel field never reaches it.
5
- */
6
-
7
- export type SurfaceAction =
8
- | 'tool-move'
9
- | 'tool-pan'
10
- | 'zoom-in'
11
- | 'zoom-out'
12
- | 'zoom-fit'
13
- | 'pan-hold'
14
- | 'pan-release'
15
- | 'undo'
16
- | 'redo'
17
- | 'panels-toggle'
18
-
19
- /** The fields of a `KeyboardEvent` the map reads, so a test needs no DOM. */
20
- export interface KeyInput {
21
- readonly type: string
22
- readonly key: string
23
- readonly code: string
24
- readonly shiftKey: boolean
25
- readonly ctrlKey: boolean
26
- readonly metaKey: boolean
27
- readonly altKey: boolean
28
- }
29
-
30
- const PLAIN_KEYS: Readonly<Record<string, SurfaceAction | undefined>> = {
31
- v: 'tool-move',
32
- h: 'tool-pan',
33
- '=': 'zoom-in',
34
- '+': 'zoom-in',
35
- '-': 'zoom-out',
36
- ' ': 'pan-hold',
37
- }
38
-
39
- /**
40
- * Undo and redo are the one exception to the browser owning a modifier: Ctrl
41
- * or Cmd with Z, with Shift to redo, and Ctrl with Y. Any other Cmd, Ctrl, or
42
- * Alt combination is the browser's or the system's, so it returns nothing.
43
- */
44
- function historyAction(event: KeyInput): SurfaceAction | undefined {
45
- if (event.altKey) return undefined
46
- const key = event.key.toLowerCase()
47
- if (key === 'z') return event.shiftKey ? 'redo' : 'undo'
48
- if (key === 'y' && event.ctrlKey && !event.shiftKey) return 'redo'
49
- return undefined
50
- }
51
-
52
- /**
53
- * Space coming up releases whatever is held with it, or a modifier pressed
54
- * mid-pan would leave the pan stuck. Shift and the 1 key fits, and the
55
- * backslash key toggles the panels, by `code`, since the character each
56
- * gives differs by layout.
57
- */
58
- export function surfaceAction(event: KeyInput): SurfaceAction | undefined {
59
- if (event.type === 'keyup') {
60
- return event.key === ' ' ? 'pan-release' : undefined
61
- }
62
- if (event.type !== 'keydown') return undefined
63
- if (event.ctrlKey || event.metaKey) return historyAction(event)
64
- if (event.altKey) return undefined
65
- if (event.shiftKey) {
66
- if (event.code === 'Digit1') return 'zoom-fit'
67
- return event.key === '+' ? 'zoom-in' : undefined
68
- }
69
- if (event.code === 'Backslash') return 'panels-toggle'
70
- return PLAIN_KEYS[event.key.toLowerCase()]
71
- }
@@ -1,118 +0,0 @@
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
- }
@@ -1,71 +0,0 @@
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
- /*
55
- * The page declares an empty icon so the browser never asks for /favicon.ico,
56
- * since the bundler cannot resolve a route the server answers, and this points
57
- * it at the brand icon once the shell loads.
58
- */
59
- document
60
- .querySelector('link[rel="icon"]')
61
- ?.setAttribute('href', '/api/icon.svg')
62
-
63
- const events = new EventSource('/api/events')
64
- events.addEventListener('message', (message) => {
65
- void applyChange(JSON.parse(message.data) as ChangeEvent)
66
- })
67
-
68
- void loadPages()
69
-
70
- const mount = document.getElementById('app')
71
- if (mount) render(<App />, mount)
@@ -1,212 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { JSX } from 'preact'
3
- import { Layers } from '@/canvas/client/layers'
4
- import { ThemePanel } from '@/canvas/client/theme-panel'
5
- import type { Frame } from '@/canvas/content'
6
- import {
7
- currentPage,
8
- expandedFrames,
9
- frameKey,
10
- isLoaded,
11
- type LeftTab,
12
- leftTab,
13
- loadError,
14
- pages,
15
- selectedFrame,
16
- selectedPage,
17
- selectFrame,
18
- theme,
19
- toggleLayers,
20
- toggleTheme,
21
- } from '@/canvas/client/state'
22
-
23
- export interface PagesPanelProps {
24
- /** Brings a frame into view on the surface. */
25
- readonly onFocusFrame: (frame: Frame) => void
26
- }
27
-
28
- function ThemeToggle(): JSX.Element {
29
- const next = theme.value === 'dark' ? 'light' : 'dark'
30
- return (
31
- <button
32
- type="button"
33
- class="icon-button"
34
- aria-label={`Switch to ${next} theme`}
35
- title={`Switch to ${next} theme`}
36
- onClick={toggleTheme}
37
- >
38
- <span aria-hidden="true">{theme.value === 'dark' ? '☾' : '☀'}</span>
39
- </button>
40
- )
41
- }
42
-
43
- function PageList(): JSX.Element {
44
- if (pages.value.length === 0) {
45
- return (
46
- <p class="empty">
47
- No pages yet. Run <code>canon canvas page add &lt;name&gt;</code> to add
48
- one.
49
- </p>
50
- )
51
- }
52
- const current = currentPage.value?.name
53
- return (
54
- <ul class="list" aria-label="Pages">
55
- {pages.value.map((page) => (
56
- <li key={page.name}>
57
- <button
58
- type="button"
59
- class="row"
60
- aria-current={page.name === current ? 'page' : undefined}
61
- title={page.name}
62
- onClick={() => {
63
- selectedPage.value = page.name
64
- }}
65
- >
66
- <span class="row-label">{page.name}</span>
67
- <span class="row-meta">{page.frames.length}</span>
68
- </button>
69
- </li>
70
- ))}
71
- </ul>
72
- )
73
- }
74
-
75
- function FrameList({ onFocusFrame }: PagesPanelProps): JSX.Element | null {
76
- const page = currentPage.value
77
- if (!page) return null
78
- if (page.frames.length === 0) {
79
- return (
80
- <p class="empty">
81
- No frames on this page. Run{' '}
82
- <code>canon canvas frame add {page.name} &lt;name&gt;</code> to add one.
83
- </p>
84
- )
85
- }
86
- return (
87
- <ul class="list" aria-label="Frames">
88
- {page.frames.map((frame) => {
89
- const key = frameKey(page.name, frame)
90
- const isOpen = expandedFrames.value.has(key)
91
- return (
92
- <li key={frame.name}>
93
- <div class="frame-row">
94
- <button
95
- type="button"
96
- class="disclosure"
97
- aria-expanded={isOpen}
98
- aria-label={`${isOpen ? 'Hide' : 'Show'} layers of ${frame.name}`}
99
- title={`${isOpen ? 'Hide' : 'Show'} layers`}
100
- onClick={() => toggleLayers(key)}
101
- >
102
- <span aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
103
- </button>
104
- <button
105
- type="button"
106
- class="row"
107
- title={frame.name}
108
- aria-current={
109
- frame.name === selectedFrame.value?.name ? 'true' : undefined
110
- }
111
- onClick={() => {
112
- onFocusFrame(frame)
113
- void selectFrame({ page: page.name, frame: frame.name })
114
- }}
115
- >
116
- <span class="row-label">{frame.name}</span>
117
- <span class="row-meta">{frame.width}</span>
118
- </button>
119
- </div>
120
- {isOpen ? (
121
- <Layers
122
- frame={{ page: page.name, frame: frame.name }}
123
- frameKey={key}
124
- />
125
- ) : null}
126
- </li>
127
- )
128
- })}
129
- </ul>
130
- )
131
- }
132
-
133
- const TABS: readonly { readonly id: LeftTab; readonly label: string }[] = [
134
- { id: 'pages', label: 'Pages' },
135
- { id: 'theme', label: 'Theme' },
136
- ]
137
-
138
- /** Two tabs, so the arrow keys move between them and wrap. */
139
- function Tabs(): JSX.Element {
140
- const select = (at: number) => {
141
- const tab = TABS[(at + TABS.length) % TABS.length]
142
- if (!tab) return
143
- leftTab.value = tab.id
144
- document.getElementById(`tab-${tab.id}`)?.focus()
145
- }
146
- return (
147
- <div class="tabs" role="tablist" aria-label="Left panel">
148
- {TABS.map((tab, at) => {
149
- const isSelected = leftTab.value === tab.id
150
- return (
151
- <button
152
- key={tab.id}
153
- id={`tab-${tab.id}`}
154
- type="button"
155
- role="tab"
156
- class="tab"
157
- aria-selected={isSelected}
158
- tabIndex={isSelected ? 0 : -1}
159
- onClick={() => {
160
- leftTab.value = tab.id
161
- }}
162
- onKeyDown={(event) => {
163
- if (event.key === 'ArrowRight') select(at + 1)
164
- if (event.key === 'ArrowLeft') select(at - 1)
165
- }}
166
- >
167
- {tab.label}
168
- </button>
169
- )
170
- })}
171
- </div>
172
- )
173
- }
174
-
175
- export function PagesPanel(props: PagesPanelProps): JSX.Element {
176
- return (
177
- <nav
178
- id="pages-panel"
179
- class="panel panel-left"
180
- aria-label="Pages and frames"
181
- >
182
- <header class="panel-head">
183
- <span class="brand">Canvas</span>
184
- <ThemeToggle />
185
- </header>
186
- {loadError.value ? (
187
- <p class="empty" role="alert">
188
- {loadError.value}
189
- </p>
190
- ) : !isLoaded.value ? (
191
- <p class="empty">Loading</p>
192
- ) : leftTab.value === 'theme' ? (
193
- <>
194
- <Tabs />
195
- <ThemePanel />
196
- </>
197
- ) : (
198
- <>
199
- <Tabs />
200
- <h2 class="section-label">Pages</h2>
201
- <PageList />
202
- {currentPage.value ? (
203
- <>
204
- <h2 class="section-label">Frames</h2>
205
- <FrameList {...props} />
206
- </>
207
- ) : null}
208
- </>
209
- )}
210
- </nav>
211
- )
212
- }
@@ -1,90 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { JSX } from 'preact'
3
- import { useRef } from 'preact/hooks'
4
- import {
5
- clampPanelWidth,
6
- MAX_PANEL_WIDTH,
7
- MIN_PANEL_WIDTH,
8
- type PanelSide,
9
- panelWidths,
10
- setPanelWidth,
11
- } from '@/canvas/client/state'
12
-
13
- const KEY_STEP = 16
14
- const SHIFT_STEP = 64
15
-
16
- const NAMES: Readonly<Record<PanelSide, string>> = {
17
- left: 'Resize pages panel',
18
- right: 'Resize details panel',
19
- }
20
-
21
- /**
22
- * The drag handle on a panel's inner edge, a focusable separator the arrow
23
- * keys move as well. It sits outside the surface, so a drag on it never pans,
24
- * moves a frame, or reaches the undo history.
25
- */
26
- export function PanelHandle({
27
- side,
28
- controls,
29
- }: {
30
- readonly side: PanelSide
31
- readonly controls: string
32
- }): JSX.Element {
33
- const drag = useRef<{ x: number; width: number } | undefined>(undefined)
34
- const width = panelWidths.value[side]
35
- /* Dragging right widens the left panel and narrows the right one. */
36
- const sign = side === 'left' ? 1 : -1
37
-
38
- const handlePointerDown = (event: PointerEvent) => {
39
- event.preventDefault()
40
- drag.current = { x: event.clientX, width }
41
- ;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
42
- }
43
-
44
- const handlePointerMove = (event: PointerEvent) => {
45
- const start = drag.current
46
- if (!start) return
47
- setPanelWidth(
48
- side,
49
- start.width + sign * (event.clientX - start.x),
50
- window.innerWidth,
51
- )
52
- }
53
-
54
- const handlePointerUp = () => {
55
- drag.current = undefined
56
- }
57
-
58
- const handleKeyDown = (event: KeyboardEvent) => {
59
- const step = event.shiftKey ? SHIFT_STEP : KEY_STEP
60
- const next: Record<string, number | undefined> = {
61
- ArrowRight: width + sign * step,
62
- ArrowLeft: width - sign * step,
63
- Home: MIN_PANEL_WIDTH,
64
- End: MAX_PANEL_WIDTH,
65
- }
66
- const wanted = next[event.key]
67
- if (wanted === undefined) return
68
- event.preventDefault()
69
- setPanelWidth(side, wanted, window.innerWidth)
70
- }
71
-
72
- return (
73
- <div
74
- class={`panel-handle panel-handle-${side}`}
75
- role="separator"
76
- aria-orientation="vertical"
77
- aria-label={NAMES[side]}
78
- aria-controls={controls}
79
- aria-valuenow={width}
80
- aria-valuemin={MIN_PANEL_WIDTH}
81
- aria-valuemax={clampPanelWidth(side, MAX_PANEL_WIDTH, window.innerWidth)}
82
- tabIndex={0}
83
- onPointerDown={handlePointerDown}
84
- onPointerMove={handlePointerMove}
85
- onPointerUp={handlePointerUp}
86
- onPointerCancel={handlePointerUp}
87
- onKeyDown={handleKeyDown}
88
- />
89
- )
90
- }