@erclx/canon 5.27.0 → 5.29.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-ready/SKILL.md +3 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +4 -2
- package/claude/skills/review-address/SKILL.md +16 -7
- package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
- package/claude/skills/review-craft/references/ui.md +4 -4
- package/claude/skills/review-ui/REQUIREMENT.md +2 -2
- package/claude/skills/review-ui/SKILL.md +13 -11
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +19 -7
- package/claude/skills/review-ui/references/tick.md +22 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
- package/claude/skills/role-orchestrator/SKILL.md +24 -26
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +12 -2
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
- package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +2 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +3 -2
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/docs/agents/canvas.md +12 -4
- package/docs/agents/gate.md +3 -1
- package/docs/agents/index.md +2 -1
- package/docs/agents/pr-evidence.md +15 -14
- package/docs/agents/pr-tick.md +72 -0
- package/docs/agents/sandbox.md +2 -2
- package/docs/agents/tasks-plans.md +1 -1
- package/docs/agents/tasks-validate.md +18 -18
- package/docs/agents/tasks.md +1 -1
- package/docs/workflow/operating-model.md +10 -15
- package/docs/workflow/parallel-features.md +7 -7
- package/package.json +1 -1
- package/src/audits/baseline.ts +1 -1
- package/src/audits/run.ts +1 -1
- package/src/canvas/capture.ts +1 -1
- package/src/canvas/client/app.tsx +105 -4
- package/src/canvas/client/inspector/field.tsx +61 -2
- package/src/canvas/client/inspector/fields.css +78 -0
- package/src/canvas/client/inspector/token-select.tsx +109 -0
- package/src/canvas/client/inspector/values.ts +74 -2
- package/src/canvas/client/inspector.tsx +134 -15
- package/src/canvas/client/keys.ts +71 -0
- package/src/canvas/client/pages-panel.tsx +5 -1
- package/src/canvas/client/panel-handle.tsx +90 -0
- package/src/canvas/client/selection.tsx +3 -1
- package/src/canvas/client/shell.css +194 -4
- package/src/canvas/client/state.ts +255 -11
- package/src/canvas/client/surface.tsx +231 -18
- package/src/canvas/client/tools.tsx +153 -0
- package/src/canvas/content.ts +66 -1
- package/src/canvas/edit.ts +207 -25
- package/src/canvas/history.ts +116 -0
- package/src/canvas/server.ts +119 -13
- package/src/canvas/tokens.ts +1 -1
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-reach.ts +1 -1
- package/src/{exec.ts → cli/exec.ts} +2 -2
- package/src/{help.ts → cli/help.ts} +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/audits.ts +9 -1
- package/src/commands/autoship.ts +1 -1
- package/src/commands/canvas.ts +2 -2
- package/src/commands/capture.ts +1 -1
- package/src/commands/census.ts +1 -1
- package/src/commands/claude.ts +5 -5
- package/src/commands/comments.ts +1 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/demo.ts +9 -1
- package/src/commands/deps.ts +1 -1
- package/src/commands/design.ts +12 -4
- package/src/commands/docs.ts +2 -2
- package/src/commands/driver.ts +9 -1
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gate.ts +10 -3
- package/src/commands/gov.ts +3 -3
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +11 -3
- package/src/commands/intake.ts +1 -1
- package/src/commands/inventory.ts +9 -1
- package/src/commands/labels.ts +1 -1
- package/src/commands/markdown.ts +1 -1
- package/src/commands/migrate.ts +2 -2
- package/src/commands/pr.ts +324 -9
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +11 -14
- package/src/commands/secrets.ts +9 -1
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +2 -2
- package/src/commands/standards.ts +1 -1
- package/src/commands/sync.ts +3 -3
- package/src/commands/targets.ts +1 -1
- package/src/commands/tasks.ts +20 -3
- package/src/commands/teach.ts +1 -1
- package/src/commands/tooling.ts +2 -2
- package/src/commands/transcripts.ts +2 -2
- package/src/commands/upgrade.ts +2 -2
- package/src/commands/wiki.ts +2 -2
- package/src/commands/worktrees.ts +1 -1
- package/src/context/architecture.ts +1 -1
- package/src/context/citations.ts +2 -2
- package/src/context/classify/extract.ts +1 -1
- package/src/context/folders.ts +2 -2
- package/src/context/requirements.ts +1 -1
- package/src/docs/read.ts +1 -1
- package/src/feedback/github.ts +1 -1
- package/src/gate/measures.ts +2 -2
- package/src/gate/sequencer.ts +55 -3
- package/src/gate/stages.ts +8 -0
- package/src/gate/suite-lock.ts +214 -0
- package/src/init/run.ts +1 -1
- package/src/intake/folder.ts +1 -1
- package/src/labels/map.ts +1 -1
- package/src/labels/phase.ts +3 -3
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/records.ts +11 -11
- package/src/migrate/rename.ts +1 -1
- package/src/migrate/scratch-evidence.ts +1 -1
- package/src/migrate/surface-roots.ts +6 -6
- package/src/pr/evidence.ts +188 -16
- package/src/pr/tick.ts +56 -0
- package/src/process/harness.ts +1 -1
- package/src/records/backup.ts +1 -1
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +1 -1
- package/src/records/size.ts +1 -1
- package/src/records/stale-canonical.ts +1 -1
- package/src/records/stale.ts +1 -1
- package/src/records/validate.ts +2 -2
- package/src/{project-root.ts → roots/project.ts} +2 -2
- package/src/{record-root.ts → roots/record.ts} +1 -1
- package/src/sandbox/coverage.ts +11 -12
- package/src/sandbox/equivalence.ts +57 -26
- package/src/sandbox/headless/run.ts +1 -1
- package/src/sandbox/hooks.ts +1 -1
- package/src/sandbox/provision.ts +79 -29
- package/src/sandbox/scenario.ts +588 -0
- package/src/serve/report.ts +1 -1
- package/src/sessions/live.ts +4 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/slides/decks.ts +1 -1
- package/src/standards/read.ts +1 -1
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/layout.ts +2 -2
- package/src/sync/stamp.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/validate.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/label.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/validate.ts +38 -14
- package/src/teach/nav.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/read.ts +1 -1
- package/src/tooling/scan.ts +1 -1
- package/src/tooling/verify-stack.ts +1 -1
- package/src/version/installed.ts +1 -1
- package/src/version/skew.ts +1 -1
- package/standards/board.md +3 -3
- package/standards/plan.md +13 -7
- package/tsconfig.json +1 -1
- /package/src/{cli-run.ts → cli/run.ts} +0 -0
- /package/src/{ui.ts → cli/ui.ts} +0 -0
- /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
- /package/src/{surface-root.ts → roots/surface.ts} +0 -0
|
@@ -22,6 +22,11 @@ export interface TokenSource {
|
|
|
22
22
|
|
|
23
23
|
export type LeftTab = 'pages' | 'theme'
|
|
24
24
|
|
|
25
|
+
export type Tool = 'move' | 'pan'
|
|
26
|
+
|
|
27
|
+
/** A Space pan held, and whether a drag has moved the view while it was. */
|
|
28
|
+
export type SpacePan = 'held' | 'dragged'
|
|
29
|
+
|
|
25
30
|
/** One property change to the selected element, as the inspector posts it. */
|
|
26
31
|
export interface PendingEdit {
|
|
27
32
|
readonly key: string
|
|
@@ -45,11 +50,26 @@ export interface SelectionRef extends FrameRef {
|
|
|
45
50
|
readonly element?: ElementRef
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
/** A frame a session says it is editing, until the stamp it expires at. */
|
|
54
|
+
export interface EditingRef extends FrameRef {
|
|
55
|
+
readonly by: string
|
|
56
|
+
readonly until: string
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
export interface PagesRecord {
|
|
49
60
|
readonly pages: readonly Page[]
|
|
50
61
|
readonly tokens: TokenSource
|
|
51
62
|
/** Absent when nothing is selected or the selected frame is gone. */
|
|
52
63
|
readonly selection?: SelectionRef | null
|
|
64
|
+
/** The live marks, which a server older than the mark leaves out. */
|
|
65
|
+
readonly editing?: readonly EditingRef[]
|
|
66
|
+
/** Whether the server holds an edit to undo or redo, absent before undo shipped. */
|
|
67
|
+
readonly history?: HistoryFlags
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface HistoryFlags {
|
|
71
|
+
readonly canUndo: boolean
|
|
72
|
+
readonly canRedo: boolean
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
export interface ChangeEvent {
|
|
@@ -101,6 +121,9 @@ export const hoveredElement = signal<
|
|
|
101
121
|
{ readonly key: string; readonly index: number } | undefined
|
|
102
122
|
>(undefined)
|
|
103
123
|
|
|
124
|
+
/** Each marked frame's mark by frame key, dropped at its `until`. */
|
|
125
|
+
export const editingFrames = signal<ReadonlyMap<string, EditingRef>>(new Map())
|
|
126
|
+
|
|
104
127
|
/** Frame keys whose layers are open in the pages panel. */
|
|
105
128
|
export const expandedFrames = signal<ReadonlySet<string>>(new Set())
|
|
106
129
|
|
|
@@ -112,6 +135,70 @@ export const writeError = signal<string | undefined>(undefined)
|
|
|
112
135
|
|
|
113
136
|
export const leftTab = signal<LeftTab>('pages')
|
|
114
137
|
|
|
138
|
+
/** Whether the operator hid both side panels to see the whole surface. */
|
|
139
|
+
export const panelsHidden = signal(false)
|
|
140
|
+
|
|
141
|
+
export function togglePanels(): void {
|
|
142
|
+
panelsHidden.value = !panelsHidden.value
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type PanelSide = 'left' | 'right'
|
|
146
|
+
|
|
147
|
+
export interface PanelWidths {
|
|
148
|
+
readonly left: number
|
|
149
|
+
readonly right: number
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The 15rem and 16rem columns the shell opened at before panels resized. */
|
|
153
|
+
export const DEFAULT_PANEL_WIDTHS: PanelWidths = { left: 240, right: 256 }
|
|
154
|
+
export const MIN_PANEL_WIDTH = 180
|
|
155
|
+
export const MAX_PANEL_WIDTH = 480
|
|
156
|
+
/** What a panel never squeezes the surface below, the reflow floor. */
|
|
157
|
+
export const MIN_SURFACE_WIDTH = 320
|
|
158
|
+
|
|
159
|
+
export const panelWidths = signal<PanelWidths>(DEFAULT_PANEL_WIDTHS)
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Holds one panel between the floor and the ceiling, and under whatever keeps
|
|
163
|
+
* the surface at its own floor beside the other panel. The floor wins on a
|
|
164
|
+
* window too narrow for both.
|
|
165
|
+
*/
|
|
166
|
+
export function clampPanelWidth(
|
|
167
|
+
side: PanelSide,
|
|
168
|
+
width: number,
|
|
169
|
+
windowWidth: number,
|
|
170
|
+
widths: PanelWidths = panelWidths.value,
|
|
171
|
+
): number {
|
|
172
|
+
const other = side === 'left' ? widths.right : widths.left
|
|
173
|
+
const ceiling = Math.min(
|
|
174
|
+
MAX_PANEL_WIDTH,
|
|
175
|
+
windowWidth - other - MIN_SURFACE_WIDTH,
|
|
176
|
+
)
|
|
177
|
+
const fallback = DEFAULT_PANEL_WIDTHS[side]
|
|
178
|
+
const wanted = Number.isFinite(width) ? width : fallback
|
|
179
|
+
return Math.round(Math.max(MIN_PANEL_WIDTH, Math.min(ceiling, wanted)))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function setPanelWidth(
|
|
183
|
+
side: PanelSide,
|
|
184
|
+
width: number,
|
|
185
|
+
windowWidth: number,
|
|
186
|
+
): void {
|
|
187
|
+
const next = clampPanelWidth(side, width, windowWidth)
|
|
188
|
+
if (panelWidths.value[side] === next) return
|
|
189
|
+
panelWidths.value = { ...panelWidths.value, [side]: next }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The tool the strip has lit, which a Space pan overrides while held. */
|
|
193
|
+
export const activeTool = signal<Tool>('move')
|
|
194
|
+
|
|
195
|
+
export const spacePan = signal<SpacePan | undefined>(undefined)
|
|
196
|
+
|
|
197
|
+
/** The tool in effect, so Space shows Pan lit and releasing it restores it. */
|
|
198
|
+
export const shownTool = computed<Tool>(() =>
|
|
199
|
+
spacePan.value ? 'pan' : activeTool.value,
|
|
200
|
+
)
|
|
201
|
+
|
|
115
202
|
/** The edit in flight, which holds its field until the server answers. */
|
|
116
203
|
export const pendingEdit = signal<PendingEdit | undefined>(undefined)
|
|
117
204
|
|
|
@@ -123,6 +210,14 @@ const SAVED_MS = 2500
|
|
|
123
210
|
/** Why the last edit wrote nothing, until the next one lands. */
|
|
124
211
|
export const editRefusal = signal<string | undefined>(undefined)
|
|
125
212
|
|
|
213
|
+
const NO_HISTORY: HistoryFlags = { canUndo: false, canRedo: false }
|
|
214
|
+
|
|
215
|
+
/** What the server's history holds, as the last page record reported it. */
|
|
216
|
+
export const history = signal<HistoryFlags>(NO_HISTORY)
|
|
217
|
+
|
|
218
|
+
/** Why the last undo or redo wrote nothing, until the next one runs. */
|
|
219
|
+
export const historyNotice = signal<string | undefined>(undefined)
|
|
220
|
+
|
|
126
221
|
/**
|
|
127
222
|
* The hash an edit answered by frame key, until that frame reloads. A second
|
|
128
223
|
* edit sent before the reload is made from the file the first one wrote, not
|
|
@@ -193,10 +288,63 @@ export function toggleLayers(key: string): void {
|
|
|
193
288
|
expandedFrames.value = next
|
|
194
289
|
}
|
|
195
290
|
|
|
291
|
+
/*
|
|
292
|
+
* An expiry writes no file and raises no change event, so nothing rereads the
|
|
293
|
+
* page list when a mark lapses. One timer, set for the earliest `until`, drops
|
|
294
|
+
* it instead, and every record resets it.
|
|
295
|
+
*/
|
|
296
|
+
let editingTimer: ReturnType<typeof setTimeout> | undefined
|
|
297
|
+
|
|
298
|
+
/*
|
|
299
|
+
* The longest delay a timer holds. A longer one fires at once, so an `until`
|
|
300
|
+
* far out would reschedule on every tick rather than wait.
|
|
301
|
+
*/
|
|
302
|
+
const MAX_TIMER_MS = 2 ** 31 - 1
|
|
303
|
+
|
|
304
|
+
function liveMarks(
|
|
305
|
+
marks: Iterable<readonly [string, EditingRef]>,
|
|
306
|
+
now: number,
|
|
307
|
+
): ReadonlyMap<string, EditingRef> {
|
|
308
|
+
return new Map([...marks].filter(([, mark]) => Date.parse(mark.until) > now))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function scheduleExpiry(): void {
|
|
312
|
+
clearTimeout(editingTimer)
|
|
313
|
+
editingTimer = undefined
|
|
314
|
+
const next = Math.min(
|
|
315
|
+
...[...editingFrames.value.values()].map((mark) => Date.parse(mark.until)),
|
|
316
|
+
)
|
|
317
|
+
if (!Number.isFinite(next)) return
|
|
318
|
+
editingTimer = setTimeout(
|
|
319
|
+
() => {
|
|
320
|
+
editingFrames.value = liveMarks(editingFrames.value, Date.now())
|
|
321
|
+
scheduleExpiry()
|
|
322
|
+
},
|
|
323
|
+
Math.min(MAX_TIMER_MS, Math.max(0, next - Date.now())),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Keys each mark by the file of the frame it names, as the surface reads. */
|
|
328
|
+
function applyEditing(
|
|
329
|
+
all: readonly Page[],
|
|
330
|
+
marks: readonly EditingRef[],
|
|
331
|
+
): void {
|
|
332
|
+
const keyed = marks.flatMap((mark) => {
|
|
333
|
+
const frame = all
|
|
334
|
+
.find((candidate) => candidate.name === mark.page)
|
|
335
|
+
?.frames.find((candidate) => candidate.name === mark.frame)
|
|
336
|
+
return frame ? [[frameKey(mark.page, frame), mark] as const] : []
|
|
337
|
+
})
|
|
338
|
+
editingFrames.value = liveMarks(keyed, Date.now())
|
|
339
|
+
scheduleExpiry()
|
|
340
|
+
}
|
|
341
|
+
|
|
196
342
|
export function applyRecord(record: PagesRecord): void {
|
|
197
343
|
pages.value = record.pages
|
|
198
344
|
tokens.value = record.tokens
|
|
199
345
|
selection.value = record.selection ?? undefined
|
|
346
|
+
applyEditing(record.pages, record.editing ?? [])
|
|
347
|
+
history.value = record.history ?? NO_HISTORY
|
|
200
348
|
loadError.value = undefined
|
|
201
349
|
isLoaded.value = true
|
|
202
350
|
}
|
|
@@ -317,11 +465,22 @@ const EDIT_NOTICES: Readonly<Record<string, string | undefined>> = {
|
|
|
317
465
|
'Could not save the text, since this element holds other elements.',
|
|
318
466
|
}
|
|
319
467
|
|
|
468
|
+
let stepCount = 0
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* A token the edits of one gesture share, so the server merges them into one
|
|
472
|
+
* undo. The time keeps it apart from a token an earlier shell load sent.
|
|
473
|
+
*/
|
|
474
|
+
export function newStep(): string {
|
|
475
|
+
stepCount += 1
|
|
476
|
+
return `${Date.now().toString(36)}-${stepCount}`
|
|
477
|
+
}
|
|
478
|
+
|
|
320
479
|
/**
|
|
321
480
|
* Posts one property change to the element the address names. A refusal
|
|
322
481
|
* says why and reloads the frame, so a pending value never sits over a file
|
|
323
482
|
* that moved under it. Resolves to what the write answered, or undefined when
|
|
324
|
-
* nothing was written.
|
|
483
|
+
* nothing was written. Edits carrying one step undo as one.
|
|
325
484
|
*/
|
|
326
485
|
export async function editElement(
|
|
327
486
|
ref: FrameRef,
|
|
@@ -329,6 +488,7 @@ export async function editElement(
|
|
|
329
488
|
address: ElementAddress,
|
|
330
489
|
property: string,
|
|
331
490
|
value: string,
|
|
491
|
+
step?: string,
|
|
332
492
|
fetchImpl: typeof fetch = fetch,
|
|
333
493
|
): Promise<{ readonly hash?: string } | undefined> {
|
|
334
494
|
const hash = editedHashes.value.get(key) ?? address.hash
|
|
@@ -342,6 +502,7 @@ export async function editElement(
|
|
|
342
502
|
element: { ...address, hash },
|
|
343
503
|
property,
|
|
344
504
|
value,
|
|
505
|
+
...(step !== undefined && { step }),
|
|
345
506
|
}),
|
|
346
507
|
})
|
|
347
508
|
const body: unknown = await response.json().catch(() => undefined)
|
|
@@ -362,6 +523,7 @@ export async function editElement(
|
|
|
362
523
|
return undefined
|
|
363
524
|
}
|
|
364
525
|
editRefusal.value = undefined
|
|
526
|
+
historyNotice.value = undefined
|
|
365
527
|
const saved = pendingEdit.value
|
|
366
528
|
savedEdit.value = saved
|
|
367
529
|
setTimeout(() => {
|
|
@@ -381,10 +543,11 @@ export async function editElement(
|
|
|
381
543
|
}
|
|
382
544
|
|
|
383
545
|
/**
|
|
384
|
-
* Writes an element's new size as one edit per changed axis, width first
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* first edit sends
|
|
546
|
+
* Writes an element's new size as one edit per changed axis, width first,
|
|
547
|
+
* sharing one step so a single undo restores both. The second carries the
|
|
548
|
+
* hash the first answered, so a frame reloading between the two cannot leave
|
|
549
|
+
* it holding the hash of a file already rewritten. A refused first edit sends
|
|
550
|
+
* no second.
|
|
388
551
|
*/
|
|
389
552
|
export async function resizeElement(
|
|
390
553
|
ref: FrameRef,
|
|
@@ -394,6 +557,7 @@ export async function resizeElement(
|
|
|
394
557
|
height: number | undefined,
|
|
395
558
|
fetchImpl: typeof fetch = fetch,
|
|
396
559
|
): Promise<void> {
|
|
560
|
+
const step = newStep()
|
|
397
561
|
let current = address
|
|
398
562
|
if (width !== undefined) {
|
|
399
563
|
const written = await editElement(
|
|
@@ -402,16 +566,79 @@ export async function resizeElement(
|
|
|
402
566
|
current,
|
|
403
567
|
'width',
|
|
404
568
|
`${width}px`,
|
|
569
|
+
step,
|
|
405
570
|
fetchImpl,
|
|
406
571
|
)
|
|
407
572
|
if (!written) return
|
|
408
573
|
if (written.hash) current = { ...current, hash: written.hash }
|
|
409
574
|
}
|
|
410
575
|
if (height !== undefined) {
|
|
411
|
-
await editElement(
|
|
576
|
+
await editElement(
|
|
577
|
+
ref,
|
|
578
|
+
key,
|
|
579
|
+
current,
|
|
580
|
+
'height',
|
|
581
|
+
`${height}px`,
|
|
582
|
+
step,
|
|
583
|
+
fetchImpl,
|
|
584
|
+
)
|
|
412
585
|
}
|
|
413
586
|
}
|
|
414
587
|
|
|
588
|
+
const HISTORY_NOTICES = {
|
|
589
|
+
undo: 'Could not undo, since that element or frame changed after your edit. Nothing was written.',
|
|
590
|
+
redo: 'Could not redo, since that element or frame changed after the undo. Nothing was written.',
|
|
591
|
+
} as const
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Asks the server to step its history and rereads the page list for the new
|
|
595
|
+
* flags. A written step clears every hash an edit answered, since the frame
|
|
596
|
+
* it named was just rewritten under it. The frame itself reloads on the
|
|
597
|
+
* change event the write raises.
|
|
598
|
+
*/
|
|
599
|
+
async function stepHistory(
|
|
600
|
+
direction: 'undo' | 'redo',
|
|
601
|
+
fetchImpl: typeof fetch,
|
|
602
|
+
): Promise<void> {
|
|
603
|
+
try {
|
|
604
|
+
const response = await fetchImpl(`/api/history/${direction}`, {
|
|
605
|
+
method: 'POST',
|
|
606
|
+
headers: { 'content-type': 'application/json' },
|
|
607
|
+
body: '{}',
|
|
608
|
+
})
|
|
609
|
+
const body: unknown = await response.json().catch(() => undefined)
|
|
610
|
+
const outcome =
|
|
611
|
+
typeof body === 'object' && body !== null && 'outcome' in body
|
|
612
|
+
? body.outcome
|
|
613
|
+
: undefined
|
|
614
|
+
if (!response.ok) {
|
|
615
|
+
historyNotice.value = `Could not ${direction} (status ${response.status}). Try again.`
|
|
616
|
+
} else if (outcome === 'dropped') {
|
|
617
|
+
historyNotice.value = HISTORY_NOTICES[direction]
|
|
618
|
+
} else if (outcome === 'applied') {
|
|
619
|
+
const done = direction === 'undo' ? 'Undone' : 'Redone'
|
|
620
|
+
historyNotice.value = done
|
|
621
|
+
setTimeout(() => {
|
|
622
|
+
if (historyNotice.value === done) historyNotice.value = undefined
|
|
623
|
+
}, SAVED_MS)
|
|
624
|
+
} else {
|
|
625
|
+
historyNotice.value = undefined
|
|
626
|
+
}
|
|
627
|
+
if (outcome === 'applied') editedHashes.value = new Map()
|
|
628
|
+
} catch (error) {
|
|
629
|
+
historyNotice.value = `Could not ${direction} (${error instanceof Error ? error.message : 'unknown'}). Check canon canvas serve is still running.`
|
|
630
|
+
}
|
|
631
|
+
await loadPages(fetchImpl)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function undo(fetchImpl: typeof fetch = fetch): Promise<void> {
|
|
635
|
+
return stepHistory('undo', fetchImpl)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function redo(fetchImpl: typeof fetch = fetch): Promise<void> {
|
|
639
|
+
return stepHistory('redo', fetchImpl)
|
|
640
|
+
}
|
|
641
|
+
|
|
415
642
|
/**
|
|
416
643
|
* Moves a frame on the surface without writing, which a drag calls on every
|
|
417
644
|
* pointer move. The next reread replaces it with what the layout holds.
|
|
@@ -487,11 +714,19 @@ export function panBy(dx: number, dy: number): void {
|
|
|
487
714
|
view.value = { ...current, x: current.x + dx, y: current.y + dy }
|
|
488
715
|
}
|
|
489
716
|
|
|
490
|
-
/**
|
|
491
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Fits every frame on the current page into a viewport of the given size,
|
|
719
|
+
* clear of `insetLeft` pixels of chrome floating over its left edge.
|
|
720
|
+
*/
|
|
721
|
+
export function fitView(
|
|
722
|
+
width: number,
|
|
723
|
+
height: number,
|
|
724
|
+
padding = 48,
|
|
725
|
+
insetLeft = 0,
|
|
726
|
+
): void {
|
|
492
727
|
const frames = currentPage.value?.frames ?? []
|
|
493
728
|
if (frames.length === 0 || width <= 0 || height <= 0) {
|
|
494
|
-
view.value = { x: padding, y: padding, zoom: 0.5 }
|
|
729
|
+
view.value = { x: insetLeft + padding, y: padding, zoom: 0.5 }
|
|
495
730
|
return
|
|
496
731
|
}
|
|
497
732
|
const left = Math.min(...frames.map((frame) => frame.x))
|
|
@@ -500,14 +735,14 @@ export function fitView(width: number, height: number, padding = 48): void {
|
|
|
500
735
|
const bottom = Math.max(...frames.map((frame) => frame.y + frame.height))
|
|
501
736
|
const zoom = clampZoom(
|
|
502
737
|
Math.min(
|
|
503
|
-
(width - padding * 2) / (right - left),
|
|
738
|
+
(width - insetLeft - padding * 2) / (right - left),
|
|
504
739
|
(height - padding * 2) / (bottom - top),
|
|
505
740
|
1,
|
|
506
741
|
),
|
|
507
742
|
)
|
|
508
743
|
view.value = {
|
|
509
744
|
zoom,
|
|
510
|
-
x: padding - left * zoom,
|
|
745
|
+
x: insetLeft + padding - left * zoom,
|
|
511
746
|
y: padding - top * zoom,
|
|
512
747
|
}
|
|
513
748
|
}
|
|
@@ -537,12 +772,21 @@ export function resetState(): void {
|
|
|
537
772
|
selection.value = undefined
|
|
538
773
|
frameDocuments.value = new Map()
|
|
539
774
|
hoveredElement.value = undefined
|
|
775
|
+
editingFrames.value = new Map()
|
|
776
|
+
clearTimeout(editingTimer)
|
|
777
|
+
editingTimer = undefined
|
|
540
778
|
expandedFrames.value = new Set()
|
|
541
779
|
draggingFrame.value = undefined
|
|
542
780
|
writeError.value = undefined
|
|
543
781
|
leftTab.value = 'pages'
|
|
782
|
+
activeTool.value = 'move'
|
|
783
|
+
spacePan.value = undefined
|
|
544
784
|
pendingEdit.value = undefined
|
|
545
785
|
savedEdit.value = undefined
|
|
546
786
|
editRefusal.value = undefined
|
|
547
787
|
editedHashes.value = new Map()
|
|
788
|
+
history.value = NO_HISTORY
|
|
789
|
+
historyNotice.value = undefined
|
|
790
|
+
panelsHidden.value = false
|
|
791
|
+
panelWidths.value = DEFAULT_PANEL_WIDTHS
|
|
548
792
|
}
|