@erclx/canon 5.25.0 → 5.27.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/README.md +4 -3
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +14 -1
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +9 -21
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +6 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +23 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +3 -1
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +3 -1
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +11 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +33 -20
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +14 -6
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +21 -8
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +2 -2
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/SKILL.md +3 -1
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +5 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +4 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +3 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -6
- package/docs/agents/capture.md +6 -6
- package/docs/agents/commands.md +5 -5
- package/docs/agents/index.md +5 -5
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +29 -5
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +11 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +23 -2
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -34
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +6 -162
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +398 -0
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +5 -1
- package/src/canvas/client/inspector/fields.css +547 -17
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +166 -1
- package/src/canvas/client/inspector.tsx +236 -73
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +59 -17
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +2 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-audit.ts +96 -14
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +94 -11
- package/src/commands/design.ts +142 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +89 -7
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +103 -11
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +31 -77
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/audit.ts +1 -1
- package/src/context/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +2 -2
- package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
- package/src/gate/measures.ts +53 -13
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +2 -2
- package/src/gov/install.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +3 -3
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/markdown/ceiling.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +2 -1
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +607 -0
- package/src/sandbox/tree.ts +99 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +2 -2
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/standards/read.ts +1 -1
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +2 -2
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +3 -2
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +2 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { useRef, useState } from 'preact/hooks'
|
|
4
|
+
import { 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
|
+
event.preventDefault()
|
|
101
|
+
event.stopPropagation()
|
|
102
|
+
const origin = measure()
|
|
103
|
+
gesture.current = {
|
|
104
|
+
corner,
|
|
105
|
+
startX: event.clientX,
|
|
106
|
+
startY: event.clientY,
|
|
107
|
+
origin,
|
|
108
|
+
box: origin,
|
|
109
|
+
moved: false,
|
|
110
|
+
}
|
|
111
|
+
;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
115
|
+
const current = gesture.current
|
|
116
|
+
if (!current) return
|
|
117
|
+
const dx = event.clientX - current.startX
|
|
118
|
+
const dy = event.clientY - current.startY
|
|
119
|
+
if (!current.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return
|
|
120
|
+
if (!current.moved) onStart?.()
|
|
121
|
+
current.moved = true
|
|
122
|
+
/* The delta is in screen pixels and the box in surface units. */
|
|
123
|
+
const { zoom } = view.value
|
|
124
|
+
current.box = resizedBox(
|
|
125
|
+
current.origin,
|
|
126
|
+
current.corner,
|
|
127
|
+
Math.round(dx / zoom),
|
|
128
|
+
Math.round(dy / zoom),
|
|
129
|
+
)
|
|
130
|
+
onPreview?.(current.box, current.origin)
|
|
131
|
+
setDragged(isMeasuredWhileDragging ? measure() : current.box)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const handlePointerUp = () => {
|
|
135
|
+
const current = gesture.current
|
|
136
|
+
gesture.current = undefined
|
|
137
|
+
if (current?.moved) onCommit(current.box, current.origin)
|
|
138
|
+
setDragged(undefined)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handlePointerCancel = () => {
|
|
142
|
+
const current = gesture.current
|
|
143
|
+
gesture.current = undefined
|
|
144
|
+
if (current?.moved) onCancel?.(current.origin)
|
|
145
|
+
setDragged(undefined)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
class="selection"
|
|
151
|
+
data-selection={kind}
|
|
152
|
+
data-resizing={dragged ? 'true' : undefined}
|
|
153
|
+
style={{
|
|
154
|
+
left: `${box.x - offset.x}px`,
|
|
155
|
+
top: `${box.y - offset.y}px`,
|
|
156
|
+
width: `${box.width}px`,
|
|
157
|
+
height: `${box.height}px`,
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{CORNERS.map((corner) => (
|
|
161
|
+
<span
|
|
162
|
+
key={corner}
|
|
163
|
+
class="selection-handle"
|
|
164
|
+
data-handle={corner}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
onPointerDown={handlePointerDown(corner)}
|
|
167
|
+
onPointerMove={handlePointerMove}
|
|
168
|
+
onPointerUp={handlePointerUp}
|
|
169
|
+
onPointerCancel={handlePointerCancel}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
{hasChip ? (
|
|
173
|
+
<span class="selection-chip" aria-hidden="true">
|
|
174
|
+
{sizeLabel(box)}
|
|
175
|
+
</span>
|
|
176
|
+
) : null}
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The element under the pointer, drawn without handles. */
|
|
182
|
+
export function HoverOutline({ element }: { readonly element: Element }) {
|
|
183
|
+
const rect = element.getBoundingClientRect()
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
class="element-outline"
|
|
187
|
+
data-outline="hover"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
style={{
|
|
190
|
+
left: `${rect.left}px`,
|
|
191
|
+
top: `${rect.top}px`,
|
|
192
|
+
width: `${rect.width}px`,
|
|
193
|
+
height: `${rect.height}px`,
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
@@ -237,14 +237,32 @@ code {
|
|
|
237
237
|
right: 0;
|
|
238
238
|
display: flex;
|
|
239
239
|
align-items: center;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
/*
|
|
241
|
+
* Sized against the zoom, so the label reads at one screen size at any zoom.
|
|
242
|
+
* A row too short for all three parts wraps the theme switch and then the
|
|
243
|
+
* size onto lines the clip hides, so the name keeps the row longest.
|
|
244
|
+
*/
|
|
245
|
+
flex-wrap: wrap;
|
|
246
|
+
align-content: flex-start;
|
|
247
|
+
column-gap: calc(6px * var(--outline-scale, 1));
|
|
248
|
+
row-gap: calc(40px * var(--outline-scale, 1));
|
|
249
|
+
height: calc(25px * var(--outline-scale, 1));
|
|
250
|
+
overflow: clip;
|
|
251
|
+
overflow-clip-margin: calc(4px * var(--outline-scale, 1));
|
|
252
|
+
padding-bottom: calc(6px * var(--outline-scale, 1));
|
|
253
|
+
font-size: calc(11px * var(--outline-scale, 1));
|
|
254
|
+
line-height: 1.3;
|
|
243
255
|
color: var(--color-text-secondary);
|
|
244
256
|
white-space: nowrap;
|
|
245
257
|
}
|
|
246
258
|
|
|
259
|
+
/* A theme switch focused on a hidden line shows rather than staying hidden. */
|
|
260
|
+
.frame-label:focus-within {
|
|
261
|
+
overflow: visible;
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
.frame-name {
|
|
265
|
+
min-width: 0;
|
|
248
266
|
overflow: hidden;
|
|
249
267
|
text-overflow: ellipsis;
|
|
250
268
|
color: var(--color-text);
|
|
@@ -257,8 +275,9 @@ code {
|
|
|
257
275
|
|
|
258
276
|
.frame-theme {
|
|
259
277
|
margin-left: auto;
|
|
260
|
-
padding:
|
|
261
|
-
|
|
278
|
+
padding: calc(1px * var(--outline-scale, 1))
|
|
279
|
+
calc(6px * var(--outline-scale, 1));
|
|
280
|
+
border: calc(1px * var(--outline-scale, 1)) solid var(--color-border);
|
|
262
281
|
border-radius: var(--radius-pill);
|
|
263
282
|
background: var(--color-surface);
|
|
264
283
|
color: var(--color-text-secondary);
|
|
@@ -301,18 +320,91 @@ code {
|
|
|
301
320
|
outline: calc(1px * var(--outline-scale, 1)) dashed var(--color-accent);
|
|
302
321
|
}
|
|
303
322
|
|
|
304
|
-
.element-outline[data-outline='selected'] {
|
|
305
|
-
outline: calc(2px * var(--outline-scale, 1)) solid var(--color-accent);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
323
|
/* Above its neighbors, so a frame dragged over another stays whole. */
|
|
309
324
|
.frame[data-selected='true'] {
|
|
310
325
|
z-index: 1;
|
|
311
326
|
}
|
|
312
327
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
328
|
+
/* A selected frame names itself in the accent, and a picked element does not. */
|
|
329
|
+
.frame[data-selected='true']:not([data-picked]) .frame-name {
|
|
330
|
+
color: var(--color-accent);
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* The selected frame's or element's outline, handles, and size chip. Every
|
|
336
|
+
* length multiplies by the outline scale, so each holds one screen size.
|
|
337
|
+
*/
|
|
338
|
+
.selection {
|
|
339
|
+
--handle: calc(8px * var(--outline-scale, 1));
|
|
340
|
+
--handle-hit: calc(24px * var(--outline-scale, 1));
|
|
341
|
+
position: absolute;
|
|
342
|
+
pointer-events: none;
|
|
343
|
+
outline: calc(1px * var(--outline-scale, 1)) solid var(--color-accent);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Inset by the iframe border, so the box lines up with the element it marks. */
|
|
347
|
+
.selection[data-selection='element'] {
|
|
348
|
+
translate: 1px 1px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* A 24 pixel hit area around an 8 pixel square, per the target size minimum. */
|
|
352
|
+
.selection-handle {
|
|
353
|
+
position: absolute;
|
|
354
|
+
width: var(--handle-hit);
|
|
355
|
+
height: var(--handle-hit);
|
|
356
|
+
margin: calc(var(--handle-hit) / -2);
|
|
357
|
+
pointer-events: auto;
|
|
358
|
+
touch-action: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.selection-handle::after {
|
|
362
|
+
content: '';
|
|
363
|
+
position: absolute;
|
|
364
|
+
inset: calc((var(--handle-hit) - var(--handle)) / 2);
|
|
365
|
+
border: calc(1px * var(--outline-scale, 1)) solid var(--color-accent);
|
|
366
|
+
background: var(--color-background);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.selection-handle[data-handle='nw'] {
|
|
370
|
+
top: 0;
|
|
371
|
+
left: 0;
|
|
372
|
+
cursor: nwse-resize;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.selection-handle[data-handle='ne'] {
|
|
376
|
+
top: 0;
|
|
377
|
+
left: 100%;
|
|
378
|
+
cursor: nesw-resize;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.selection-handle[data-handle='sw'] {
|
|
382
|
+
top: 100%;
|
|
383
|
+
left: 0;
|
|
384
|
+
cursor: nesw-resize;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.selection-handle[data-handle='se'] {
|
|
388
|
+
top: 100%;
|
|
389
|
+
left: 100%;
|
|
390
|
+
cursor: nwse-resize;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.selection-chip {
|
|
394
|
+
position: absolute;
|
|
395
|
+
top: calc(100% + 8px * var(--outline-scale, 1));
|
|
396
|
+
left: 50%;
|
|
397
|
+
translate: -50% 0;
|
|
398
|
+
padding: calc(2px * var(--outline-scale, 1))
|
|
399
|
+
calc(5px * var(--outline-scale, 1));
|
|
400
|
+
border-radius: calc(3px * var(--outline-scale, 1));
|
|
401
|
+
background: var(--color-accent);
|
|
402
|
+
color: var(--color-background);
|
|
403
|
+
font-family: var(--type-code-family);
|
|
404
|
+
font-size: calc(11px * var(--outline-scale, 1));
|
|
405
|
+
line-height: 1.3;
|
|
406
|
+
font-variant-numeric: tabular-nums;
|
|
407
|
+
white-space: nowrap;
|
|
316
408
|
}
|
|
317
409
|
|
|
318
410
|
.frame:focus-visible {
|
|
@@ -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
|
/**
|
|
@@ -320,7 +320,8 @@ const EDIT_NOTICES: Readonly<Record<string, string | undefined>> = {
|
|
|
320
320
|
/**
|
|
321
321
|
* Posts one property change to the element the address names. A refusal
|
|
322
322
|
* says why and reloads the frame, so a pending value never sits over a file
|
|
323
|
-
* that moved under it.
|
|
323
|
+
* that moved under it. Resolves to what the write answered, or undefined when
|
|
324
|
+
* nothing was written.
|
|
324
325
|
*/
|
|
325
326
|
export async function editElement(
|
|
326
327
|
ref: FrameRef,
|
|
@@ -329,7 +330,7 @@ export async function editElement(
|
|
|
329
330
|
property: string,
|
|
330
331
|
value: string,
|
|
331
332
|
fetchImpl: typeof fetch = fetch,
|
|
332
|
-
): Promise<
|
|
333
|
+
): Promise<{ readonly hash?: string } | undefined> {
|
|
333
334
|
const hash = editedHashes.value.get(key) ?? address.hash
|
|
334
335
|
pendingEdit.value = { key, index: address.index, property }
|
|
335
336
|
try {
|
|
@@ -358,7 +359,7 @@ export async function editElement(
|
|
|
358
359
|
: `Could not save (status ${response.status}). Check canon canvas serve is still running.`)
|
|
359
360
|
reloadFrame(key)
|
|
360
361
|
await loadPages(fetchImpl)
|
|
361
|
-
return
|
|
362
|
+
return undefined
|
|
362
363
|
}
|
|
363
364
|
editRefusal.value = undefined
|
|
364
365
|
const saved = pendingEdit.value
|
|
@@ -366,18 +367,51 @@ export async function editElement(
|
|
|
366
367
|
setTimeout(() => {
|
|
367
368
|
if (savedEdit.value === saved) savedEdit.value = undefined
|
|
368
369
|
}, SAVED_MS)
|
|
369
|
-
if (typeof record.hash
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
370
|
+
if (typeof record.hash !== 'string') return {}
|
|
371
|
+
const hashes = new Map(editedHashes.value)
|
|
372
|
+
hashes.set(key, record.hash)
|
|
373
|
+
editedHashes.value = hashes
|
|
374
|
+
return { hash: record.hash }
|
|
374
375
|
} catch (error) {
|
|
375
376
|
editRefusal.value = `Could not save (${error instanceof Error ? error.message : 'unknown'}). Check canon canvas serve is still running.`
|
|
377
|
+
return undefined
|
|
376
378
|
} finally {
|
|
377
379
|
pendingEdit.value = undefined
|
|
378
380
|
}
|
|
379
381
|
}
|
|
380
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Writes an element's new size as one edit per changed axis, width first. The
|
|
385
|
+
* second carries the hash the first answered, so a frame reloading between the
|
|
386
|
+
* two cannot leave it holding the hash of a file already rewritten. A refused
|
|
387
|
+
* first edit sends no second.
|
|
388
|
+
*/
|
|
389
|
+
export async function resizeElement(
|
|
390
|
+
ref: FrameRef,
|
|
391
|
+
key: string,
|
|
392
|
+
address: ElementAddress,
|
|
393
|
+
width: number | undefined,
|
|
394
|
+
height: number | undefined,
|
|
395
|
+
fetchImpl: typeof fetch = fetch,
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
let current = address
|
|
398
|
+
if (width !== undefined) {
|
|
399
|
+
const written = await editElement(
|
|
400
|
+
ref,
|
|
401
|
+
key,
|
|
402
|
+
current,
|
|
403
|
+
'width',
|
|
404
|
+
`${width}px`,
|
|
405
|
+
fetchImpl,
|
|
406
|
+
)
|
|
407
|
+
if (!written) return
|
|
408
|
+
if (written.hash) current = { ...current, hash: written.hash }
|
|
409
|
+
}
|
|
410
|
+
if (height !== undefined) {
|
|
411
|
+
await editElement(ref, key, current, 'height', `${height}px`, fetchImpl)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
381
415
|
/**
|
|
382
416
|
* Moves a frame on the surface without writing, which a drag calls on every
|
|
383
417
|
* pointer move. The next reread replaces it with what the layout holds.
|
|
@@ -406,6 +440,32 @@ export async function moveFrameTo(
|
|
|
406
440
|
await write('/api/frames/move', { ...ref, x, y }, fetchImpl)
|
|
407
441
|
}
|
|
408
442
|
|
|
443
|
+
/** Resizes a frame on the surface without writing, as a handle drag does. */
|
|
444
|
+
export function previewResize(ref: FrameRef, box: Box): void {
|
|
445
|
+
pages.value = pages.value.map((candidate) =>
|
|
446
|
+
candidate.name === ref.page
|
|
447
|
+
? {
|
|
448
|
+
...candidate,
|
|
449
|
+
frames: candidate.frames.map((frame) =>
|
|
450
|
+
frame.name === ref.frame
|
|
451
|
+
? { ...frame, ...box, placed: true }
|
|
452
|
+
: frame,
|
|
453
|
+
),
|
|
454
|
+
}
|
|
455
|
+
: candidate,
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Shows the new box and writes it through the server's shared layout writer. */
|
|
460
|
+
export async function resizeFrameTo(
|
|
461
|
+
ref: FrameRef,
|
|
462
|
+
box: Box,
|
|
463
|
+
fetchImpl: typeof fetch = fetch,
|
|
464
|
+
): Promise<void> {
|
|
465
|
+
previewResize(ref, box)
|
|
466
|
+
await write('/api/frames/resize', { ...ref, ...box }, fetchImpl)
|
|
467
|
+
}
|
|
468
|
+
|
|
409
469
|
export function clampZoom(zoom: number): number {
|
|
410
470
|
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom))
|
|
411
471
|
}
|
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
moveFrameTo,
|
|
17
17
|
panBy,
|
|
18
18
|
previewMove,
|
|
19
|
+
previewResize,
|
|
19
20
|
registerFrameDocument,
|
|
21
|
+
resizeElement,
|
|
22
|
+
resizeFrameTo,
|
|
20
23
|
selectElement,
|
|
21
24
|
selectFrame,
|
|
22
25
|
selection,
|
|
@@ -25,13 +28,16 @@ import {
|
|
|
25
28
|
view,
|
|
26
29
|
zoomAt,
|
|
27
30
|
} from '@/canvas/client/state'
|
|
31
|
+
import {
|
|
32
|
+
DRAG_THRESHOLD,
|
|
33
|
+
HoverOutline,
|
|
34
|
+
type OverlayBox,
|
|
35
|
+
SelectionOverlay,
|
|
36
|
+
} from '@/canvas/client/selection'
|
|
28
37
|
|
|
29
38
|
/** One wheel notch or one button press. */
|
|
30
39
|
const ZOOM_STEP = 1.2
|
|
31
40
|
|
|
32
|
-
/** Screen pixels a press travels before it reads as a drag and not a click. */
|
|
33
|
-
const DRAG_THRESHOLD = 3
|
|
34
|
-
|
|
35
41
|
/** Surface units one arrow press moves a frame, and one with Shift held. */
|
|
36
42
|
const NUDGE = 10
|
|
37
43
|
const NUDGE_LARGE = 50
|
|
@@ -76,25 +82,111 @@ function asElement(target: EventTarget | null): Element | undefined {
|
|
|
76
82
|
return node?.nodeType === 1 ? (node as Element) : undefined
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
/** A computed length in pixels, or the fallback for one that is not. */
|
|
86
|
+
function pixels(value: string, fallback: number): number {
|
|
87
|
+
const parsed = Number.parseFloat(value)
|
|
88
|
+
return value.endsWith('px') && Number.isFinite(parsed) ? parsed : fallback
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The selected element's overlay. A handle drag previews the size as inline
|
|
93
|
+
* style on the element and writes it once on release, the way the inspector's
|
|
94
|
+
* scrub does. The drag measures the border box while `width` and `height` set
|
|
95
|
+
* whatever box the element sizes by, so the delta is added to the computed
|
|
96
|
+
* value rather than written as the box itself.
|
|
97
|
+
*/
|
|
98
|
+
function ElementSelection({
|
|
99
|
+
node,
|
|
100
|
+
doc,
|
|
101
|
+
frameRef,
|
|
102
|
+
docKey,
|
|
82
103
|
}: {
|
|
83
|
-
readonly
|
|
84
|
-
readonly
|
|
104
|
+
readonly node: Element
|
|
105
|
+
readonly doc: Document
|
|
106
|
+
readonly frameRef: FrameRef
|
|
107
|
+
readonly docKey: string
|
|
85
108
|
}): JSX.Element {
|
|
86
|
-
const
|
|
109
|
+
const start = useRef<
|
|
110
|
+
| {
|
|
111
|
+
readonly width: number
|
|
112
|
+
readonly height: number
|
|
113
|
+
readonly inlineWidth: string
|
|
114
|
+
readonly inlineHeight: string
|
|
115
|
+
}
|
|
116
|
+
| undefined
|
|
117
|
+
>(undefined)
|
|
118
|
+
const style = (node as HTMLElement).style
|
|
119
|
+
|
|
120
|
+
const measure = () => {
|
|
121
|
+
const rect = node.getBoundingClientRect()
|
|
122
|
+
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cssSize = (box: OverlayBox, origin: OverlayBox) => {
|
|
126
|
+
const from = start.current
|
|
127
|
+
return {
|
|
128
|
+
width: Math.round(
|
|
129
|
+
(from?.width ?? origin.width) + box.width - origin.width,
|
|
130
|
+
),
|
|
131
|
+
height: Math.round(
|
|
132
|
+
(from?.height ?? origin.height) + box.height - origin.height,
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleStart = () => {
|
|
138
|
+
const computed = doc.defaultView?.getComputedStyle(node)
|
|
139
|
+
const rect = node.getBoundingClientRect()
|
|
140
|
+
start.current = {
|
|
141
|
+
width: pixels(computed?.width ?? '', rect.width),
|
|
142
|
+
height: pixels(computed?.height ?? '', rect.height),
|
|
143
|
+
inlineWidth: style.getPropertyValue('width'),
|
|
144
|
+
inlineHeight: style.getPropertyValue('height'),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const handlePreview = (box: OverlayBox, from: OverlayBox) => {
|
|
149
|
+
const size = cssSize(box, from)
|
|
150
|
+
style.setProperty('width', `${size.width}px`)
|
|
151
|
+
style.setProperty('height', `${size.height}px`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const restore = () => {
|
|
155
|
+
const from = start.current
|
|
156
|
+
if (!from) return
|
|
157
|
+
style.setProperty('width', from.inlineWidth)
|
|
158
|
+
style.setProperty('height', from.inlineHeight)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleCommit = (box: OverlayBox, from: OverlayBox) => {
|
|
162
|
+
const size = cssSize(box, from)
|
|
163
|
+
const address = addressOf(doc, node)
|
|
164
|
+
const isUnchanged = box.width === from.width && box.height === from.height
|
|
165
|
+
if (!address || isUnchanged) {
|
|
166
|
+
restore()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
void resizeElement(
|
|
170
|
+
frameRef,
|
|
171
|
+
docKey,
|
|
172
|
+
address,
|
|
173
|
+
box.width === from.width ? undefined : size.width,
|
|
174
|
+
box.height === from.height ? undefined : size.height,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleCancel = restore
|
|
179
|
+
|
|
87
180
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}}
|
|
181
|
+
<SelectionOverlay
|
|
182
|
+
kind="element"
|
|
183
|
+
measure={measure}
|
|
184
|
+
hasChip
|
|
185
|
+
isMeasuredWhileDragging
|
|
186
|
+
onStart={handleStart}
|
|
187
|
+
onPreview={handlePreview}
|
|
188
|
+
onCommit={handleCommit}
|
|
189
|
+
onCancel={handleCancel}
|
|
98
190
|
/>
|
|
99
191
|
)
|
|
100
192
|
}
|
|
@@ -263,15 +355,33 @@ function FrameView({
|
|
|
263
355
|
const delta = ARROWS[event.key]
|
|
264
356
|
if (!delta || !isSelected) return
|
|
265
357
|
event.preventDefault()
|
|
358
|
+
/* The keyboard path for a handle drag: right and down grow the frame. */
|
|
359
|
+
if (event.ctrlKey || event.metaKey) {
|
|
360
|
+
void resizeFrameTo(ref, {
|
|
361
|
+
x: frame.x,
|
|
362
|
+
y: frame.y,
|
|
363
|
+
width: Math.max(1, frame.width + delta.x * step),
|
|
364
|
+
height: Math.max(1, frame.height + delta.y * step),
|
|
365
|
+
})
|
|
366
|
+
return
|
|
367
|
+
}
|
|
266
368
|
void moveFrameTo(ref, frame.x + delta.x * step, frame.y + delta.y * step)
|
|
267
369
|
}
|
|
268
370
|
|
|
371
|
+
const frameBox = (): OverlayBox => ({
|
|
372
|
+
x: frame.x,
|
|
373
|
+
y: frame.y,
|
|
374
|
+
width: frame.width,
|
|
375
|
+
height: frame.height,
|
|
376
|
+
})
|
|
377
|
+
|
|
269
378
|
const next = value === 'dark' ? 'light' : 'dark'
|
|
270
379
|
return (
|
|
271
380
|
<figure
|
|
272
381
|
class="frame"
|
|
273
382
|
data-frame={frame.name}
|
|
274
383
|
data-selected={isSelected ? 'true' : undefined}
|
|
384
|
+
data-picked={picked ? 'true' : undefined}
|
|
275
385
|
data-dragging={draggingFrame.value === frame.name ? 'true' : undefined}
|
|
276
386
|
tabIndex={0}
|
|
277
387
|
aria-label={`${frame.name}, ${frame.width} by ${frame.height}`}
|
|
@@ -318,10 +428,25 @@ function FrameView({
|
|
|
318
428
|
onLoad={handleLoad}
|
|
319
429
|
/>
|
|
320
430
|
{hoveredNode && hoveredNode !== selectedNode ? (
|
|
321
|
-
<
|
|
431
|
+
<HoverOutline element={hoveredNode} />
|
|
432
|
+
) : null}
|
|
433
|
+
{isSelected && !picked ? (
|
|
434
|
+
<SelectionOverlay
|
|
435
|
+
kind="frame"
|
|
436
|
+
measure={frameBox}
|
|
437
|
+
offset={{ x: frame.x, y: frame.y }}
|
|
438
|
+
onPreview={(box) => previewResize(ref, box)}
|
|
439
|
+
onCommit={(box) => void resizeFrameTo(ref, box)}
|
|
440
|
+
onCancel={(origin) => previewResize(ref, origin)}
|
|
441
|
+
/>
|
|
322
442
|
) : null}
|
|
323
|
-
{selectedNode ? (
|
|
324
|
-
<
|
|
443
|
+
{doc && selectedNode ? (
|
|
444
|
+
<ElementSelection
|
|
445
|
+
node={selectedNode}
|
|
446
|
+
doc={doc}
|
|
447
|
+
frameRef={ref}
|
|
448
|
+
docKey={key}
|
|
449
|
+
/>
|
|
325
450
|
) : null}
|
|
326
451
|
</div>
|
|
327
452
|
</figure>
|