@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
@@ -0,0 +1,199 @@
1
+ /** @jsxImportSource preact */
2
+ import type { JSX } from 'preact'
3
+ import { useRef, useState } from 'preact/hooks'
4
+ import { shownTool, view } from '@/canvas/client/state'
5
+
6
+ /** Screen pixels a press travels before it reads as a drag and not a click. */
7
+ export const DRAG_THRESHOLD = 3
8
+
9
+ /** The smallest box a handle drag leaves, in surface units. */
10
+ const MIN_SIZE = 1
11
+
12
+ export type Corner = 'nw' | 'ne' | 'sw' | 'se'
13
+
14
+ const CORNERS: readonly Corner[] = ['nw', 'ne', 'sw', 'se']
15
+
16
+ export interface OverlayBox {
17
+ readonly x: number
18
+ readonly y: number
19
+ readonly width: number
20
+ readonly height: number
21
+ }
22
+
23
+ /**
24
+ * The box a corner drag leaves, with the delta already in surface units. The
25
+ * corner opposite the one dragged stays put, so a west or north handle moves
26
+ * the box as it resizes it.
27
+ */
28
+ export function resizedBox(
29
+ origin: OverlayBox,
30
+ corner: Corner,
31
+ dx: number,
32
+ dy: number,
33
+ ): OverlayBox {
34
+ const isWest = corner === 'nw' || corner === 'sw'
35
+ const isNorth = corner === 'nw' || corner === 'ne'
36
+ const width = Math.max(MIN_SIZE, origin.width + (isWest ? -dx : dx))
37
+ const height = Math.max(MIN_SIZE, origin.height + (isNorth ? -dy : dy))
38
+ return {
39
+ x: isWest ? origin.x + origin.width - width : origin.x,
40
+ y: isNorth ? origin.y + origin.height - height : origin.y,
41
+ width,
42
+ height,
43
+ }
44
+ }
45
+
46
+ export function sizeLabel(box: Pick<OverlayBox, 'width' | 'height'>): string {
47
+ return `${Math.round(box.width)} × ${Math.round(box.height)}`
48
+ }
49
+
50
+ interface HandleGesture {
51
+ readonly corner: Corner
52
+ readonly startX: number
53
+ readonly startY: number
54
+ readonly origin: OverlayBox
55
+ box: OverlayBox
56
+ moved: boolean
57
+ }
58
+
59
+ export interface SelectionOverlayProps {
60
+ readonly kind: 'frame' | 'element'
61
+ /** Read on every render, so a box the drag previewed is what shows after. */
62
+ readonly measure: () => OverlayBox
63
+ /** Subtracted from the box to place the overlay inside its container. */
64
+ readonly offset?: { readonly x: number; readonly y: number }
65
+ readonly hasChip?: boolean
66
+ /**
67
+ * Draws the target's own measured box during a drag rather than the dragged
68
+ * one. An element in normal flow grows from its own top left whichever corner
69
+ * is dragged, so only its measured box shows where it actually sits.
70
+ */
71
+ readonly isMeasuredWhileDragging?: boolean
72
+ readonly onStart?: () => void
73
+ readonly onPreview?: (box: OverlayBox, origin: OverlayBox) => void
74
+ readonly onCommit: (box: OverlayBox, origin: OverlayBox) => void
75
+ readonly onCancel?: (origin: OverlayBox) => void
76
+ }
77
+
78
+ /**
79
+ * The selected frame's or element's outline, its four corner handles, and an
80
+ * element's size chip. Every part sizes against `--outline-scale`, so it keeps
81
+ * one screen weight at any zoom.
82
+ */
83
+ export function SelectionOverlay({
84
+ kind,
85
+ measure,
86
+ offset = { x: 0, y: 0 },
87
+ hasChip = false,
88
+ isMeasuredWhileDragging = false,
89
+ onStart,
90
+ onPreview,
91
+ onCommit,
92
+ onCancel,
93
+ }: SelectionOverlayProps): JSX.Element {
94
+ const gesture = useRef<HandleGesture | undefined>(undefined)
95
+ const [dragged, setDragged] = useState<OverlayBox | undefined>(undefined)
96
+ const box = dragged ?? measure()
97
+
98
+ const handlePointerDown = (corner: Corner) => (event: PointerEvent) => {
99
+ if (event.button !== 0) return
100
+ /* A pan drag starting on a handle belongs to the surface behind it. */
101
+ if (shownTool.value === 'pan') return
102
+ event.preventDefault()
103
+ event.stopPropagation()
104
+ const origin = measure()
105
+ gesture.current = {
106
+ corner,
107
+ startX: event.clientX,
108
+ startY: event.clientY,
109
+ origin,
110
+ box: origin,
111
+ moved: false,
112
+ }
113
+ ;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
114
+ }
115
+
116
+ const handlePointerMove = (event: PointerEvent) => {
117
+ const current = gesture.current
118
+ if (!current) return
119
+ const dx = event.clientX - current.startX
120
+ const dy = event.clientY - current.startY
121
+ if (!current.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return
122
+ if (!current.moved) onStart?.()
123
+ current.moved = true
124
+ /* The delta is in screen pixels and the box in surface units. */
125
+ const { zoom } = view.value
126
+ current.box = resizedBox(
127
+ current.origin,
128
+ current.corner,
129
+ Math.round(dx / zoom),
130
+ Math.round(dy / zoom),
131
+ )
132
+ onPreview?.(current.box, current.origin)
133
+ setDragged(isMeasuredWhileDragging ? measure() : current.box)
134
+ }
135
+
136
+ const handlePointerUp = () => {
137
+ const current = gesture.current
138
+ gesture.current = undefined
139
+ if (current?.moved) onCommit(current.box, current.origin)
140
+ setDragged(undefined)
141
+ }
142
+
143
+ const handlePointerCancel = () => {
144
+ const current = gesture.current
145
+ gesture.current = undefined
146
+ if (current?.moved) onCancel?.(current.origin)
147
+ setDragged(undefined)
148
+ }
149
+
150
+ return (
151
+ <div
152
+ class="selection"
153
+ data-selection={kind}
154
+ data-resizing={dragged ? 'true' : undefined}
155
+ style={{
156
+ left: `${box.x - offset.x}px`,
157
+ top: `${box.y - offset.y}px`,
158
+ width: `${box.width}px`,
159
+ height: `${box.height}px`,
160
+ }}
161
+ >
162
+ {CORNERS.map((corner) => (
163
+ <span
164
+ key={corner}
165
+ class="selection-handle"
166
+ data-handle={corner}
167
+ aria-hidden="true"
168
+ onPointerDown={handlePointerDown(corner)}
169
+ onPointerMove={handlePointerMove}
170
+ onPointerUp={handlePointerUp}
171
+ onPointerCancel={handlePointerCancel}
172
+ />
173
+ ))}
174
+ {hasChip ? (
175
+ <span class="selection-chip" aria-hidden="true">
176
+ {sizeLabel(box)}
177
+ </span>
178
+ ) : null}
179
+ </div>
180
+ )
181
+ }
182
+
183
+ /** The element under the pointer, drawn without handles. */
184
+ export function HoverOutline({ element }: { readonly element: Element }) {
185
+ const rect = element.getBoundingClientRect()
186
+ return (
187
+ <div
188
+ class="element-outline"
189
+ data-outline="hover"
190
+ aria-hidden="true"
191
+ style={{
192
+ left: `${rect.left}px`,
193
+ top: `${rect.top}px`,
194
+ width: `${rect.width}px`,
195
+ height: `${rect.height}px`,
196
+ }}
197
+ />
198
+ )
199
+ }
@@ -217,6 +217,27 @@ code {
217
217
  cursor: grabbing;
218
218
  }
219
219
 
220
+ /* Inset, so the panels on either side never clip the ring. */
221
+ .surface:focus-visible {
222
+ outline-offset: -2px;
223
+ }
224
+
225
+ /*
226
+ * While panning, a frame takes no pointer, so a drag over one moves the view
227
+ * rather than picking an element, and the cursor reads the viewport's grab.
228
+ * The descendants are named too, since a selection handle sets its own
229
+ * pointer events and would otherwise take the drag as a resize. Nothing in
230
+ * the viewport starts a text selection while a pan drags across it.
231
+ */
232
+ .surface[data-tool='pan'] .viewport {
233
+ user-select: none;
234
+ }
235
+
236
+ .surface[data-tool='pan'] .frame,
237
+ .surface[data-tool='pan'] .frame * {
238
+ pointer-events: none;
239
+ }
240
+
220
241
  .plane {
221
242
  position: absolute;
222
243
  top: 0;
@@ -237,14 +258,33 @@ code {
237
258
  right: 0;
238
259
  display: flex;
239
260
  align-items: center;
240
- gap: var(--space-sm);
241
- padding-bottom: var(--space-sm);
242
- font-size: 1.5rem;
261
+ /*
262
+ * Sized against the zoom, so the label reads at one screen size at any zoom.
263
+ * A row too short for every part wraps the theme switch, then the editing
264
+ * badge, then the size onto lines the clip hides, so the name keeps the row
265
+ * longest.
266
+ */
267
+ flex-wrap: wrap;
268
+ align-content: flex-start;
269
+ column-gap: calc(6px * var(--outline-scale, 1));
270
+ row-gap: calc(40px * var(--outline-scale, 1));
271
+ height: calc(25px * var(--outline-scale, 1));
272
+ overflow: clip;
273
+ overflow-clip-margin: calc(4px * var(--outline-scale, 1));
274
+ padding-bottom: calc(6px * var(--outline-scale, 1));
275
+ font-size: calc(11px * var(--outline-scale, 1));
276
+ line-height: 1.3;
243
277
  color: var(--color-text-secondary);
244
278
  white-space: nowrap;
245
279
  }
246
280
 
281
+ /* A theme switch focused on a hidden line shows rather than staying hidden. */
282
+ .frame-label:focus-within {
283
+ overflow: visible;
284
+ }
285
+
247
286
  .frame-name {
287
+ min-width: 0;
248
288
  overflow: hidden;
249
289
  text-overflow: ellipsis;
250
290
  color: var(--color-text);
@@ -255,10 +295,45 @@ code {
255
295
  font-variant-numeric: tabular-nums;
256
296
  }
257
297
 
298
+ /*
299
+ * Ahead of the theme switch, so a row too short for both wraps the switch
300
+ * first. The editing outline still marks the frame when the badge wraps too.
301
+ */
302
+ .frame-editing {
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: calc(4px * var(--outline-scale, 1));
306
+ padding: calc(1px * var(--outline-scale, 1))
307
+ calc(6px * var(--outline-scale, 1));
308
+ border-radius: var(--radius-pill);
309
+ background: color-mix(in srgb, var(--color-success) 16%, transparent);
310
+ color: var(--color-success);
311
+ font-weight: 600;
312
+ }
313
+
314
+ .frame-editing:empty {
315
+ display: none;
316
+ }
317
+
318
+ .frame-editing::before {
319
+ content: '';
320
+ width: calc(6px * var(--outline-scale, 1));
321
+ height: calc(6px * var(--outline-scale, 1));
322
+ border-radius: 50%;
323
+ background: currentColor;
324
+ }
325
+
326
+ /* Dashed, so it reads apart from the solid accent of a selection. */
327
+ .frame[data-editing='true'] iframe {
328
+ outline: calc(2px * var(--outline-scale, 1)) dashed var(--color-success);
329
+ outline-offset: calc(2px * var(--outline-scale, 1));
330
+ }
331
+
258
332
  .frame-theme {
259
333
  margin-left: auto;
260
- padding: 2px var(--space-sm);
261
- border: 1px solid var(--color-border);
334
+ padding: calc(1px * var(--outline-scale, 1))
335
+ calc(6px * var(--outline-scale, 1));
336
+ border: calc(1px * var(--outline-scale, 1)) solid var(--color-border);
262
337
  border-radius: var(--radius-pill);
263
338
  background: var(--color-surface);
264
339
  color: var(--color-text-secondary);
@@ -301,18 +376,91 @@ code {
301
376
  outline: calc(1px * var(--outline-scale, 1)) dashed var(--color-accent);
302
377
  }
303
378
 
304
- .element-outline[data-outline='selected'] {
305
- outline: calc(2px * var(--outline-scale, 1)) solid var(--color-accent);
306
- }
307
-
308
379
  /* Above its neighbors, so a frame dragged over another stays whole. */
309
380
  .frame[data-selected='true'] {
310
381
  z-index: 1;
311
382
  }
312
383
 
313
- .frame[data-selected='true'] iframe {
314
- outline: 2px solid var(--color-accent);
315
- outline-offset: 2px;
384
+ /* A selected frame names itself in the accent, and a picked element does not. */
385
+ .frame[data-selected='true']:not([data-picked]) .frame-name {
386
+ color: var(--color-accent);
387
+ font-weight: 600;
388
+ }
389
+
390
+ /*
391
+ * The selected frame's or element's outline, handles, and size chip. Every
392
+ * length multiplies by the outline scale, so each holds one screen size.
393
+ */
394
+ .selection {
395
+ --handle: calc(8px * var(--outline-scale, 1));
396
+ --handle-hit: calc(24px * var(--outline-scale, 1));
397
+ position: absolute;
398
+ pointer-events: none;
399
+ outline: calc(1px * var(--outline-scale, 1)) solid var(--color-accent);
400
+ }
401
+
402
+ /* Inset by the iframe border, so the box lines up with the element it marks. */
403
+ .selection[data-selection='element'] {
404
+ translate: 1px 1px;
405
+ }
406
+
407
+ /* A 24 pixel hit area around an 8 pixel square, per the target size minimum. */
408
+ .selection-handle {
409
+ position: absolute;
410
+ width: var(--handle-hit);
411
+ height: var(--handle-hit);
412
+ margin: calc(var(--handle-hit) / -2);
413
+ pointer-events: auto;
414
+ touch-action: none;
415
+ }
416
+
417
+ .selection-handle::after {
418
+ content: '';
419
+ position: absolute;
420
+ inset: calc((var(--handle-hit) - var(--handle)) / 2);
421
+ border: calc(1px * var(--outline-scale, 1)) solid var(--color-accent);
422
+ background: var(--color-background);
423
+ }
424
+
425
+ .selection-handle[data-handle='nw'] {
426
+ top: 0;
427
+ left: 0;
428
+ cursor: nwse-resize;
429
+ }
430
+
431
+ .selection-handle[data-handle='ne'] {
432
+ top: 0;
433
+ left: 100%;
434
+ cursor: nesw-resize;
435
+ }
436
+
437
+ .selection-handle[data-handle='sw'] {
438
+ top: 100%;
439
+ left: 0;
440
+ cursor: nesw-resize;
441
+ }
442
+
443
+ .selection-handle[data-handle='se'] {
444
+ top: 100%;
445
+ left: 100%;
446
+ cursor: nwse-resize;
447
+ }
448
+
449
+ .selection-chip {
450
+ position: absolute;
451
+ top: calc(100% + 8px * var(--outline-scale, 1));
452
+ left: 50%;
453
+ translate: -50% 0;
454
+ padding: calc(2px * var(--outline-scale, 1))
455
+ calc(5px * var(--outline-scale, 1));
456
+ border-radius: calc(3px * var(--outline-scale, 1));
457
+ background: var(--color-accent);
458
+ color: var(--color-background);
459
+ font-family: var(--type-code-family);
460
+ font-size: calc(11px * var(--outline-scale, 1));
461
+ line-height: 1.3;
462
+ font-variant-numeric: tabular-nums;
463
+ white-space: nowrap;
316
464
  }
317
465
 
318
466
  .frame:focus-visible {
@@ -333,6 +481,56 @@ code {
333
481
  background: var(--color-chrome);
334
482
  }
335
483
 
484
+ .tools {
485
+ position: absolute;
486
+ top: var(--space-md);
487
+ left: var(--space-md);
488
+ display: flex;
489
+ flex-direction: column;
490
+ gap: 2px;
491
+ padding: var(--space-xs);
492
+ border: 1px solid var(--color-border);
493
+ border-radius: var(--radius-panel);
494
+ background: var(--color-chrome);
495
+ }
496
+
497
+ .tool-button {
498
+ display: grid;
499
+ place-items: center;
500
+ width: 28px;
501
+ height: 28px;
502
+ padding: 0;
503
+ border: 0;
504
+ border-radius: var(--radius-action);
505
+ background: transparent;
506
+ color: var(--color-text-secondary);
507
+ cursor: pointer;
508
+ }
509
+
510
+ .tool-button svg {
511
+ width: 16px;
512
+ height: 16px;
513
+ fill: none;
514
+ stroke: currentColor;
515
+ stroke-width: 1.3;
516
+ stroke-linejoin: round;
517
+ stroke-linecap: round;
518
+ }
519
+
520
+ .tool-button[data-tool='move'] svg {
521
+ fill: currentColor;
522
+ }
523
+
524
+ .tool-button:hover {
525
+ color: var(--color-text);
526
+ background: var(--color-surface);
527
+ }
528
+
529
+ .tool-button[aria-pressed='true'] {
530
+ color: var(--color-text);
531
+ background: var(--color-border);
532
+ }
533
+
336
534
  .zoom {
337
535
  min-width: 3.5rem;
338
536
  text-align: center;