@erclx/canon 5.53.0 → 5.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +6 -6
- package/claude/skills/document-repair/SKILL.md +5 -5
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/target-check/SKILL.md +6 -8
- package/claude/skills/target-check/references/domains.md +1 -4
- package/claude/skills/target-setup/SKILL.md +8 -8
- package/claude/skills/target-update/SKILL.md +5 -5
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +2 -10
- package/docs/agents/output-shape.md +11 -10
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -5
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/ui.sh +12 -65
- package/src/audits/run.ts +1 -1
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -2
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +20 -324
- package/src/commands/gate.ts +5 -3
- package/src/commands/gov.ts +11 -21
- package/src/commands/labels.ts +1 -1
- package/src/commands/pr.ts +1 -1
- package/src/commands/sync.ts +5 -6
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +4 -20
- package/src/gov/audit.ts +1 -6
- package/src/hooks/post-merge.ts +2 -5
- package/src/markdown/scope.ts +2 -4
- package/src/process/harness.ts +3 -4
- package/src/sandbox/equivalence.ts +0 -1
- package/src/sandbox/provision.ts +8 -13
- package/src/sandbox/scenario.ts +6 -12
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/engine.ts +3 -8
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/src/sync/workflow.ts +3 -5
- package/src/tooling/verify-stack.ts +1 -1
- package/tooling/base/configs/.husky/post-merge +6 -10
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/governance/rules/ui/canvas.md +0 -13
- package/scripts/core/check-color-source.sh +0 -51
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
|
@@ -1,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 <name></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} <name></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
|
-
}
|