nudge-ui 0.2.0 → 0.2.1
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 +872 -12
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
- package/dist/hosts/astro/clientTransport.js +60 -11
- package/dist/hosts/next/sidecar.d.ts +4 -0
- package/dist/hosts/next/sidecar.js +49 -6
- package/dist/hosts/next/wrapper.js +30 -2
- package/dist/hosts/projectBridge.d.ts +20 -0
- package/dist/hosts/projectBridge.js +97 -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 +1 -1
- package/dist/hosts/static/server.js +44 -3
- package/dist/hosts/vite/vite.d.ts +5 -1
- package/dist/hosts/vite/vite.js +78 -14
- package/dist/inspector/agent/McpConnectionDialog.d.ts +2 -2
- package/dist/inspector/agent/McpConnectionDialog.js +11 -35
- package/dist/inspector/agent/client.d.ts +10 -3
- package/dist/inspector/agent/client.js +80 -9
- package/dist/inspector/agent/connectionStatus.js +3 -3
- package/dist/inspector/agent/httpTransport.d.ts +2 -0
- package/dist/inspector/agent/httpTransport.js +37 -17
- package/dist/inspector/agent/protocol.d.ts +8 -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 +246 -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 +392 -73
- 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 +69 -5
- package/dist/inspector/canvas/frameProtocol.js +56 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -0
- package/dist/inspector/canvas/keyboardShortcuts.js +14 -0
- package/dist/inspector/canvas/projection.d.ts +7 -0
- package/dist/inspector/canvas/projection.js +118 -25
- package/dist/inspector/canvas/rendererBootstrap.js +29 -29
- package/dist/inspector/canvas/rendererElementSelector.js +341 -30
- 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/clientManifest.d.ts +5 -0
- package/dist/inspector/clientManifest.js +11 -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 +74 -24
- 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/selectedGeometry.js +21 -2
- package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
- package/dist/inspector/overlay/spacingGestures.js +353 -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 +4 -0
- package/dist/inspector/runtime/runtimeConfig.js +4 -0
- package/dist/inspector/selection/selectionStore.js +2 -0
- package/dist/inspector/settings/SettingsDialog.js +10 -0
- package/dist/inspector/shell/AppShell.js +5 -1
- package/dist/inspector/shell/ChangesLog.js +8 -4
- package/dist/inspector/shell/CopyPromptButton.js +95 -19
- package/dist/inspector/shell/EmptyState.js +1 -1
- package/dist/inspector/shell/InspectorShell.d.ts +0 -1
- package/dist/inspector/shell/InspectorShell.js +37 -38
- 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 +409 -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 +23 -30
- 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 +91 -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 +111 -7
- 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/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/styles.js +13 -11
- package/dist/internal/agent-protocol.d.ts +65 -1
- package/dist/internal/agent-protocol.js +183 -3
- package/dist/nudge-ui.mjs +261 -57
- package/dist/transport/editor.d.ts +28 -0
- package/dist/transport/editor.js +132 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.js +1 -0
- package/dist/transport/routes.d.ts +7 -0
- package/dist/transport/routes.js +7 -0
- package/package.json +3 -1
- package/dist/inspector/shell/panelLayout.d.ts +0 -2
- package/dist/inspector/shell/panelLayout.js +0 -29
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { SketchSvgLayer } from "./freehand.js";
|
|
4
|
+
import { useSketchStore } from "./store.js";
|
|
5
|
+
import { getSketchScrollPosition } from "./capture.js";
|
|
6
|
+
export function sketchRouteKey(url) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = new URL(url);
|
|
9
|
+
// These parameters belong to the inspector host, not to the application
|
|
10
|
+
// route. Removing them lets a sketch follow the same page in Focus mode,
|
|
11
|
+
// the canvas, and a direct editor launch.
|
|
12
|
+
parsed.searchParams.delete("__nudge_ui_direct");
|
|
13
|
+
parsed.searchParams.delete("nudge-ui");
|
|
14
|
+
// Hashes identify an in-document location, not a different sketch canvas.
|
|
15
|
+
// Keep saved marks attached when navigation changes only the anchor.
|
|
16
|
+
return `${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function frameRoute(iframe, fallback) {
|
|
23
|
+
try {
|
|
24
|
+
return sketchRouteKey(iframe.contentWindow?.location.href ?? fallback);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return sketchRouteKey(fallback);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function sameFrame(document, iframe, cardUrl) {
|
|
31
|
+
const sketchRoute = sketchRouteKey(document.capture.url);
|
|
32
|
+
if (!sketchRoute)
|
|
33
|
+
return false;
|
|
34
|
+
return sketchRoute === frameRoute(iframe, cardUrl);
|
|
35
|
+
}
|
|
36
|
+
export function projectSketchPoint(point, document, frameWindow, width, height) {
|
|
37
|
+
const capture = document.capture;
|
|
38
|
+
const imageWidth = Math.max(1, document.imageWidth);
|
|
39
|
+
const imageHeight = Math.max(1, document.imageHeight);
|
|
40
|
+
const viewportWidth = Math.max(1, frameWindow.innerWidth || width);
|
|
41
|
+
const viewportHeight = Math.max(1, frameWindow.innerHeight || height);
|
|
42
|
+
const scroll = getSketchScrollPosition(frameWindow);
|
|
43
|
+
// Preserve the point's relative horizontal position when the responsive
|
|
44
|
+
// iframe changes width. Apply the scroll delta separately so horizontal
|
|
45
|
+
// page scrolling still moves the sketch with the content.
|
|
46
|
+
const captureLocalX = (point.x / imageWidth) * capture.viewportWidth;
|
|
47
|
+
const currentLocalX = (captureLocalX / Math.max(1, capture.viewportWidth)) * viewportWidth
|
|
48
|
+
- (scroll.x - capture.scrollX);
|
|
49
|
+
const pageY = capture.scrollY + (point.y / imageHeight) * capture.viewportHeight;
|
|
50
|
+
return {
|
|
51
|
+
x: (currentLocalX / viewportWidth) * width,
|
|
52
|
+
y: ((pageY - scroll.y) / viewportHeight) * height,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function visibleStroke(stroke, document, frameWindow, width, height) {
|
|
56
|
+
const captureScale = document.capture.viewportWidth / Math.max(1, document.imageWidth);
|
|
57
|
+
const currentScale = width / Math.max(1, frameWindow.innerWidth || width);
|
|
58
|
+
return {
|
|
59
|
+
...stroke,
|
|
60
|
+
width: stroke.width * captureScale * currentScale,
|
|
61
|
+
points: stroke.points.map((point) => projectSketchPoint(point, document, frameWindow, width, height)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function SketchFrameOverlay({ iframe, cardUrl, ready }) {
|
|
65
|
+
const { items } = useSketchStore();
|
|
66
|
+
const [revision, setRevision] = useState(0);
|
|
67
|
+
const documents = useMemo(() => ready && iframe
|
|
68
|
+
? items.map((item) => item.document).filter((document) => sameFrame(document, iframe, cardUrl))
|
|
69
|
+
: [], [cardUrl, iframe, items, ready]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!iframe || documents.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
const frameWindow = iframe.contentWindow;
|
|
74
|
+
if (!frameWindow)
|
|
75
|
+
return;
|
|
76
|
+
const refresh = () => setRevision((value) => value + 1);
|
|
77
|
+
window.addEventListener("resize", refresh);
|
|
78
|
+
frameWindow.addEventListener("resize", refresh);
|
|
79
|
+
frameWindow.addEventListener("scroll", refresh, true);
|
|
80
|
+
const observer = typeof ResizeObserver === "undefined"
|
|
81
|
+
? null
|
|
82
|
+
: new ResizeObserver(refresh);
|
|
83
|
+
observer?.observe(iframe);
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener("resize", refresh);
|
|
86
|
+
frameWindow.removeEventListener("resize", refresh);
|
|
87
|
+
frameWindow.removeEventListener("scroll", refresh, true);
|
|
88
|
+
observer?.disconnect();
|
|
89
|
+
};
|
|
90
|
+
}, [documents.length, iframe]);
|
|
91
|
+
const width = iframe
|
|
92
|
+
? Math.max(1, iframe.clientWidth || iframe.getBoundingClientRect().width)
|
|
93
|
+
: 1;
|
|
94
|
+
const height = iframe
|
|
95
|
+
? Math.max(1, iframe.clientHeight || iframe.getBoundingClientRect().height)
|
|
96
|
+
: 1;
|
|
97
|
+
const frameWindow = iframe?.contentWindow ?? null;
|
|
98
|
+
const layers = useMemo(() => {
|
|
99
|
+
if (!iframe || !frameWindow || documents.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
strokes: [],
|
|
102
|
+
annotations: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const strokes = [];
|
|
106
|
+
const annotations = [];
|
|
107
|
+
for (const document of documents) {
|
|
108
|
+
strokes.push(...document.strokes.map((stroke) => visibleStroke(stroke, document, frameWindow, width, height)));
|
|
109
|
+
annotations.push(...(document.annotations ?? []).map((annotation) => ({
|
|
110
|
+
...annotation,
|
|
111
|
+
point: projectSketchPoint(annotation.point, document, frameWindow, width, height),
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
114
|
+
return { strokes, annotations };
|
|
115
|
+
}, [documents, frameWindow, height, iframe, revision, width]);
|
|
116
|
+
if (!iframe || documents.length === 0 || !frameWindow)
|
|
117
|
+
return null;
|
|
118
|
+
return (_jsx("svg", { className: "canvas-card__sketch-overlay", width: width, height: height, viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", "aria-hidden": "true", "data-test": "canvas-sketch-overlay", children: _jsx(SketchSvgLayer, { strokes: layers.strokes, annotations: layers.annotations }) }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { IconCheck, IconCopy, IconX } from "@tabler/icons-react";
|
|
4
|
+
import { Button } from "../ui/Button.js";
|
|
5
|
+
import { IconButton } from "../ui/IconButton.js";
|
|
6
|
+
import { copyImageToClipboard } from "./raster.js";
|
|
7
|
+
import { closeSketchNote } from "./sketchNote.js";
|
|
8
|
+
import { removeSketch, useSketchStore } from "./store.js";
|
|
9
|
+
function SketchThumbnail({ item }) {
|
|
10
|
+
const [url, setUrl] = useState(null);
|
|
11
|
+
const hasMarks = item.document.strokes.length > 0 || (item.document.annotations?.length ?? 0) > 0;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!hasMarks) {
|
|
14
|
+
setUrl(null);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function")
|
|
18
|
+
return;
|
|
19
|
+
const next = URL.createObjectURL(item.document.annotatedImage);
|
|
20
|
+
setUrl(next);
|
|
21
|
+
return () => URL.revokeObjectURL(next);
|
|
22
|
+
}, [hasMarks, item.document.annotatedImage]);
|
|
23
|
+
return hasMarks && url ? (_jsx("img", { className: "sketch-layers__thumbnail", src: url, alt: "", "aria-hidden": "true" })) : (_jsx("span", { className: "sketch-layers__thumbnail sketch-layers__thumbnail--empty", "aria-hidden": "true" }));
|
|
24
|
+
}
|
|
25
|
+
function SketchLayer({ item, index }) {
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
const busy = item.status === "dispatching" || item.status === "handing-off";
|
|
29
|
+
async function copyImage() {
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
await copyImageToClipboard(item.document.annotatedImage);
|
|
33
|
+
setCopied(true);
|
|
34
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
35
|
+
}
|
|
36
|
+
catch (copyError) {
|
|
37
|
+
setError(copyError instanceof Error ? copyError.message : "The sketch image could not be copied.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function deleteLayer() {
|
|
41
|
+
setError(null);
|
|
42
|
+
try {
|
|
43
|
+
if (!await removeSketch(item.document.id)) {
|
|
44
|
+
setError("This sketch is being sent and cannot be deleted yet.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
closeSketchNote();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (deleteError) {
|
|
51
|
+
setError(deleteError instanceof Error ? deleteError.message : "The sketch could not be deleted.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return (_jsxs("div", { className: "sketch-layers__item", "data-test": "sketch-layer", "data-sketch-id": item.document.id, children: [_jsxs(Button, { className: "sketch-layers__copy", variant: "secondary", type: "button", "data-test": "sketch-layer-copy-image", onClick: () => void copyImage(), disabled: busy, children: [_jsxs("span", { className: "sketch-layers__copy-label", children: [copied
|
|
55
|
+
? _jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" })
|
|
56
|
+
: _jsx(IconCopy, { size: "var(--icon-size-small)", "aria-hidden": "true" }), _jsx("span", { className: "sketch-layers__name", children: copied ? "Copied" : `Sketch ${index + 1}` })] }), _jsx(SketchThumbnail, { item: item })] }), _jsx(IconButton, { className: "sketch-layers__delete", variant: "quiet", size: "compact", label: "Delete sketch", title: "Delete sketch", "data-test": "sketch-layer-delete", onClick: () => void deleteLayer(), disabled: busy, children: _jsx(IconX, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), error ? _jsx("span", { className: "sketch-layers__error", role: "status", children: error }) : null] }));
|
|
57
|
+
}
|
|
58
|
+
export function SketchLayersPanel() {
|
|
59
|
+
const { items } = useSketchStore();
|
|
60
|
+
if (items.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
return (_jsxs("section", { className: "sketch-layers", "data-test": "sketch-layers", "aria-label": "Sketch layers", children: [items.map((item, index) => (_jsx(SketchLayer, { item: item, index: index }, `${item.document.id}:${item.document.revision}`))), _jsx("p", { className: "sketch-layers__hint", "data-test": "sketch-copy-helper", children: "Copy prompt and sketch individually into agent" })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type SketchViewport } from "./capture.js";
|
|
3
|
+
import { type SketchAnnotation, type SketchStroke } from "./model.js";
|
|
4
|
+
import type { SketchEntryTool } from "./interaction.js";
|
|
5
|
+
export interface SketchLiveDraft {
|
|
6
|
+
/** Kept for compatibility with sketches created with the original editor. */
|
|
7
|
+
readonly description: string;
|
|
8
|
+
readonly strokes: readonly SketchStroke[];
|
|
9
|
+
readonly annotations: readonly SketchAnnotation[];
|
|
10
|
+
readonly viewport: SketchViewport;
|
|
11
|
+
}
|
|
12
|
+
export interface SketchOverlayProps {
|
|
13
|
+
readonly open: boolean;
|
|
14
|
+
readonly hostElement: HTMLElement;
|
|
15
|
+
readonly initialTool?: SketchEntryTool;
|
|
16
|
+
readonly onCancel: () => void;
|
|
17
|
+
readonly onDone: (draft: SketchLiveDraft) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function SketchOverlay({ open, hostElement, initialTool, onCancel, onDone }: SketchOverlayProps): ReactElement | null;
|
|
@@ -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
|
+
}
|