@erclx/canon 5.26.0 → 5.28.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 (320) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +2 -0
  4. package/claude/skills/auto-ship/SKILL.md +3 -11
  5. package/claude/skills/auto-ship/references/review-findings.md +20 -0
  6. package/claude/skills/backlog-triage/SKILL.md +2 -0
  7. package/claude/skills/bash-cli-script/SKILL.md +2 -0
  8. package/claude/skills/build-in-slices/SKILL.md +2 -0
  9. package/claude/skills/canon-cli/SKILL.md +2 -0
  10. package/claude/skills/canon-feedback/SKILL.md +2 -0
  11. package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
  12. package/claude/skills/canon-operator/SKILL.md +2 -0
  13. package/claude/skills/canon-rollout/SKILL.md +3 -11
  14. package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
  15. package/claude/skills/canvas/SKILL.md +13 -0
  16. package/claude/skills/ci-workflow/SKILL.md +2 -0
  17. package/claude/skills/code-craft/SKILL.md +2 -0
  18. package/claude/skills/codebase-layout/SKILL.md +2 -0
  19. package/claude/skills/context-fold/SKILL.md +3 -17
  20. package/claude/skills/context-fold/references/completion-report.md +26 -0
  21. package/claude/skills/create-rule/SKILL.md +2 -0
  22. package/claude/skills/create-skill/SKILL.md +2 -0
  23. package/claude/skills/create-standard/SKILL.md +2 -0
  24. package/claude/skills/decision-escalate/SKILL.md +2 -0
  25. package/claude/skills/deploy-app/SKILL.md +2 -0
  26. package/claude/skills/deprecation-migration/SKILL.md +2 -0
  27. package/claude/skills/design-extract/SKILL.md +2 -0
  28. package/claude/skills/design-taste/SKILL.md +2 -0
  29. package/claude/skills/docs-sync/SKILL.md +2 -0
  30. package/claude/skills/document-health/SKILL.md +2 -0
  31. package/claude/skills/draft-and-pick/SKILL.md +2 -0
  32. package/claude/skills/draft-doc/SKILL.md +2 -0
  33. package/claude/skills/draft-doc/references/wiki.md +1 -1
  34. package/claude/skills/draft-figure/SKILL.md +2 -0
  35. package/claude/skills/draft-identity/SKILL.md +2 -0
  36. package/claude/skills/draft-ready/SKILL.md +5 -1
  37. package/claude/skills/draft-screencast/SKILL.md +2 -0
  38. package/claude/skills/draft-slides/SKILL.md +2 -0
  39. package/claude/skills/git-branch/SKILL.md +2 -0
  40. package/claude/skills/git-commit/SKILL.md +2 -0
  41. package/claude/skills/git-followup/SKILL.md +2 -0
  42. package/claude/skills/git-issue/SKILL.md +2 -0
  43. package/claude/skills/git-pr/SKILL.md +2 -0
  44. package/claude/skills/git-pr/references/evidence.md +7 -7
  45. package/claude/skills/git-ship/SKILL.md +2 -0
  46. package/claude/skills/git-split/SKILL.md +2 -0
  47. package/claude/skills/git-stage/SKILL.md +2 -0
  48. package/claude/skills/git-worktree/SKILL.md +2 -0
  49. package/claude/skills/index-lookup/SKILL.md +2 -0
  50. package/claude/skills/markdown-propose/SKILL.md +2 -0
  51. package/claude/skills/memory-capture/SKILL.md +2 -0
  52. package/claude/skills/memory-review/SKILL.md +2 -0
  53. package/claude/skills/plan-feature/SKILL.md +2 -0
  54. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  55. package/claude/skills/plan-intake/SKILL.md +2 -0
  56. package/claude/skills/plan-intake-answer/SKILL.md +2 -0
  57. package/claude/skills/project-commands/SKILL.md +2 -0
  58. package/claude/skills/read-frames/SKILL.md +2 -0
  59. package/claude/skills/record-screencast/SKILL.md +2 -0
  60. package/claude/skills/repo-metadata/SKILL.md +2 -0
  61. package/claude/skills/restate-plainly/SKILL.md +2 -0
  62. package/claude/skills/review-address/REQUIREMENT.md +4 -2
  63. package/claude/skills/review-address/SKILL.md +19 -14
  64. package/claude/skills/review-address/references/post-review-findings.md +16 -0
  65. package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
  66. package/claude/skills/review-branch/SKILL.md +2 -0
  67. package/claude/skills/review-craft/SKILL.md +2 -0
  68. package/claude/skills/review-craft/references/ui.md +6 -6
  69. package/claude/skills/review-pr/SKILL.md +4 -8
  70. package/claude/skills/review-pr/references/body-variants.md +52 -0
  71. package/claude/skills/review-pr/references/key-changes.md +16 -0
  72. package/claude/skills/review-pr/references/post.md +15 -43
  73. package/claude/skills/review-ui/REQUIREMENT.md +2 -2
  74. package/claude/skills/review-ui/SKILL.md +16 -12
  75. package/claude/skills/review-ui/references/driving.md +8 -8
  76. package/claude/skills/review-ui/references/post.md +13 -3
  77. package/claude/skills/review-ui/references/tick.md +22 -0
  78. package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
  79. package/claude/skills/role-orchestrator/SKILL.md +31 -42
  80. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
  81. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +13 -3
  82. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
  83. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -34
  84. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
  85. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
  86. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +12 -23
  87. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
  88. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
  89. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
  90. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
  91. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
  92. package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
  93. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  94. package/claude/skills/role-planner/SKILL.md +4 -2
  95. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  96. package/claude/skills/role-reviewer/SKILL.md +3 -1
  97. package/claude/skills/role-worker/SKILL.md +5 -2
  98. package/claude/skills/search-craft/SKILL.md +2 -0
  99. package/claude/skills/seed-sync/SKILL.md +2 -0
  100. package/claude/skills/session-compact/SKILL.md +2 -0
  101. package/claude/skills/session-map/SKILL.md +2 -0
  102. package/claude/skills/session-relay/SKILL.md +2 -0
  103. package/claude/skills/session-resume/SKILL.md +2 -0
  104. package/claude/skills/session-worktree/SKILL.md +3 -11
  105. package/claude/skills/session-worktree/references/install-command.md +20 -0
  106. package/claude/skills/sketch-design/SKILL.md +2 -0
  107. package/claude/skills/systematic-debugging/SKILL.md +2 -0
  108. package/claude/skills/target-check/SKILL.md +2 -0
  109. package/claude/skills/target-setup/SKILL.md +2 -0
  110. package/claude/skills/task-board/SKILL.md +2 -0
  111. package/claude/skills/teach-workspace/SKILL.md +5 -15
  112. package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
  113. package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
  114. package/claude/skills/teach-workspace/references/revisit.md +22 -0
  115. package/claude/skills/test-craft/SKILL.md +2 -0
  116. package/claude/skills/test-first/SKILL.md +2 -0
  117. package/claude/skills/ui-checklist/SKILL.md +3 -1
  118. package/claude/skills/ux-audit/SKILL.md +2 -0
  119. package/claude/skills/ux-measure/SKILL.md +2 -0
  120. package/claude/skills/ux-walkthrough/SKILL.md +2 -0
  121. package/claude/skills/video-craft/SKILL.md +2 -0
  122. package/claude/skills/write-human/SKILL.md +2 -0
  123. package/claude/skills/youtube-transcripts/SKILL.md +2 -0
  124. package/docs/agents/canvas.md +24 -8
  125. package/docs/agents/capture.md +5 -5
  126. package/docs/agents/commands.md +2 -2
  127. package/docs/agents/gate.md +3 -1
  128. package/docs/agents/index.md +5 -4
  129. package/docs/agents/install-and-sync.md +7 -0
  130. package/docs/agents/key-changes.md +1 -1
  131. package/docs/agents/pr-evidence.md +47 -23
  132. package/docs/agents/pr-local.md +19 -13
  133. package/docs/agents/pr-preview.md +44 -21
  134. package/docs/agents/pr-tick.md +72 -0
  135. package/docs/agents/sandbox.md +30 -6
  136. package/docs/agents/scripting.md +8 -1
  137. package/docs/agents/skills-audit.md +2 -1
  138. package/docs/agents/tasks-plans.md +1 -1
  139. package/docs/agents/tasks-validate.md +18 -18
  140. package/docs/agents/tasks.md +1 -1
  141. package/docs/workflow/operating-model.md +15 -21
  142. package/docs/workflow/parallel-features.md +24 -3
  143. package/docs/workflow/skill-map.md +3 -1
  144. package/docs/workflow/zshrc-aliases.md +1 -1
  145. package/package.json +1 -1
  146. package/scripts/core/regen-hero.ts +1 -1
  147. package/scripts/lib/sandbox-path.sh +6 -162
  148. package/src/audits/baseline.ts +1 -1
  149. package/src/audits/run.ts +2 -2
  150. package/src/canvas/address.ts +12 -3
  151. package/src/canvas/capture.ts +1 -1
  152. package/src/canvas/client/inspector/align-grid.tsx +162 -0
  153. package/src/canvas/client/inspector/color-field.tsx +201 -171
  154. package/src/canvas/client/inspector/color-inputs.tsx +139 -0
  155. package/src/canvas/client/inspector/color-picker.tsx +668 -0
  156. package/src/canvas/client/inspector/color-space.ts +240 -0
  157. package/src/canvas/client/inspector/field.tsx +66 -3
  158. package/src/canvas/client/inspector/fields.css +473 -6
  159. package/src/canvas/client/inspector/size-field.tsx +69 -0
  160. package/src/canvas/client/inspector/token-select.tsx +109 -0
  161. package/src/canvas/client/inspector/values.ts +117 -3
  162. package/src/canvas/client/inspector.tsx +311 -15
  163. package/src/canvas/client/keys.ts +54 -0
  164. package/src/canvas/client/selection.tsx +199 -0
  165. package/src/canvas/client/shell.css +210 -12
  166. package/src/canvas/client/state.ts +165 -14
  167. package/src/canvas/client/surface.tsx +341 -39
  168. package/src/canvas/client/tools.tsx +64 -0
  169. package/src/canvas/content.ts +51 -2
  170. package/src/canvas/edit.ts +49 -6
  171. package/src/canvas/editing.ts +203 -0
  172. package/src/canvas/global.d.ts +15 -0
  173. package/src/canvas/server.ts +149 -12
  174. package/src/canvas/tokens.ts +60 -18
  175. package/src/census/count.ts +1 -1
  176. package/src/claude/seeds.ts +3 -3
  177. package/src/claude/skills-audit.ts +28 -0
  178. package/src/claude/skills-drift.ts +1 -1
  179. package/src/claude/skills-families.ts +36 -0
  180. package/src/claude/skills-list.ts +76 -15
  181. package/src/claude/skills-reach.ts +1 -1
  182. package/src/commands/audits.ts +2 -2
  183. package/src/commands/canvas.ts +180 -3
  184. package/src/commands/claude.ts +70 -12
  185. package/src/commands/design.ts +145 -5
  186. package/src/commands/docs.ts +1 -1
  187. package/src/commands/feedback.ts +2 -2
  188. package/src/commands/gate.ts +9 -2
  189. package/src/commands/gov.ts +3 -3
  190. package/src/commands/hooks.ts +1 -1
  191. package/src/commands/indexes.ts +1 -1
  192. package/src/commands/init.ts +2 -2
  193. package/src/commands/intake.ts +1 -1
  194. package/src/commands/migrate.ts +2 -2
  195. package/src/commands/pr.ts +288 -65
  196. package/src/commands/records.ts +1 -1
  197. package/src/commands/repo.ts +1 -1
  198. package/src/commands/sandbox.ts +64 -15
  199. package/src/commands/sessions.ts +1 -1
  200. package/src/commands/slides.ts +1 -1
  201. package/src/commands/sync.ts +2 -2
  202. package/src/commands/tasks.ts +21 -4
  203. package/src/commands/teach.ts +1 -1
  204. package/src/commands/tooling.ts +1 -1
  205. package/src/commands/transcripts.ts +2 -2
  206. package/src/commands/upgrade.ts +1 -1
  207. package/src/commands/wiki.ts +1 -1
  208. package/src/commands/worktrees.ts +1 -1
  209. package/src/comments/scan.ts +1 -1
  210. package/src/comments/trend.ts +1 -1
  211. package/src/context/architecture.ts +1 -1
  212. package/src/context/citations.ts +3 -3
  213. package/src/context/classify/extract.ts +3 -3
  214. package/src/context/folders.ts +2 -2
  215. package/src/context/requirements.ts +1 -1
  216. package/src/design/project-fonts.ts +392 -0
  217. package/src/docs/read.ts +1 -1
  218. package/src/exec.ts +1 -1
  219. package/src/feedback/github.ts +2 -2
  220. package/src/gate/measures.ts +48 -10
  221. package/src/gate/sequencer.ts +56 -4
  222. package/src/gate/stages.ts +17 -0
  223. package/src/gate/suite-lock.ts +214 -0
  224. package/src/{git-files.ts → git/files.ts} +2 -2
  225. package/src/{git-ignore.ts → git/ignore.ts} +1 -1
  226. package/src/{worktree.ts → git/worktree.ts} +1 -1
  227. package/src/gov/citations.ts +1 -1
  228. package/src/gov/install.ts +1 -1
  229. package/src/gov/superseded.ts +1 -1
  230. package/src/gov/test-order.ts +2 -2
  231. package/src/help.ts +1 -1
  232. package/src/hooks/post-merge.ts +1 -1
  233. package/src/indexes/walk.ts +1 -1
  234. package/src/intake/folder.ts +1 -1
  235. package/src/labels/audit.ts +1 -1
  236. package/src/labels/map.ts +1 -1
  237. package/src/labels/phase.ts +3 -3
  238. package/src/markdown/files.ts +1 -1
  239. package/src/migrate/apply.ts +1 -1
  240. package/src/migrate/record-layout.ts +1 -1
  241. package/src/migrate/records.ts +11 -11
  242. package/src/migrate/scratch-evidence.ts +1 -1
  243. package/src/migrate/surface-roots.ts +6 -6
  244. package/src/pr/bijection.ts +1 -1
  245. package/src/pr/evidence.ts +250 -59
  246. package/src/pr/local.ts +16 -10
  247. package/src/pr/preview.ts +78 -2
  248. package/src/pr/tick.ts +56 -0
  249. package/src/process/harness.ts +1 -1
  250. package/src/records/backup.ts +2 -2
  251. package/src/records/ordinal.ts +1 -1
  252. package/src/records/prune.ts +1 -1
  253. package/src/records/size.ts +1 -1
  254. package/src/records/stale-canonical.ts +2 -2
  255. package/src/records/stale.ts +1 -1
  256. package/src/records/validate.ts +2 -2
  257. package/src/{project-root.ts → roots/project.ts} +1 -1
  258. package/src/{record-root.ts → roots/record.ts} +1 -1
  259. package/src/sandbox/coverage.ts +11 -12
  260. package/src/sandbox/equivalence.ts +57 -26
  261. package/src/sandbox/headless/dispatch.ts +264 -0
  262. package/src/sandbox/headless/run.ts +653 -0
  263. package/src/sandbox/headless/watch.ts +145 -0
  264. package/src/sandbox/hooks.ts +1 -1
  265. package/src/sandbox/provision.ts +90 -29
  266. package/src/sandbox/scenario.ts +588 -0
  267. package/src/sandbox/tree.ts +4 -8
  268. package/src/secrets/scan.ts +1 -1
  269. package/src/sessions/claim.ts +1 -1
  270. package/src/sessions/live.ts +4 -1
  271. package/src/sessions/resolve.ts +1 -1
  272. package/src/sessions/transfer/bundle.ts +2 -2
  273. package/src/slides/decks.ts +1 -1
  274. package/src/slides/package/fonts.ts +6 -2
  275. package/src/standards/audit.ts +2 -2
  276. package/src/standards/read.ts +1 -1
  277. package/src/sync/check.ts +2 -2
  278. package/src/sync/engine.ts +3 -3
  279. package/src/sync/git.ts +1 -1
  280. package/src/sync/history.ts +1 -1
  281. package/src/sync/layout.ts +2 -2
  282. package/src/sync/reverse.ts +1 -1
  283. package/src/sync/stamp.ts +1 -1
  284. package/src/sync/target.ts +1 -1
  285. package/src/sync/workflow.ts +1 -1
  286. package/src/targets/pulls.ts +2 -2
  287. package/src/targets/sweep.ts +2 -2
  288. package/src/{target.ts → targets/validate.ts} +1 -1
  289. package/src/tasks/answers.ts +1 -1
  290. package/src/tasks/archive.ts +1 -1
  291. package/src/tasks/label.ts +1 -1
  292. package/src/tasks/reach.ts +2 -2
  293. package/src/tasks/trunk.ts +1 -1
  294. package/src/tasks/validate.ts +38 -14
  295. package/src/teach/nav.ts +1 -1
  296. package/src/teach/workspace.ts +1 -1
  297. package/src/tooling/inject.ts +2 -2
  298. package/src/tooling/read.ts +1 -1
  299. package/src/tooling/scan.ts +1 -1
  300. package/src/tooling/subfolder.ts +1 -1
  301. package/src/version/installed.ts +1 -1
  302. package/src/version/skew.ts +1 -1
  303. package/src/web/readme-citations.ts +1 -1
  304. package/src/worktrees/bare-flag.ts +1 -1
  305. package/src/worktrees/reclaim.ts +2 -2
  306. package/src/worktrees/remove.ts +1 -1
  307. package/standards/board.md +3 -3
  308. package/standards/canvas.md +2 -1
  309. package/standards/plan.md +13 -7
  310. package/standards/skill.md +1 -0
  311. package/standards/wiki.md +12 -3
  312. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
  313. package/tsconfig.json +1 -1
  314. package/scripts/lib/sandbox-dispatch.sh +0 -190
  315. /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
  316. /package/src/{git-base.ts → git/base.ts} +0 -0
  317. /package/src/{git-env.ts → git/env.ts} +0 -0
  318. /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
  319. /package/src/{surface-root.ts → roots/surface.ts} +0 -0
  320. /package/src/{copy.ts → sync/copy.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { computed, signal } from '@preact/signals'
2
2
  import type { ElementAddress } from '@/canvas/address'
3
- import type { Frame, Page } from '@/canvas/content'
3
+ import type { Box, Frame, Page } from '@/canvas/content'
4
4
  import type { TokenGroup } from '@/canvas/tokens'
5
5
 
6
6
  /**
@@ -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,19 @@ 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[]
53
66
  }
54
67
 
55
68
  export interface ChangeEvent {
@@ -101,6 +114,9 @@ export const hoveredElement = signal<
101
114
  { readonly key: string; readonly index: number } | undefined
102
115
  >(undefined)
103
116
 
117
+ /** Each marked frame's mark by frame key, dropped at its `until`. */
118
+ export const editingFrames = signal<ReadonlyMap<string, EditingRef>>(new Map())
119
+
104
120
  /** Frame keys whose layers are open in the pages panel. */
105
121
  export const expandedFrames = signal<ReadonlySet<string>>(new Set())
106
122
 
@@ -112,6 +128,16 @@ export const writeError = signal<string | undefined>(undefined)
112
128
 
113
129
  export const leftTab = signal<LeftTab>('pages')
114
130
 
131
+ /** The tool the strip has lit, which a Space pan overrides while held. */
132
+ export const activeTool = signal<Tool>('move')
133
+
134
+ export const spacePan = signal<SpacePan | undefined>(undefined)
135
+
136
+ /** The tool in effect, so Space shows Pan lit and releasing it restores it. */
137
+ export const shownTool = computed<Tool>(() =>
138
+ spacePan.value ? 'pan' : activeTool.value,
139
+ )
140
+
115
141
  /** The edit in flight, which holds its field until the server answers. */
116
142
  export const pendingEdit = signal<PendingEdit | undefined>(undefined)
117
143
 
@@ -193,10 +219,62 @@ export function toggleLayers(key: string): void {
193
219
  expandedFrames.value = next
194
220
  }
195
221
 
222
+ /*
223
+ * An expiry writes no file and raises no change event, so nothing rereads the
224
+ * page list when a mark lapses. One timer, set for the earliest `until`, drops
225
+ * it instead, and every record resets it.
226
+ */
227
+ let editingTimer: ReturnType<typeof setTimeout> | undefined
228
+
229
+ /*
230
+ * The longest delay a timer holds. A longer one fires at once, so an `until`
231
+ * far out would reschedule on every tick rather than wait.
232
+ */
233
+ const MAX_TIMER_MS = 2 ** 31 - 1
234
+
235
+ function liveMarks(
236
+ marks: Iterable<readonly [string, EditingRef]>,
237
+ now: number,
238
+ ): ReadonlyMap<string, EditingRef> {
239
+ return new Map([...marks].filter(([, mark]) => Date.parse(mark.until) > now))
240
+ }
241
+
242
+ function scheduleExpiry(): void {
243
+ clearTimeout(editingTimer)
244
+ editingTimer = undefined
245
+ const next = Math.min(
246
+ ...[...editingFrames.value.values()].map((mark) => Date.parse(mark.until)),
247
+ )
248
+ if (!Number.isFinite(next)) return
249
+ editingTimer = setTimeout(
250
+ () => {
251
+ editingFrames.value = liveMarks(editingFrames.value, Date.now())
252
+ scheduleExpiry()
253
+ },
254
+ Math.min(MAX_TIMER_MS, Math.max(0, next - Date.now())),
255
+ )
256
+ }
257
+
258
+ /** Keys each mark by the file of the frame it names, as the surface reads. */
259
+ function applyEditing(
260
+ all: readonly Page[],
261
+ marks: readonly EditingRef[],
262
+ ): void {
263
+ const keyed = marks.flatMap((mark) => {
264
+ const frame = all
265
+ .find((candidate) => candidate.name === mark.page)
266
+ ?.frames.find((candidate) => candidate.name === mark.frame)
267
+ return frame ? [[frameKey(mark.page, frame), mark] as const] : []
268
+ })
269
+ editingFrames.value = liveMarks(keyed, Date.now())
270
+ scheduleExpiry()
271
+ }
272
+
196
273
  export function applyRecord(record: PagesRecord): void {
197
274
  pages.value = record.pages
198
275
  tokens.value = record.tokens
199
276
  selection.value = record.selection ?? undefined
277
+ applyEditing(record.pages, record.editing ?? [])
200
278
  loadError.value = undefined
201
279
  isLoaded.value = true
202
280
  }
@@ -320,7 +398,8 @@ const EDIT_NOTICES: Readonly<Record<string, string | undefined>> = {
320
398
  /**
321
399
  * Posts one property change to the element the address names. A refusal
322
400
  * says why and reloads the frame, so a pending value never sits over a file
323
- * that moved under it.
401
+ * that moved under it. Resolves to what the write answered, or undefined when
402
+ * nothing was written.
324
403
  */
325
404
  export async function editElement(
326
405
  ref: FrameRef,
@@ -329,7 +408,7 @@ export async function editElement(
329
408
  property: string,
330
409
  value: string,
331
410
  fetchImpl: typeof fetch = fetch,
332
- ): Promise<void> {
411
+ ): Promise<{ readonly hash?: string } | undefined> {
333
412
  const hash = editedHashes.value.get(key) ?? address.hash
334
413
  pendingEdit.value = { key, index: address.index, property }
335
414
  try {
@@ -358,7 +437,7 @@ export async function editElement(
358
437
  : `Could not save (status ${response.status}). Check canon canvas serve is still running.`)
359
438
  reloadFrame(key)
360
439
  await loadPages(fetchImpl)
361
- return
440
+ return undefined
362
441
  }
363
442
  editRefusal.value = undefined
364
443
  const saved = pendingEdit.value
@@ -366,18 +445,51 @@ export async function editElement(
366
445
  setTimeout(() => {
367
446
  if (savedEdit.value === saved) savedEdit.value = undefined
368
447
  }, SAVED_MS)
369
- if (typeof record.hash === 'string') {
370
- const hashes = new Map(editedHashes.value)
371
- hashes.set(key, record.hash)
372
- editedHashes.value = hashes
373
- }
448
+ if (typeof record.hash !== 'string') return {}
449
+ const hashes = new Map(editedHashes.value)
450
+ hashes.set(key, record.hash)
451
+ editedHashes.value = hashes
452
+ return { hash: record.hash }
374
453
  } catch (error) {
375
454
  editRefusal.value = `Could not save (${error instanceof Error ? error.message : 'unknown'}). Check canon canvas serve is still running.`
455
+ return undefined
376
456
  } finally {
377
457
  pendingEdit.value = undefined
378
458
  }
379
459
  }
380
460
 
461
+ /**
462
+ * Writes an element's new size as one edit per changed axis, width first. The
463
+ * second carries the hash the first answered, so a frame reloading between the
464
+ * two cannot leave it holding the hash of a file already rewritten. A refused
465
+ * first edit sends no second.
466
+ */
467
+ export async function resizeElement(
468
+ ref: FrameRef,
469
+ key: string,
470
+ address: ElementAddress,
471
+ width: number | undefined,
472
+ height: number | undefined,
473
+ fetchImpl: typeof fetch = fetch,
474
+ ): Promise<void> {
475
+ let current = address
476
+ if (width !== undefined) {
477
+ const written = await editElement(
478
+ ref,
479
+ key,
480
+ current,
481
+ 'width',
482
+ `${width}px`,
483
+ fetchImpl,
484
+ )
485
+ if (!written) return
486
+ if (written.hash) current = { ...current, hash: written.hash }
487
+ }
488
+ if (height !== undefined) {
489
+ await editElement(ref, key, current, 'height', `${height}px`, fetchImpl)
490
+ }
491
+ }
492
+
381
493
  /**
382
494
  * Moves a frame on the surface without writing, which a drag calls on every
383
495
  * pointer move. The next reread replaces it with what the layout holds.
@@ -406,6 +518,32 @@ export async function moveFrameTo(
406
518
  await write('/api/frames/move', { ...ref, x, y }, fetchImpl)
407
519
  }
408
520
 
521
+ /** Resizes a frame on the surface without writing, as a handle drag does. */
522
+ export function previewResize(ref: FrameRef, box: Box): void {
523
+ pages.value = pages.value.map((candidate) =>
524
+ candidate.name === ref.page
525
+ ? {
526
+ ...candidate,
527
+ frames: candidate.frames.map((frame) =>
528
+ frame.name === ref.frame
529
+ ? { ...frame, ...box, placed: true }
530
+ : frame,
531
+ ),
532
+ }
533
+ : candidate,
534
+ )
535
+ }
536
+
537
+ /** Shows the new box and writes it through the server's shared layout writer. */
538
+ export async function resizeFrameTo(
539
+ ref: FrameRef,
540
+ box: Box,
541
+ fetchImpl: typeof fetch = fetch,
542
+ ): Promise<void> {
543
+ previewResize(ref, box)
544
+ await write('/api/frames/resize', { ...ref, ...box }, fetchImpl)
545
+ }
546
+
409
547
  export function clampZoom(zoom: number): number {
410
548
  return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom))
411
549
  }
@@ -427,11 +565,19 @@ export function panBy(dx: number, dy: number): void {
427
565
  view.value = { ...current, x: current.x + dx, y: current.y + dy }
428
566
  }
429
567
 
430
- /** Fits every frame on the current page into a viewport of the given size. */
431
- export function fitView(width: number, height: number, padding = 48): void {
568
+ /**
569
+ * Fits every frame on the current page into a viewport of the given size,
570
+ * clear of `insetLeft` pixels of chrome floating over its left edge.
571
+ */
572
+ export function fitView(
573
+ width: number,
574
+ height: number,
575
+ padding = 48,
576
+ insetLeft = 0,
577
+ ): void {
432
578
  const frames = currentPage.value?.frames ?? []
433
579
  if (frames.length === 0 || width <= 0 || height <= 0) {
434
- view.value = { x: padding, y: padding, zoom: 0.5 }
580
+ view.value = { x: insetLeft + padding, y: padding, zoom: 0.5 }
435
581
  return
436
582
  }
437
583
  const left = Math.min(...frames.map((frame) => frame.x))
@@ -440,14 +586,14 @@ export function fitView(width: number, height: number, padding = 48): void {
440
586
  const bottom = Math.max(...frames.map((frame) => frame.y + frame.height))
441
587
  const zoom = clampZoom(
442
588
  Math.min(
443
- (width - padding * 2) / (right - left),
589
+ (width - insetLeft - padding * 2) / (right - left),
444
590
  (height - padding * 2) / (bottom - top),
445
591
  1,
446
592
  ),
447
593
  )
448
594
  view.value = {
449
595
  zoom,
450
- x: padding - left * zoom,
596
+ x: insetLeft + padding - left * zoom,
451
597
  y: padding - top * zoom,
452
598
  }
453
599
  }
@@ -477,10 +623,15 @@ export function resetState(): void {
477
623
  selection.value = undefined
478
624
  frameDocuments.value = new Map()
479
625
  hoveredElement.value = undefined
626
+ editingFrames.value = new Map()
627
+ clearTimeout(editingTimer)
628
+ editingTimer = undefined
480
629
  expandedFrames.value = new Set()
481
630
  draggingFrame.value = undefined
482
631
  writeError.value = undefined
483
632
  leftTab.value = 'pages'
633
+ activeTool.value = 'move'
634
+ spacePan.value = undefined
484
635
  pendingEdit.value = undefined
485
636
  savedEdit.value = undefined
486
637
  editRefusal.value = undefined