nudge-ui 0.2.0 → 0.2.2
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/bin/nudge-ui.mjs +3 -1
- package/dist/cli/agentCommand.d.ts +8 -0
- package/dist/cli/agentCommand.js +59 -0
- package/dist/client.mjs +878 -11
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
- package/dist/hosts/astro/clientTransport.js +63 -11
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +4 -3
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/sidecar.d.ts +4 -0
- package/dist/hosts/next/sidecar.js +56 -7
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +32 -3
- package/dist/hosts/projectBridge.d.ts +21 -0
- package/dist/hosts/projectBridge.js +98 -0
- package/dist/hosts/static/cli.d.ts +1 -1
- package/dist/hosts/static/cli.js +6 -1
- package/dist/hosts/static/manifest.d.ts +1 -1
- package/dist/hosts/static/manifest.js +3 -1
- package/dist/hosts/static/server.js +45 -3
- package/dist/hosts/vite/vite.d.ts +6 -1
- package/dist/hosts/vite/vite.js +82 -15
- package/dist/inspector/agent/McpConnectionDialog.d.ts +5 -4
- package/dist/inspector/agent/McpConnectionDialog.js +20 -37
- package/dist/inspector/agent/client.d.ts +16 -3
- package/dist/inspector/agent/client.js +114 -11
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +11 -4
- package/dist/inspector/agent/httpTransport.d.ts +8 -0
- package/dist/inspector/agent/httpTransport.js +78 -18
- package/dist/inspector/agent/protocol.d.ts +11 -3
- package/dist/inspector/agent/protocol.js +1 -1
- package/dist/inspector/agent/verification.d.ts +1 -1
- package/dist/inspector/agent/verification.js +69 -12
- package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
- package/dist/inspector/canvas/CanvasCard.js +181 -91
- package/dist/inspector/canvas/CanvasElementOverlay.js +288 -52
- package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
- package/dist/inspector/canvas/CanvasToolbar.js +24 -0
- package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
- package/dist/inspector/canvas/CanvasWorkspace.js +410 -72
- package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
- package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
- package/dist/inspector/canvas/canvasResize.d.ts +30 -0
- package/dist/inspector/canvas/canvasResize.js +52 -0
- package/dist/inspector/canvas/canvasStore.d.ts +30 -6
- package/dist/inspector/canvas/canvasStore.js +128 -61
- package/dist/inspector/canvas/frameProtocol.d.ts +82 -5
- package/dist/inspector/canvas/frameProtocol.js +63 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -0
- package/dist/inspector/canvas/keyboardShortcuts.js +16 -0
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/projection.d.ts +7 -0
- package/dist/inspector/canvas/projection.js +118 -25
- package/dist/inspector/canvas/rendererBootstrap.js +42 -29
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +385 -58
- package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
- package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
- package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
- package/dist/inspector/canvas/sessionStore.d.ts +1 -0
- package/dist/inspector/canvas/sessionStore.js +12 -4
- package/dist/inspector/canvas/staleChangeDetector.js +16 -14
- package/dist/inspector/changes/changesLog.js +31 -1
- package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
- package/dist/inspector/changes/managedStyleProjection.js +3 -3
- package/dist/inspector/changes/structuralTypes.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.js +29 -3
- package/dist/inspector/clientManifest.d.ts +7 -0
- package/dist/inspector/clientManifest.js +12 -0
- package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
- package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
- package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
- package/dist/inspector/index.d.ts +2 -0
- package/dist/inspector/index.js +80 -32
- package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
- package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
- package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
- package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
- package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
- package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
- package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
- package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
- package/dist/inspector/inline-text/textEditChange.js +165 -0
- package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
- package/dist/inspector/inspection/selectionProperty.js +1 -0
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +40 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/selectedGeometry.js +21 -2
- package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
- package/dist/inspector/overlay/spacingGestures.js +351 -0
- package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
- package/dist/inspector/overlay/structuralGestures.js +12 -10
- package/dist/inspector/projection/managedStylesheet.js +4 -1
- package/dist/inspector/projection/renderedInstance.d.ts +2 -0
- package/dist/inspector/projection/renderedInstance.js +5 -1
- package/dist/inspector/projection/structuralProjection.d.ts +2 -0
- package/dist/inspector/projection/structuralProjection.js +85 -1
- package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
- package/dist/inspector/projection/textProjection.d.ts +2 -0
- package/dist/inspector/projection/textProjection.js +10 -4
- package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
- package/dist/inspector/prompt/clipboardHandoff.js +3 -3
- package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
- package/dist/inspector/prompt/generatePrompt.js +19 -2
- package/dist/inspector/runtime/editorShell.d.ts +2 -0
- package/dist/inspector/runtime/editorShell.js +4 -0
- package/dist/inspector/runtime/runtimeConfig.d.ts +5 -1
- package/dist/inspector/runtime/runtimeConfig.js +4 -0
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/selection/selectionStore.js +2 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +12 -2
- package/dist/inspector/shell/AppShell.js +5 -1
- package/dist/inspector/shell/ChangesLog.js +26 -22
- package/dist/inspector/shell/CopyPromptButton.js +102 -19
- package/dist/inspector/shell/EmptyState.js +1 -1
- package/dist/inspector/shell/InspectorShell.d.ts +0 -1
- package/dist/inspector/shell/InspectorShell.js +38 -42
- package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
- package/dist/inspector/shell/PeekOriginalButton.js +57 -0
- package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
- package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
- package/dist/inspector/shell/originalPreview.d.ts +9 -0
- package/dist/inspector/shell/originalPreview.js +61 -0
- package/dist/inspector/shell/shortcuts.d.ts +3 -0
- package/dist/inspector/shell/shortcuts.js +15 -0
- package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
- package/dist/inspector/sketch/SketchChanges.js +72 -0
- package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
- package/dist/inspector/sketch/SketchEditor.js +402 -0
- package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
- package/dist/inspector/sketch/SketchEntryButton.js +11 -0
- package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
- package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
- package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
- package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
- package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
- package/dist/inspector/sketch/SketchOverlay.js +420 -0
- package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
- package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
- package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
- package/dist/inspector/sketch/SketchWorkspace.js +152 -0
- package/dist/inspector/sketch/capture.d.ts +55 -0
- package/dist/inspector/sketch/capture.js +350 -0
- package/dist/inspector/sketch/freehand.d.ts +13 -0
- package/dist/inspector/sketch/freehand.js +64 -0
- package/dist/inspector/sketch/geometry.d.ts +17 -0
- package/dist/inspector/sketch/geometry.js +98 -0
- package/dist/inspector/sketch/handoff.d.ts +21 -0
- package/dist/inspector/sketch/handoff.js +72 -0
- package/dist/inspector/sketch/interaction.d.ts +21 -0
- package/dist/inspector/sketch/interaction.js +98 -0
- package/dist/inspector/sketch/model.d.ts +104 -0
- package/dist/inspector/sketch/model.js +282 -0
- package/dist/inspector/sketch/persistence.d.ts +21 -0
- package/dist/inspector/sketch/persistence.js +205 -0
- package/dist/inspector/sketch/raster.d.ts +22 -0
- package/dist/inspector/sketch/raster.js +203 -0
- package/dist/inspector/sketch/sketchNote.d.ts +10 -0
- package/dist/inspector/sketch/sketchNote.js +30 -0
- package/dist/inspector/sketch/store.d.ts +47 -0
- package/dist/inspector/sketch/store.js +434 -0
- package/dist/inspector/styleEditors/BorderEditor.js +15 -19
- package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
- package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
- package/dist/inspector/styleEditors/ColorPicker.js +11 -36
- package/dist/inspector/styleEditors/GapField.d.ts +3 -2
- package/dist/inspector/styleEditors/GapField.js +2 -2
- package/dist/inspector/styleEditors/GridSection.js +1 -1
- package/dist/inspector/styleEditors/LayoutSection.js +11 -8
- package/dist/inspector/styleEditors/SpacingBox.js +31 -33
- package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
- package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
- package/dist/inspector/styleEditors/layoutValue.js +3 -0
- package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
- package/dist/inspector/styleEditors/nudgeValue.js +93 -5
- package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
- package/dist/inspector/styleEditors/stylePresence.js +22 -0
- package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
- package/dist/inspector/styleEditors/textRelevance.js +35 -0
- package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
- package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
- package/dist/inspector/tokens/TokenField.js +130 -7
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/tokens/editActions.d.ts +2 -0
- package/dist/inspector/tokens/editActions.js +2 -2
- package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
- package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
- package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
- package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
- package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
- package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
- package/dist/inspector/tokens/resolution.d.ts +3 -26
- package/dist/inspector/tokens/resolution.js +44 -735
- package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
- package/dist/inspector/tokens/tokenAvailability.js +204 -0
- package/dist/inspector/ui/Button.d.ts +1 -1
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/SideValuesField.js +2 -2
- package/dist/inspector/ui/StatusCallout.d.ts +4 -1
- package/dist/inspector/ui/StatusCallout.js +2 -2
- package/dist/inspector/ui/dragCursor.d.ts +8 -0
- package/dist/inspector/ui/dragCursor.js +21 -0
- package/dist/inspector/ui/styles.js +21 -17
- package/dist/internal/agent-protocol.d.ts +65 -1
- package/dist/internal/agent-protocol.js +183 -3
- package/dist/nudge-ui.mjs +276 -58
- package/dist/transport/editor.d.ts +47 -0
- package/dist/transport/editor.js +163 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.js +1 -0
- package/dist/transport/routes.d.ts +10 -0
- package/dist/transport/routes.js +10 -0
- package/package.json +3 -1
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- package/dist/inspector/shell/DomNavigation.js +0 -30
- package/dist/inspector/shell/panelLayout.d.ts +0 -2
- package/dist/inspector/shell/panelLayout.js +0 -29
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
4
|
+
import { IconCheck, IconX, } from "@tabler/icons-react";
|
|
5
|
+
import { Button } from "../ui/Button.js";
|
|
6
|
+
import { IconButton } from "../ui/IconButton.js";
|
|
7
|
+
import { getSketchViewport } from "./capture.js";
|
|
8
|
+
import { SKETCH_ANNOTATION_RADIUS, SKETCH_LIMITS, SKETCH_STROKE_COLOR, SKETCH_STROKE_OUTLINE, SKETCH_STROKE_WIDTH, } from "./model.js";
|
|
9
|
+
import { findSketchElementAt, translateSketchAnnotation, translateSketchStroke, } from "./geometry.js";
|
|
10
|
+
import { SketchPromptPanel } from "./SketchPromptPanel.js";
|
|
11
|
+
import { SketchSvgLayer } from "./freehand.js";
|
|
12
|
+
import { useInspectorOpen } from "../shell/openStore.js";
|
|
13
|
+
function portalContainer() {
|
|
14
|
+
return typeof document !== "undefined"
|
|
15
|
+
? document.getElementById("nudge-ui-root")?.shadowRoot ?? document.body
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
function clamp(value, maximum) {
|
|
19
|
+
return Math.max(0, Math.min(maximum, value));
|
|
20
|
+
}
|
|
21
|
+
function pointFromEvent(event, canvas, viewport) {
|
|
22
|
+
const bounds = canvas.getBoundingClientRect();
|
|
23
|
+
return {
|
|
24
|
+
x: clamp(((event.clientX - bounds.left) / Math.max(1, bounds.width)) * viewport.width, viewport.width) + viewport.scrollX,
|
|
25
|
+
y: clamp(((event.clientY - bounds.top) / Math.max(1, bounds.height)) * viewport.height, viewport.height) + viewport.scrollY,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function distance(left, right) {
|
|
29
|
+
return Math.hypot(left.x - right.x, left.y - right.y);
|
|
30
|
+
}
|
|
31
|
+
function visibleStroke(stroke, viewport) {
|
|
32
|
+
return {
|
|
33
|
+
...stroke,
|
|
34
|
+
points: stroke.points.map((point) => ({
|
|
35
|
+
x: point.x - viewport.scrollX,
|
|
36
|
+
y: point.y - viewport.scrollY,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function visibleAnnotation(annotation, viewport) {
|
|
41
|
+
return {
|
|
42
|
+
...annotation,
|
|
43
|
+
point: {
|
|
44
|
+
x: annotation.point.x - viewport.scrollX,
|
|
45
|
+
y: annotation.point.y - viewport.scrollY,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function rectanglePoints(start, end) {
|
|
50
|
+
const left = Math.min(start.x, end.x);
|
|
51
|
+
const right = Math.max(start.x, end.x);
|
|
52
|
+
const top = Math.min(start.y, end.y);
|
|
53
|
+
const bottom = Math.max(start.y, end.y);
|
|
54
|
+
return [
|
|
55
|
+
{ x: left, y: top },
|
|
56
|
+
{ x: right, y: top },
|
|
57
|
+
{ x: right, y: bottom },
|
|
58
|
+
{ x: left, y: bottom },
|
|
59
|
+
{ x: left, y: top },
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function annotationEditorStyle(annotation, viewport) {
|
|
63
|
+
const width = 280;
|
|
64
|
+
const height = 52;
|
|
65
|
+
return {
|
|
66
|
+
left: clamp(annotation.point.x - viewport.scrollX + SKETCH_ANNOTATION_RADIUS + 8, Math.max(8, viewport.width - width - 8)),
|
|
67
|
+
top: clamp(annotation.point.y - viewport.scrollY + SKETCH_ANNOTATION_RADIUS + 8, Math.max(8, viewport.height - height - 8)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function SketchOverlay({ open, hostElement, initialTool = "pen", onCancel, onDone }) {
|
|
71
|
+
const inspectorOpen = useInspectorOpen();
|
|
72
|
+
const [viewport, setViewport] = useState(() => getSketchViewport(hostElement));
|
|
73
|
+
const [tool, setTool] = useState("pen");
|
|
74
|
+
const [description, setDescription] = useState("");
|
|
75
|
+
const [strokes, setStrokes] = useState([]);
|
|
76
|
+
const [redoStrokes, setRedoStrokes] = useState([]);
|
|
77
|
+
const [annotations, setAnnotations] = useState([]);
|
|
78
|
+
const [activeAnnotationId, setActiveAnnotationId] = useState(null);
|
|
79
|
+
const [activeAnnotationOriginal, setActiveAnnotationOriginal] = useState(null);
|
|
80
|
+
const [annotationDraft, setAnnotationDraft] = useState("");
|
|
81
|
+
const [draftPoints, setDraftPoints] = useState([]);
|
|
82
|
+
const [submitting, setSubmitting] = useState(false);
|
|
83
|
+
const [error, setError] = useState(null);
|
|
84
|
+
const svgRef = useRef(null);
|
|
85
|
+
const annotationInputRef = useRef(null);
|
|
86
|
+
const pointerId = useRef(null);
|
|
87
|
+
const moveState = useRef(null);
|
|
88
|
+
const draftPointLimitReached = useRef(false);
|
|
89
|
+
const activeAnnotation = annotations.find((annotation) => annotation.id === activeAnnotationId) ?? null;
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!open)
|
|
92
|
+
return;
|
|
93
|
+
setViewport(getSketchViewport(hostElement));
|
|
94
|
+
// Sketch starts with a freehand pen. The separate Annotate entry point
|
|
95
|
+
// still opens directly in annotation mode without the old live toolbar.
|
|
96
|
+
setTool(initialTool);
|
|
97
|
+
setDescription("");
|
|
98
|
+
setStrokes([]);
|
|
99
|
+
setRedoStrokes([]);
|
|
100
|
+
setAnnotations([]);
|
|
101
|
+
setActiveAnnotationId(null);
|
|
102
|
+
setActiveAnnotationOriginal(null);
|
|
103
|
+
setAnnotationDraft("");
|
|
104
|
+
setDraftPoints([]);
|
|
105
|
+
moveState.current = null;
|
|
106
|
+
setSubmitting(false);
|
|
107
|
+
setError(null);
|
|
108
|
+
}, [hostElement, initialTool, open]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!open)
|
|
111
|
+
return;
|
|
112
|
+
const updateViewport = () => setViewport(getSketchViewport(hostElement));
|
|
113
|
+
window.addEventListener("resize", updateViewport);
|
|
114
|
+
window.addEventListener("scroll", updateViewport, true);
|
|
115
|
+
const frameWindow = typeof HTMLIFrameElement !== "undefined"
|
|
116
|
+
&& hostElement instanceof HTMLIFrameElement
|
|
117
|
+
? hostElement.contentWindow
|
|
118
|
+
: null;
|
|
119
|
+
frameWindow?.addEventListener("resize", updateViewport);
|
|
120
|
+
frameWindow?.addEventListener("scroll", updateViewport, true);
|
|
121
|
+
return () => {
|
|
122
|
+
window.removeEventListener("resize", updateViewport);
|
|
123
|
+
window.removeEventListener("scroll", updateViewport, true);
|
|
124
|
+
frameWindow?.removeEventListener("resize", updateViewport);
|
|
125
|
+
frameWindow?.removeEventListener("scroll", updateViewport, true);
|
|
126
|
+
};
|
|
127
|
+
}, [hostElement, open]);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (activeAnnotationId !== null)
|
|
130
|
+
annotationInputRef.current?.focus();
|
|
131
|
+
}, [activeAnnotationId]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (open)
|
|
134
|
+
svgRef.current?.focus();
|
|
135
|
+
}, [open]);
|
|
136
|
+
function openAnnotation(annotation) {
|
|
137
|
+
setActiveAnnotationId(annotation.id);
|
|
138
|
+
setActiveAnnotationOriginal(annotation.description);
|
|
139
|
+
setAnnotationDraft(annotation.description);
|
|
140
|
+
setError(null);
|
|
141
|
+
}
|
|
142
|
+
function annotationAt(point) {
|
|
143
|
+
return [...annotations].reverse().find((annotation) => distance(annotation.point, point) <= SKETCH_ANNOTATION_RADIUS * 1.5) ?? null;
|
|
144
|
+
}
|
|
145
|
+
function commitActiveAnnotation() {
|
|
146
|
+
if (activeAnnotationId === null)
|
|
147
|
+
return annotations;
|
|
148
|
+
const description = annotationDraft.trim();
|
|
149
|
+
if (!description) {
|
|
150
|
+
setError("Add a short description for this annotation before continuing.");
|
|
151
|
+
annotationInputRef.current?.focus();
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const next = annotations.map((annotation) => annotation.id === activeAnnotationId
|
|
155
|
+
? { ...annotation, description }
|
|
156
|
+
: annotation);
|
|
157
|
+
setAnnotations(next);
|
|
158
|
+
setActiveAnnotationId(null);
|
|
159
|
+
setActiveAnnotationOriginal(null);
|
|
160
|
+
setAnnotationDraft("");
|
|
161
|
+
setError(null);
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
function cancelActiveAnnotation() {
|
|
165
|
+
if (activeAnnotationId !== null && activeAnnotationOriginal === "") {
|
|
166
|
+
setAnnotations((current) => current.filter((annotation) => annotation.id !== activeAnnotationId));
|
|
167
|
+
}
|
|
168
|
+
setActiveAnnotationId(null);
|
|
169
|
+
setActiveAnnotationOriginal(null);
|
|
170
|
+
setAnnotationDraft("");
|
|
171
|
+
setError(null);
|
|
172
|
+
}
|
|
173
|
+
function onPointerDown(event) {
|
|
174
|
+
if (submitting || pointerId.current !== null || moveState.current !== null)
|
|
175
|
+
return;
|
|
176
|
+
const point = pointFromEvent(event, event.currentTarget, viewport);
|
|
177
|
+
if (tool === "move") {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
const element = findSketchElementAt(point, strokes, annotations);
|
|
180
|
+
if (!element)
|
|
181
|
+
return;
|
|
182
|
+
moveState.current = { pointerId: event.pointerId, element, lastPoint: point };
|
|
183
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (tool === "annotate") {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
if (activeAnnotationId !== null) {
|
|
189
|
+
if (commitActiveAnnotation())
|
|
190
|
+
setError("Select Annotate again, then click another area.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const existing = annotationAt(point);
|
|
194
|
+
if (existing) {
|
|
195
|
+
openAnnotation(existing);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (annotations.length >= SKETCH_LIMITS.annotations) {
|
|
199
|
+
setError(`A sketch can contain at most ${SKETCH_LIMITS.annotations} annotations.`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const usedNumbers = new Set(annotations.map((annotation) => annotation.number));
|
|
203
|
+
let nextNumber = 1;
|
|
204
|
+
while (usedNumbers.has(nextNumber))
|
|
205
|
+
nextNumber += 1;
|
|
206
|
+
const annotation = {
|
|
207
|
+
id: `annotation-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
208
|
+
number: nextNumber,
|
|
209
|
+
point,
|
|
210
|
+
description: "",
|
|
211
|
+
};
|
|
212
|
+
setAnnotations((current) => [...current, annotation]);
|
|
213
|
+
setActiveAnnotationId(annotation.id);
|
|
214
|
+
setActiveAnnotationOriginal("");
|
|
215
|
+
setAnnotationDraft("");
|
|
216
|
+
setError(null);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
pointerId.current = event.pointerId;
|
|
221
|
+
draftPointLimitReached.current = false;
|
|
222
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
223
|
+
setDraftPoints([point]);
|
|
224
|
+
}
|
|
225
|
+
function onPointerMove(event) {
|
|
226
|
+
const activeMove = moveState.current;
|
|
227
|
+
if (activeMove?.pointerId === event.pointerId) {
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
const point = pointFromEvent(event, event.currentTarget, viewport);
|
|
230
|
+
const delta = {
|
|
231
|
+
x: point.x - activeMove.lastPoint.x,
|
|
232
|
+
y: point.y - activeMove.lastPoint.y,
|
|
233
|
+
};
|
|
234
|
+
if (delta.x !== 0 || delta.y !== 0) {
|
|
235
|
+
const bounds = { width: SKETCH_LIMITS.imageEdge, height: SKETCH_LIMITS.imageEdge };
|
|
236
|
+
if (activeMove.element.kind === "stroke") {
|
|
237
|
+
setStrokes((current) => current.map((stroke) => stroke.id === activeMove.element.id
|
|
238
|
+
? translateSketchStroke(stroke, delta, bounds)
|
|
239
|
+
: stroke));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
setAnnotations((current) => current.map((annotation) => annotation.id === activeMove.element.id
|
|
243
|
+
? translateSketchAnnotation(annotation, delta, bounds)
|
|
244
|
+
: annotation));
|
|
245
|
+
}
|
|
246
|
+
activeMove.lastPoint = point;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (pointerId.current !== event.pointerId)
|
|
251
|
+
return;
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
const point = pointFromEvent(event, event.currentTarget, viewport);
|
|
254
|
+
setDraftPoints((previous) => {
|
|
255
|
+
const last = previous.at(-1);
|
|
256
|
+
if (last && distance(last, point) < 1)
|
|
257
|
+
return previous;
|
|
258
|
+
if (previous.length >= SKETCH_LIMITS.points) {
|
|
259
|
+
draftPointLimitReached.current = true;
|
|
260
|
+
return previous;
|
|
261
|
+
}
|
|
262
|
+
return [...previous, point];
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function finishPointer(event) {
|
|
266
|
+
const activeMove = moveState.current;
|
|
267
|
+
if (activeMove?.pointerId === event.pointerId) {
|
|
268
|
+
moveState.current = null;
|
|
269
|
+
try {
|
|
270
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// The pointer may already have been released by the browser.
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (pointerId.current !== event.pointerId)
|
|
278
|
+
return;
|
|
279
|
+
pointerId.current = null;
|
|
280
|
+
try {
|
|
281
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// The pointer may already have been released by the browser.
|
|
285
|
+
}
|
|
286
|
+
if (draftPointLimitReached.current) {
|
|
287
|
+
draftPointLimitReached.current = false;
|
|
288
|
+
setDraftPoints([]);
|
|
289
|
+
setError(`A stroke can contain at most ${SKETCH_LIMITS.points.toLocaleString()} points. The incomplete stroke was discarded.`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
setDraftPoints((previous) => {
|
|
293
|
+
if (previous.length === 0)
|
|
294
|
+
return previous;
|
|
295
|
+
const points = tool === "rectangle" && previous.length > 1
|
|
296
|
+
? rectanglePoints(previous[0], previous.at(-1))
|
|
297
|
+
: previous;
|
|
298
|
+
const totalPoints = strokes.reduce((total, stroke) => total + stroke.points.length, 0) + points.length;
|
|
299
|
+
if (strokes.length >= SKETCH_LIMITS.strokes || totalPoints > SKETCH_LIMITS.points) {
|
|
300
|
+
setError("The sketch has reached its stroke limit. Undo a mark before drawing again.");
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
if (points.length < 2)
|
|
304
|
+
return [];
|
|
305
|
+
if (tool === "rectangle" && distance(previous[0], previous.at(-1)) < 1)
|
|
306
|
+
return [];
|
|
307
|
+
const stroke = {
|
|
308
|
+
id: `stroke-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
309
|
+
kind: tool === "rectangle" ? "rectangle" : "freehand",
|
|
310
|
+
width: SKETCH_STROKE_WIDTH,
|
|
311
|
+
color: SKETCH_STROKE_COLOR,
|
|
312
|
+
outlineColor: SKETCH_STROKE_OUTLINE,
|
|
313
|
+
points: points.map((point) => ({ ...point })),
|
|
314
|
+
};
|
|
315
|
+
setStrokes((current) => [...current, stroke]);
|
|
316
|
+
setRedoStrokes([]);
|
|
317
|
+
return [];
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function undo() {
|
|
321
|
+
setStrokes((current) => {
|
|
322
|
+
const last = current.at(-1);
|
|
323
|
+
if (!last)
|
|
324
|
+
return current;
|
|
325
|
+
setRedoStrokes((redo) => [...redo, last]);
|
|
326
|
+
return current.slice(0, -1);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function redo() {
|
|
330
|
+
setRedoStrokes((current) => {
|
|
331
|
+
const last = current.at(-1);
|
|
332
|
+
if (!last)
|
|
333
|
+
return current;
|
|
334
|
+
setStrokes((existing) => [...existing, last]);
|
|
335
|
+
return current.slice(0, -1);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function onEditorKeyDown(event) {
|
|
339
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
340
|
+
if (mod && event.key.toLowerCase() === "z") {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
event.stopPropagation();
|
|
343
|
+
if (event.shiftKey)
|
|
344
|
+
redo();
|
|
345
|
+
else
|
|
346
|
+
undo();
|
|
347
|
+
}
|
|
348
|
+
else if (mod && event.key.toLowerCase() === "y") {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
event.stopPropagation();
|
|
351
|
+
redo();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function onAnnotationKeyDown(event) {
|
|
355
|
+
if (event.key === "Enter") {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
commitActiveAnnotation();
|
|
358
|
+
}
|
|
359
|
+
else if (event.key === "Escape") {
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
cancelActiveAnnotation();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function done() {
|
|
365
|
+
if (submitting)
|
|
366
|
+
return;
|
|
367
|
+
const committedAnnotations = commitActiveAnnotation();
|
|
368
|
+
if (committedAnnotations === null)
|
|
369
|
+
return;
|
|
370
|
+
const trimmedDescription = description.trim();
|
|
371
|
+
if (strokes.length === 0 && committedAnnotations.length === 0 && trimmedDescription === "") {
|
|
372
|
+
setError("Add a mark or a short description before saving the sketch.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
setSubmitting(true);
|
|
376
|
+
setError(null);
|
|
377
|
+
try {
|
|
378
|
+
await onDone({
|
|
379
|
+
description: trimmedDescription,
|
|
380
|
+
strokes: strokes.map((stroke) => ({
|
|
381
|
+
...stroke,
|
|
382
|
+
points: stroke.points.map((point) => ({ ...point })),
|
|
383
|
+
})),
|
|
384
|
+
annotations: committedAnnotations.map((annotation) => ({
|
|
385
|
+
...annotation,
|
|
386
|
+
point: { ...annotation.point },
|
|
387
|
+
})),
|
|
388
|
+
viewport: getSketchViewport(hostElement),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (doneError) {
|
|
392
|
+
setError(doneError instanceof Error ? doneError.message : "The sketch could not be saved.");
|
|
393
|
+
setSubmitting(false);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const visibleStrokes = strokes.map((stroke) => visibleStroke(stroke, viewport));
|
|
397
|
+
const visibleAnnotations = annotations.map((annotation) => visibleAnnotation(annotation, viewport));
|
|
398
|
+
const draftStroke = draftPoints.length > 0 ? {
|
|
399
|
+
id: "draft",
|
|
400
|
+
kind: tool === "rectangle" ? "rectangle" : "freehand",
|
|
401
|
+
width: SKETCH_STROKE_WIDTH,
|
|
402
|
+
color: SKETCH_STROKE_COLOR,
|
|
403
|
+
outlineColor: SKETCH_STROKE_OUTLINE,
|
|
404
|
+
points: (tool === "rectangle" && draftPoints.length > 1
|
|
405
|
+
? rectanglePoints(draftPoints[0], draftPoints.at(-1))
|
|
406
|
+
: draftPoints).map((point) => ({
|
|
407
|
+
x: point.x - viewport.scrollX,
|
|
408
|
+
y: point.y - viewport.scrollY,
|
|
409
|
+
})),
|
|
410
|
+
} : null;
|
|
411
|
+
if (!open)
|
|
412
|
+
return null;
|
|
413
|
+
return (_jsx(Dialog.Root, { open: open, modal: false, disablePointerDismissal: true, onOpenChange: (nextOpen) => { if (!nextOpen && !submitting)
|
|
414
|
+
onCancel(); }, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "sketch__backdrop sketch__live-backdrop", "data-inspector-open": inspectorOpen ? "true" : "false", "data-test": "sketch-live-backdrop" }), _jsxs(Dialog.Popup, { className: "sketch__popup sketch__live-popup", "data-inspector-open": inspectorOpen ? "true" : "false", "data-test": "sketch-live-editor", onKeyDown: onEditorKeyDown, children: [_jsx(Dialog.Title, { className: "sketch__title sketch__sr-only", children: "Sketch viewport" }), _jsx(Dialog.Description, { className: "sketch__description sketch__sr-only", children: "Draw directly over the live interface, then add a short note for the sketch." }), _jsxs("div", { className: "sketch__canvas-frame sketch__live-frame", style: {
|
|
415
|
+
left: viewport.offsetX,
|
|
416
|
+
top: viewport.offsetY,
|
|
417
|
+
width: viewport.displayWidth,
|
|
418
|
+
height: viewport.displayHeight,
|
|
419
|
+
}, children: [_jsx("svg", { ref: svgRef, className: `sketch__canvas sketch__live-canvas sketch__canvas--${tool}`, width: viewport.width, height: viewport.height, viewBox: `0 0 ${viewport.width} ${viewport.height}`, preserveAspectRatio: "none", pointerEvents: "all", tabIndex: 0, "aria-label": "Sketch over the live interface", onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: finishPointer, onPointerCancel: finishPointer, children: _jsx(SketchSvgLayer, { strokes: draftStroke ? [...visibleStrokes, draftStroke] : visibleStrokes, annotations: visibleAnnotations }) }), activeAnnotation ? (_jsxs("div", { className: "sketch__annotation-editor", role: "dialog", "aria-label": `Annotation ${activeAnnotation.number}`, style: annotationEditorStyle(activeAnnotation, viewport), children: [_jsxs("label", { className: "sketch__sr-only", htmlFor: "sketch-annotation-description", children: ["Annotation ", activeAnnotation.number, " description"] }), _jsx("input", { ref: annotationInputRef, id: "sketch-annotation-description", value: annotationDraft, maxLength: SKETCH_LIMITS.description, placeholder: "Describe this area\u2026", autoComplete: "off", "data-test": "sketch-live-annotation-description", onChange: (event) => setAnnotationDraft(event.target.value), onKeyDown: onAnnotationKeyDown }), _jsx(IconButton, { label: "Cancel annotation", variant: "quiet", size: "compact", "data-test": "sketch-live-annotation-cancel", onClick: cancelActiveAnnotation, children: _jsx(IconX, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), _jsxs(Button, { variant: "primary", size: "compact", type: "button", "data-test": "sketch-live-annotation-save", onClick: () => { commitActiveAnnotation(); }, children: [_jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" }), "Add"] })] })) : null] }), _jsx(SketchPromptPanel, { dataTest: "sketch-live-prompt", className: "sketch__live-note-panel", description: description, error: error, saving: submitting, autoFocus: true, onDescriptionChange: setDescription, onDone: done, onCancel: onCancel })] })] }) }));
|
|
420
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface SketchPromptPanelProps {
|
|
3
|
+
readonly dataTest: string;
|
|
4
|
+
readonly className?: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly error?: string | null;
|
|
7
|
+
readonly disabled?: boolean;
|
|
8
|
+
readonly saving?: boolean;
|
|
9
|
+
readonly autoFocus?: boolean;
|
|
10
|
+
readonly onDescriptionChange: (value: string) => void;
|
|
11
|
+
readonly onDone: () => void | Promise<void>;
|
|
12
|
+
readonly onCancel?: () => void;
|
|
13
|
+
}
|
|
14
|
+
/** The small note editor shared by a new live sketch and saved sketch layers. */
|
|
15
|
+
export declare function SketchPromptPanel({ dataTest, className, description, error, disabled, saving, autoFocus, onDescriptionChange, onDone, onCancel, }: SketchPromptPanelProps): ReactElement;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "../ui/Button.js";
|
|
3
|
+
import { SKETCH_LIMITS } from "./model.js";
|
|
4
|
+
/** The small note editor shared by a new live sketch and saved sketch layers. */
|
|
5
|
+
export function SketchPromptPanel({ dataTest, className, description, error = null, disabled = false, saving = false, autoFocus = false, onDescriptionChange, onDone, onCancel, }) {
|
|
6
|
+
function onKeyDown(event) {
|
|
7
|
+
if (event.key === "Escape" && onCancel) {
|
|
8
|
+
event.preventDefault();
|
|
9
|
+
onCancel();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (event.key === "Enter" && !event.shiftKey && !disabled && !saving) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
void onDone();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return (_jsxs("section", { className: `sketch__prompt-panel${className ? ` ${className}` : ""}`, role: "dialog", "aria-label": "Sketch note", "data-test": dataTest, children: [_jsx("label", { className: "sketch__prompt-label sketch__sr-only", htmlFor: `${dataTest}-input`, children: "Sketch note" }), _jsx("textarea", { id: `${dataTest}-input`, className: "sketch__prompt-input", value: description, rows: 2, maxLength: SKETCH_LIMITS.description, placeholder: "Add a note to your sketch", "aria-label": "Sketch note", "data-test": `${dataTest}-input`, autoFocus: autoFocus, disabled: disabled, onChange: (event) => onDescriptionChange(event.target.value), onKeyDown: onKeyDown }), _jsxs("div", { className: "sketch__prompt-footer", children: [error ? _jsx("span", { className: "sketch__prompt-error", role: "status", children: error }) : _jsx("span", {}), _jsx(Button, { variant: "primary", size: "compact", type: "button", "data-test": `${dataTest}-done`, disabled: disabled || saving, onClick: () => { void onDone(); }, children: saving ? "Saving…" : "Done" })] })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { beginSketchCapture } from "./interaction.js";
|
|
3
|
+
import { type SketchLiveDraft } from "./SketchOverlay.js";
|
|
4
|
+
import type { CapturedSketch } from "./capture.js";
|
|
5
|
+
import type { SketchStroke } from "./model.js";
|
|
6
|
+
export interface SketchWorkspaceProps {
|
|
7
|
+
readonly hostElement: HTMLElement | null;
|
|
8
|
+
readonly projectId: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function projectLiveStrokes(draft: SketchLiveDraft, captured: CapturedSketch): SketchStroke[];
|
|
11
|
+
export declare function SketchWorkspace({ hostElement, projectId }: SketchWorkspaceProps): ReactElement;
|
|
12
|
+
export declare function startSketchCapture(hostElement: HTMLElement | null, initialTool?: Parameters<typeof beginSketchCapture>[0]): void;
|
|
13
|
+
export declare function editSketch(sketchId: string): void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { renderAnnotatedPng } from "./raster.js";
|
|
4
|
+
import { closeSketchEditor, completeSketchCapture, beginSketchCapture, cancelSketchInteraction, openSketchEditor, useSketchInteraction } from "./interaction.js";
|
|
5
|
+
import { initializeSketchStore, saveSketch, useSketchStore } from "./store.js";
|
|
6
|
+
import { clearSketchClipboardHandoff } from "./handoff.js";
|
|
7
|
+
import { SketchEditor } from "./SketchEditor.js";
|
|
8
|
+
import { SketchOverlay } from "./SketchOverlay.js";
|
|
9
|
+
import { closeSketchNote, useSketchNote } from "./sketchNote.js";
|
|
10
|
+
import { SketchPromptPanel } from "./SketchPromptPanel.js";
|
|
11
|
+
function clamp(value, maximum) {
|
|
12
|
+
return Math.max(0, Math.min(maximum, value));
|
|
13
|
+
}
|
|
14
|
+
export function projectLiveStrokes(draft, captured) {
|
|
15
|
+
const scaleX = captured.imageWidth / Math.max(1, draft.viewport.width);
|
|
16
|
+
const scaleY = captured.imageHeight / Math.max(1, draft.viewport.height);
|
|
17
|
+
const captureScrollX = captured.capture.scrollX;
|
|
18
|
+
const captureScrollY = captured.capture.scrollY;
|
|
19
|
+
const visibleRight = captureScrollX + draft.viewport.width;
|
|
20
|
+
const visibleBottom = captureScrollY + draft.viewport.height;
|
|
21
|
+
return draft.strokes.flatMap((stroke) => {
|
|
22
|
+
const visiblePoints = stroke.points.filter((point) => point.x >= captureScrollX
|
|
23
|
+
&& point.x <= visibleRight
|
|
24
|
+
&& point.y >= captureScrollY
|
|
25
|
+
&& point.y <= visibleBottom);
|
|
26
|
+
if (visiblePoints.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
return [{
|
|
29
|
+
...stroke,
|
|
30
|
+
width: stroke.width * Math.max(scaleX, scaleY),
|
|
31
|
+
points: visiblePoints.map((point) => ({
|
|
32
|
+
x: clamp((point.x - captureScrollX) * scaleX, captured.imageWidth),
|
|
33
|
+
y: clamp((point.y - captureScrollY) * scaleY, captured.imageHeight),
|
|
34
|
+
})),
|
|
35
|
+
}];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function projectLiveAnnotations(draft, captured) {
|
|
39
|
+
const scaleX = captured.imageWidth / Math.max(1, draft.viewport.width);
|
|
40
|
+
const scaleY = captured.imageHeight / Math.max(1, draft.viewport.height);
|
|
41
|
+
const captureScrollX = captured.capture.scrollX;
|
|
42
|
+
const captureScrollY = captured.capture.scrollY;
|
|
43
|
+
const visibleRight = captureScrollX + draft.viewport.width;
|
|
44
|
+
const visibleBottom = captureScrollY + draft.viewport.height;
|
|
45
|
+
return draft.annotations
|
|
46
|
+
.filter((annotation) => annotation.point.x >= captureScrollX
|
|
47
|
+
&& annotation.point.x <= visibleRight
|
|
48
|
+
&& annotation.point.y >= captureScrollY
|
|
49
|
+
&& annotation.point.y <= visibleBottom)
|
|
50
|
+
.map((annotation) => ({
|
|
51
|
+
...annotation,
|
|
52
|
+
point: {
|
|
53
|
+
x: clamp((annotation.point.x - captureScrollX) * scaleX, captured.imageWidth),
|
|
54
|
+
y: clamp((annotation.point.y - captureScrollY) * scaleY, captured.imageHeight),
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
export function SketchWorkspace({ hostElement, projectId }) {
|
|
59
|
+
const interaction = useSketchInteraction();
|
|
60
|
+
const store = useSketchStore();
|
|
61
|
+
const note = useSketchNote();
|
|
62
|
+
const editingDocument = interaction.editingId
|
|
63
|
+
? store.items.find((item) => item.document.id === interaction.editingId)?.document ?? null
|
|
64
|
+
: null;
|
|
65
|
+
// IndexedDB hydration is deliberately independent from the localStorage
|
|
66
|
+
// session schema. A failed hydration leaves the inspector usable and gives
|
|
67
|
+
// the Changes log a recovery message.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
clearSketchClipboardHandoff();
|
|
70
|
+
closeSketchNote();
|
|
71
|
+
void initializeSketchStore(projectId).catch(() => undefined);
|
|
72
|
+
}, [projectId]);
|
|
73
|
+
const liveOpen = interaction.editingId === null && interaction.captureState !== "idle";
|
|
74
|
+
const editorOpen = interaction.editingId !== null;
|
|
75
|
+
const noteDocument = note.sketchId
|
|
76
|
+
? store.items.find((item) => item.document.id === note.sketchId)?.document ?? null
|
|
77
|
+
: null;
|
|
78
|
+
async function save(input) {
|
|
79
|
+
await saveSketch(input);
|
|
80
|
+
closeSketchEditor();
|
|
81
|
+
}
|
|
82
|
+
async function saveLive(draft) {
|
|
83
|
+
if (!hostElement)
|
|
84
|
+
throw new Error("The inspector is no longer available.");
|
|
85
|
+
const captured = await completeSketchCapture(hostElement);
|
|
86
|
+
const strokes = projectLiveStrokes(draft, captured);
|
|
87
|
+
const annotations = projectLiveAnnotations(draft, captured);
|
|
88
|
+
const rendered = await renderAnnotatedPng(captured.originalImage, captured.imageWidth, captured.imageHeight, strokes, annotations);
|
|
89
|
+
await save({
|
|
90
|
+
capture: {
|
|
91
|
+
...captured.capture,
|
|
92
|
+
imageWidth: rendered.width,
|
|
93
|
+
imageHeight: rendered.height,
|
|
94
|
+
},
|
|
95
|
+
description: draft.description,
|
|
96
|
+
strokes,
|
|
97
|
+
annotations,
|
|
98
|
+
imageWidth: rendered.width,
|
|
99
|
+
imageHeight: rendered.height,
|
|
100
|
+
originalImage: captured.originalImage,
|
|
101
|
+
annotatedImage: rendered.blob,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function saveNote(description) {
|
|
105
|
+
if (!noteDocument)
|
|
106
|
+
return;
|
|
107
|
+
await saveSketch({
|
|
108
|
+
id: noteDocument.id,
|
|
109
|
+
capture: noteDocument.capture,
|
|
110
|
+
description: description.trim(),
|
|
111
|
+
strokes: noteDocument.strokes,
|
|
112
|
+
annotations: noteDocument.annotations ?? [],
|
|
113
|
+
imageWidth: noteDocument.imageWidth,
|
|
114
|
+
imageHeight: noteDocument.imageHeight,
|
|
115
|
+
originalImage: noteDocument.originalImage,
|
|
116
|
+
annotatedImage: noteDocument.annotatedImage,
|
|
117
|
+
});
|
|
118
|
+
closeSketchNote();
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs(_Fragment, { children: [hostElement ? (_jsx(SketchOverlay, { open: liveOpen, hostElement: hostElement, initialTool: interaction.initialTool, onCancel: cancelSketchInteraction, onDone: saveLive })) : null, _jsx(SketchEditor, { open: editorOpen, document: editingDocument, captured: interaction.captured, onCancel: cancelSketchInteraction, onSave: save }), noteDocument ? (_jsx(SavedSketchNote, { document: noteDocument, onSave: saveNote, onCancel: closeSketchNote })) : null] }));
|
|
121
|
+
}
|
|
122
|
+
function SavedSketchNote({ document, onSave, onCancel, }) {
|
|
123
|
+
const [description, setDescription] = useState(document.description);
|
|
124
|
+
const [saving, setSaving] = useState(false);
|
|
125
|
+
const [error, setError] = useState(null);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
setDescription(document.description);
|
|
128
|
+
setError(null);
|
|
129
|
+
}, [document.id, document.revision, document.description]);
|
|
130
|
+
async function done() {
|
|
131
|
+
if (saving)
|
|
132
|
+
return;
|
|
133
|
+
setSaving(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
try {
|
|
136
|
+
await onSave(description);
|
|
137
|
+
}
|
|
138
|
+
catch (saveError) {
|
|
139
|
+
setError(saveError instanceof Error ? saveError.message : "The sketch note could not be saved.");
|
|
140
|
+
setSaving(false);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return (_jsx(SketchPromptPanel, { dataTest: "sketch-note-prompt", className: "sketch__saved-note-panel", description: description, error: error, saving: saving, autoFocus: true, onDescriptionChange: setDescription, onDone: done, onCancel: onCancel }));
|
|
144
|
+
}
|
|
145
|
+
export function startSketchCapture(hostElement, initialTool = "pen") {
|
|
146
|
+
if (!hostElement)
|
|
147
|
+
return;
|
|
148
|
+
beginSketchCapture(initialTool);
|
|
149
|
+
}
|
|
150
|
+
export function editSketch(sketchId) {
|
|
151
|
+
openSketchEditor(sketchId);
|
|
152
|
+
}
|