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
|
@@ -14,6 +14,12 @@ function deeplyFocusedElement() {
|
|
|
14
14
|
export function isEditableEvent(event) {
|
|
15
15
|
return event.composedPath().some(isEditableTarget) || isEditableTarget(deeplyFocusedElement());
|
|
16
16
|
}
|
|
17
|
+
export const SEND_PROMPT_HOTKEY_EVENT = "nudge-ui-send-prompt-hotkey";
|
|
18
|
+
export function isSendPromptShortcut(event) {
|
|
19
|
+
if (event.repeat || event.altKey || isEditableEvent(event))
|
|
20
|
+
return false;
|
|
21
|
+
return event.shiftKey && !event.metaKey && !event.ctrlKey && event.code === "KeyS";
|
|
22
|
+
}
|
|
17
23
|
export function isInspectorToggleShortcut(event) {
|
|
18
24
|
if (isEditableEvent(event))
|
|
19
25
|
return false;
|
|
@@ -23,3 +29,12 @@ export function isInspectorToggleShortcut(event) {
|
|
|
23
29
|
return true;
|
|
24
30
|
return event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.code === "KeyI";
|
|
25
31
|
}
|
|
32
|
+
export function isOriginalPreviewShortcut(event) {
|
|
33
|
+
if (event.repeat)
|
|
34
|
+
return false;
|
|
35
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
|
|
36
|
+
return false;
|
|
37
|
+
if (event.code !== "Backslash")
|
|
38
|
+
return false;
|
|
39
|
+
return !isEditableEvent(event);
|
|
40
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { IconCheck, IconCopy, IconDownload, IconEdit, IconRefresh, IconTrash } from "@tabler/icons-react";
|
|
4
|
+
import { Button } from "../ui/Button.js";
|
|
5
|
+
import { StatusCallout } from "../ui/StatusCallout.js";
|
|
6
|
+
import { copyImageToClipboard, downloadImage } from "./raster.js";
|
|
7
|
+
import { editSketch } from "./SketchWorkspace.js";
|
|
8
|
+
import { removeSketch, requeueSketch, useSketchStore } from "./store.js";
|
|
9
|
+
import {} from "./model.js";
|
|
10
|
+
function routeLabel(url) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = new URL(url);
|
|
13
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function statusLabel(status) {
|
|
20
|
+
switch (status) {
|
|
21
|
+
case "dispatching": return "Sending";
|
|
22
|
+
case "handing-off": return "Handing off";
|
|
23
|
+
case "needs-review": return "Needs review";
|
|
24
|
+
case "unknown": return "Unknown";
|
|
25
|
+
default: return "Pending";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function Thumbnail({ item }) {
|
|
29
|
+
const [url, setUrl] = useState(null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function")
|
|
32
|
+
return;
|
|
33
|
+
const next = URL.createObjectURL(item.document.annotatedImage);
|
|
34
|
+
setUrl(next);
|
|
35
|
+
return () => URL.revokeObjectURL(next);
|
|
36
|
+
}, [item.document.annotatedImage]);
|
|
37
|
+
return url ? _jsx("img", { className: "sketch-change__thumbnail", src: url, alt: "Sketch annotation", "data-test": "sketch-thumbnail" }) : _jsx("div", { className: "sketch-change__thumbnail sketch-change__thumbnail--empty", "data-test": "sketch-thumbnail" });
|
|
38
|
+
}
|
|
39
|
+
function SketchRow({ item }) {
|
|
40
|
+
const [actionError, setActionError] = useState(null);
|
|
41
|
+
const busy = item.status === "dispatching" || item.status === "handing-off";
|
|
42
|
+
const canReview = item.status === "needs-review" || item.status === "unknown";
|
|
43
|
+
const document = item.document;
|
|
44
|
+
async function copyImage() {
|
|
45
|
+
setActionError(null);
|
|
46
|
+
try {
|
|
47
|
+
await copyImageToClipboard(document.annotatedImage);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
setActionError(error instanceof Error ? error.message : "The image could not be copied.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function runQueueAction(action, failureMessage) {
|
|
54
|
+
setActionError(null);
|
|
55
|
+
try {
|
|
56
|
+
if (!await action())
|
|
57
|
+
setActionError(failureMessage);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
setActionError(error instanceof Error ? error.message : failureMessage);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return (_jsxs("div", { className: "sketch-change__row", "data-test": "sketch-row", "data-sketch-id": document.id, children: [_jsx(Thumbnail, { item: item }), _jsxs("div", { className: "sketch-change__body", children: [_jsxs("div", { className: "sketch-change__heading", children: [_jsx("span", { className: "sketch-change__name", children: document.filename }), _jsx("span", { className: "sketch-change__status", "data-test": "sketch-status", "data-status": item.status, children: statusLabel(item.status) })] }), _jsxs("span", { className: "sketch-change__route", children: [routeLabel(document.capture.url), " \u00B7 revision ", document.revision] }), document.description ? _jsx("p", { className: "sketch-change__description", children: document.description }) : null, document.annotations?.length ? (_jsx("ol", { className: "sketch-change__annotations", children: document.annotations.map((annotation) => (_jsxs("li", { children: [_jsxs("span", { children: [annotation.number, "."] }), " ", annotation.description] }, annotation.id))) })) : null, _jsxs("div", { className: "sketch-change__actions", children: [_jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-copy-image", onClick: () => void copyImage(), children: [_jsx(IconCopy, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Copy image"] }), _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-download-image", onClick: () => downloadImage(document.annotatedImage, document.filename), children: [_jsx(IconDownload, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Download"] }), _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-reopen", onClick: () => editSketch(document.id), disabled: busy, children: [_jsx(IconEdit, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Reopen"] }), canReview ? (_jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-requeue", onClick: () => void runQueueAction(() => requeueSketch(document.id), "The sketch could not be requeued."), disabled: busy, children: [_jsx(IconRefresh, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Requeue"] })) : null, _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-mark-done", onClick: () => void runQueueAction(() => removeSketch(document.id), "The sketch could not be removed yet."), disabled: busy, children: [canReview ? _jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" }) : _jsx(IconTrash, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " ", canReview ? "Mark done" : "Delete"] })] }), actionError ? _jsx("span", { className: "sketch-change__error", role: "status", children: actionError }) : null] })] }));
|
|
64
|
+
}
|
|
65
|
+
export function SketchChanges() {
|
|
66
|
+
const store = useSketchStore();
|
|
67
|
+
const items = store.items;
|
|
68
|
+
const hasError = store.error !== null;
|
|
69
|
+
if (items.length === 0 && !hasError)
|
|
70
|
+
return null;
|
|
71
|
+
return (_jsxs("section", { className: "sketch-change", "data-test": "sketches", children: [_jsxs("div", { className: "sketch-change__title", children: ["Sketch annotations ", _jsx("span", { children: items.length })] }), store.error ? _jsx(StatusCallout, { tone: "warning", "data-test": "sketch-storage-error", children: store.error }) : null, items.length > 0 ? items.map((item) => _jsx(SketchRow, { item: item }, `${item.document.id}:${item.document.revision}`)) : (_jsx(StatusCallout, { tone: "warning", children: "Saved sketches need recovery. Reload the page to try again." }))] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type SketchAnnotation, type SketchDocument, type SketchStroke } from "./model.js";
|
|
3
|
+
import type { CapturedSketch } from "./capture.js";
|
|
4
|
+
export interface SketchEditorProps {
|
|
5
|
+
readonly open: boolean;
|
|
6
|
+
readonly document: SketchDocument | null;
|
|
7
|
+
readonly captured: CapturedSketch | null;
|
|
8
|
+
readonly onCancel: () => void;
|
|
9
|
+
readonly onSave: (input: {
|
|
10
|
+
readonly id?: string;
|
|
11
|
+
readonly capture: SketchDocument["capture"];
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly strokes: readonly SketchStroke[];
|
|
14
|
+
readonly annotations?: readonly SketchAnnotation[];
|
|
15
|
+
readonly imageWidth: number;
|
|
16
|
+
readonly imageHeight: number;
|
|
17
|
+
readonly originalImage: Blob;
|
|
18
|
+
readonly annotatedImage: Blob;
|
|
19
|
+
}) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function SketchEditor({ open, document: sketchDocument, captured, onCancel, onSave }: SketchEditorProps): ReactElement | null;
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
4
|
+
import { IconArrowsMove, IconCheck, IconMessageCirclePlus, IconPencil, IconRectangle, IconX, } from "@tabler/icons-react";
|
|
5
|
+
import { Button } from "../ui/Button.js";
|
|
6
|
+
import { IconButton } from "../ui/IconButton.js";
|
|
7
|
+
import { StatusCallout } from "../ui/StatusCallout.js";
|
|
8
|
+
import { renderAnnotatedPng } from "./raster.js";
|
|
9
|
+
import { SketchSvgLayer } from "./freehand.js";
|
|
10
|
+
import { findSketchElementAt, translateSketchAnnotation, translateSketchStroke, } from "./geometry.js";
|
|
11
|
+
import { SKETCH_LIMITS, SKETCH_ANNOTATION_RADIUS, SKETCH_STROKE_COLOR, SKETCH_STROKE_OUTLINE, SKETCH_STROKE_WIDTH, } from "./model.js";
|
|
12
|
+
function portalContainer() {
|
|
13
|
+
return typeof document !== "undefined"
|
|
14
|
+
? document.getElementById("nudge-ui-root")?.shadowRoot ?? document.body
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
function objectUrl(blob) {
|
|
18
|
+
if (!blob || typeof URL === "undefined" || typeof URL.createObjectURL !== "function")
|
|
19
|
+
return null;
|
|
20
|
+
return URL.createObjectURL(blob);
|
|
21
|
+
}
|
|
22
|
+
function clamp(value, maximum) {
|
|
23
|
+
return Math.max(0, Math.min(maximum, value));
|
|
24
|
+
}
|
|
25
|
+
function pointFromEvent(event, svg, width, height) {
|
|
26
|
+
const bounds = svg.getBoundingClientRect();
|
|
27
|
+
return {
|
|
28
|
+
x: clamp(((event.clientX - bounds.left) / Math.max(1, bounds.width)) * width, width),
|
|
29
|
+
y: clamp(((event.clientY - bounds.top) / Math.max(1, bounds.height)) * height, height),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function distance(left, right) {
|
|
33
|
+
return Math.hypot(left.x - right.x, left.y - right.y);
|
|
34
|
+
}
|
|
35
|
+
function rectanglePoints(start, end) {
|
|
36
|
+
const left = Math.min(start.x, end.x);
|
|
37
|
+
const right = Math.max(start.x, end.x);
|
|
38
|
+
const top = Math.min(start.y, end.y);
|
|
39
|
+
const bottom = Math.max(start.y, end.y);
|
|
40
|
+
return [
|
|
41
|
+
{ x: left, y: top },
|
|
42
|
+
{ x: right, y: top },
|
|
43
|
+
{ x: right, y: bottom },
|
|
44
|
+
{ x: left, y: bottom },
|
|
45
|
+
{ x: left, y: top },
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
export function SketchEditor({ open, document: sketchDocument, captured, onCancel, onSave }) {
|
|
49
|
+
const image = captured?.originalImage ?? sketchDocument?.originalImage ?? null;
|
|
50
|
+
const imageWidth = captured?.imageWidth ?? sketchDocument?.imageWidth ?? 0;
|
|
51
|
+
const imageHeight = captured?.imageHeight ?? sketchDocument?.imageHeight ?? 0;
|
|
52
|
+
const capture = captured?.capture ?? sketchDocument?.capture ?? null;
|
|
53
|
+
const [description, setDescription] = useState("");
|
|
54
|
+
const [tool, setTool] = useState("pen");
|
|
55
|
+
const [strokes, setStrokes] = useState([]);
|
|
56
|
+
const [redoStrokes, setRedoStrokes] = useState([]);
|
|
57
|
+
const [annotations, setAnnotations] = useState([]);
|
|
58
|
+
const [activeAnnotationId, setActiveAnnotationId] = useState(null);
|
|
59
|
+
const [activeAnnotationOriginal, setActiveAnnotationOriginal] = useState(null);
|
|
60
|
+
const [annotationDraft, setAnnotationDraft] = useState("");
|
|
61
|
+
const [draftPoints, setDraftPoints] = useState([]);
|
|
62
|
+
const [saving, setSaving] = useState(false);
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
const imageUrl = useMemo(() => objectUrl(image), [image]);
|
|
65
|
+
const svgRef = useRef(null);
|
|
66
|
+
const annotationInputRef = useRef(null);
|
|
67
|
+
const pointerId = useRef(null);
|
|
68
|
+
const moveState = useRef(null);
|
|
69
|
+
const draftPointLimitReached = useRef(false);
|
|
70
|
+
const activeAnnotation = annotations.find((annotation) => annotation.id === activeAnnotationId) ?? null;
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!open)
|
|
73
|
+
return;
|
|
74
|
+
setDescription(sketchDocument?.description ?? "");
|
|
75
|
+
setTool("pen");
|
|
76
|
+
setStrokes(sketchDocument ? sketchDocument.strokes.map((stroke) => ({ ...stroke, points: stroke.points.map((point) => ({ ...point })) })) : []);
|
|
77
|
+
setRedoStrokes([]);
|
|
78
|
+
setAnnotations(sketchDocument?.annotations?.map((annotation) => ({
|
|
79
|
+
...annotation,
|
|
80
|
+
point: { ...annotation.point },
|
|
81
|
+
})) ?? []);
|
|
82
|
+
setActiveAnnotationId(null);
|
|
83
|
+
setActiveAnnotationOriginal(null);
|
|
84
|
+
setAnnotationDraft("");
|
|
85
|
+
setDraftPoints([]);
|
|
86
|
+
moveState.current = null;
|
|
87
|
+
setError(null);
|
|
88
|
+
setSaving(false);
|
|
89
|
+
}, [open, sketchDocument?.id, sketchDocument?.revision]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (activeAnnotationId !== null)
|
|
92
|
+
annotationInputRef.current?.focus();
|
|
93
|
+
}, [activeAnnotationId]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (open)
|
|
96
|
+
svgRef.current?.focus();
|
|
97
|
+
}, [open]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return () => {
|
|
100
|
+
if (imageUrl && typeof URL !== "undefined")
|
|
101
|
+
URL.revokeObjectURL(imageUrl);
|
|
102
|
+
};
|
|
103
|
+
}, [imageUrl]);
|
|
104
|
+
function openAnnotation(annotation) {
|
|
105
|
+
setActiveAnnotationId(annotation.id);
|
|
106
|
+
setActiveAnnotationOriginal(annotation.description);
|
|
107
|
+
setAnnotationDraft(annotation.description);
|
|
108
|
+
setError(null);
|
|
109
|
+
}
|
|
110
|
+
function annotationAt(point) {
|
|
111
|
+
return [...annotations].reverse().find((annotation) => distance(annotation.point, point) <= SKETCH_ANNOTATION_RADIUS * 1.5) ?? null;
|
|
112
|
+
}
|
|
113
|
+
function commitActiveAnnotation() {
|
|
114
|
+
if (activeAnnotationId === null)
|
|
115
|
+
return annotations;
|
|
116
|
+
const nextDescription = annotationDraft.trim();
|
|
117
|
+
if (!nextDescription) {
|
|
118
|
+
setError("Add a short description for this annotation before continuing.");
|
|
119
|
+
annotationInputRef.current?.focus();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const next = annotations.map((annotation) => annotation.id === activeAnnotationId
|
|
123
|
+
? { ...annotation, description: nextDescription }
|
|
124
|
+
: annotation);
|
|
125
|
+
setAnnotations(next);
|
|
126
|
+
setActiveAnnotationId(null);
|
|
127
|
+
setActiveAnnotationOriginal(null);
|
|
128
|
+
setAnnotationDraft("");
|
|
129
|
+
setError(null);
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
function cancelActiveAnnotation() {
|
|
133
|
+
if (activeAnnotationId !== null && activeAnnotationOriginal === "") {
|
|
134
|
+
setAnnotations((current) => current.filter((annotation) => annotation.id !== activeAnnotationId));
|
|
135
|
+
}
|
|
136
|
+
setActiveAnnotationId(null);
|
|
137
|
+
setActiveAnnotationOriginal(null);
|
|
138
|
+
setAnnotationDraft("");
|
|
139
|
+
setError(null);
|
|
140
|
+
}
|
|
141
|
+
function selectTool(nextTool) {
|
|
142
|
+
if (activeAnnotationId !== null && !commitActiveAnnotation())
|
|
143
|
+
return;
|
|
144
|
+
setTool(nextTool);
|
|
145
|
+
setError(null);
|
|
146
|
+
}
|
|
147
|
+
function annotationEditorStyle(annotation) {
|
|
148
|
+
const bounds = svgRef.current?.getBoundingClientRect();
|
|
149
|
+
const scaleX = (bounds?.width ?? imageWidth) / Math.max(1, imageWidth);
|
|
150
|
+
const scaleY = (bounds?.height ?? imageHeight) / Math.max(1, imageHeight);
|
|
151
|
+
const width = 280;
|
|
152
|
+
const height = 52;
|
|
153
|
+
const availableWidth = bounds?.width ?? imageWidth;
|
|
154
|
+
const availableHeight = bounds?.height ?? imageHeight;
|
|
155
|
+
return {
|
|
156
|
+
left: clamp(annotation.point.x * scaleX + SKETCH_ANNOTATION_RADIUS * scaleX + 8, Math.max(8, availableWidth - width - 8)),
|
|
157
|
+
top: clamp(annotation.point.y * scaleY + SKETCH_ANNOTATION_RADIUS * scaleY + 8, Math.max(8, availableHeight - height - 8)),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function onPointerDown(event) {
|
|
161
|
+
if (saving || !imageWidth || !imageHeight || pointerId.current !== null || moveState.current !== null)
|
|
162
|
+
return;
|
|
163
|
+
const point = pointFromEvent(event, event.currentTarget, imageWidth, imageHeight);
|
|
164
|
+
if (tool === "move") {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
const element = findSketchElementAt(point, strokes, annotations);
|
|
167
|
+
if (!element)
|
|
168
|
+
return;
|
|
169
|
+
moveState.current = { pointerId: event.pointerId, element, lastPoint: point };
|
|
170
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (tool === "annotate") {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (activeAnnotationId !== null) {
|
|
176
|
+
if (commitActiveAnnotation())
|
|
177
|
+
setError("Select Annotate again, then click another area.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const existing = annotationAt(point);
|
|
181
|
+
if (existing) {
|
|
182
|
+
openAnnotation(existing);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (annotations.length >= SKETCH_LIMITS.annotations) {
|
|
186
|
+
setError(`A sketch can contain at most ${SKETCH_LIMITS.annotations} annotations.`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const usedNumbers = new Set(annotations.map((annotation) => annotation.number));
|
|
190
|
+
let nextNumber = 1;
|
|
191
|
+
while (usedNumbers.has(nextNumber))
|
|
192
|
+
nextNumber += 1;
|
|
193
|
+
const annotation = {
|
|
194
|
+
id: `annotation-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
195
|
+
number: nextNumber,
|
|
196
|
+
point,
|
|
197
|
+
description: "",
|
|
198
|
+
};
|
|
199
|
+
setAnnotations((current) => [...current, annotation]);
|
|
200
|
+
setActiveAnnotationId(annotation.id);
|
|
201
|
+
setActiveAnnotationOriginal("");
|
|
202
|
+
setAnnotationDraft("");
|
|
203
|
+
setError(null);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
pointerId.current = event.pointerId;
|
|
208
|
+
draftPointLimitReached.current = false;
|
|
209
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
210
|
+
setDraftPoints([point]);
|
|
211
|
+
}
|
|
212
|
+
function onPointerMove(event) {
|
|
213
|
+
const activeMove = moveState.current;
|
|
214
|
+
if (activeMove?.pointerId === event.pointerId) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
const point = pointFromEvent(event, event.currentTarget, imageWidth, imageHeight);
|
|
217
|
+
const delta = {
|
|
218
|
+
x: point.x - activeMove.lastPoint.x,
|
|
219
|
+
y: point.y - activeMove.lastPoint.y,
|
|
220
|
+
};
|
|
221
|
+
if (delta.x !== 0 || delta.y !== 0) {
|
|
222
|
+
const bounds = { width: imageWidth, height: imageHeight };
|
|
223
|
+
if (activeMove.element.kind === "stroke") {
|
|
224
|
+
setStrokes((current) => current.map((stroke) => stroke.id === activeMove.element.id
|
|
225
|
+
? translateSketchStroke(stroke, delta, bounds)
|
|
226
|
+
: stroke));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
setAnnotations((current) => current.map((annotation) => annotation.id === activeMove.element.id
|
|
230
|
+
? translateSketchAnnotation(annotation, delta, bounds)
|
|
231
|
+
: annotation));
|
|
232
|
+
}
|
|
233
|
+
activeMove.lastPoint = point;
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (pointerId.current !== event.pointerId)
|
|
238
|
+
return;
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
const point = pointFromEvent(event, event.currentTarget, imageWidth, imageHeight);
|
|
241
|
+
setDraftPoints((previous) => {
|
|
242
|
+
const last = previous.at(-1);
|
|
243
|
+
if (last && distance(last, point) < 1)
|
|
244
|
+
return previous;
|
|
245
|
+
if (previous.length >= SKETCH_LIMITS.points) {
|
|
246
|
+
draftPointLimitReached.current = true;
|
|
247
|
+
return previous;
|
|
248
|
+
}
|
|
249
|
+
return [...previous, point];
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function finishPointer(event) {
|
|
253
|
+
const activeMove = moveState.current;
|
|
254
|
+
if (activeMove?.pointerId === event.pointerId) {
|
|
255
|
+
moveState.current = null;
|
|
256
|
+
try {
|
|
257
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// The pointer may already have been released by the browser.
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (pointerId.current !== event.pointerId)
|
|
265
|
+
return;
|
|
266
|
+
pointerId.current = null;
|
|
267
|
+
try {
|
|
268
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// The pointer may already have been released by the browser.
|
|
272
|
+
}
|
|
273
|
+
if (draftPointLimitReached.current) {
|
|
274
|
+
draftPointLimitReached.current = false;
|
|
275
|
+
setDraftPoints([]);
|
|
276
|
+
setError(`A stroke can contain at most ${SKETCH_LIMITS.points.toLocaleString()} points. The incomplete stroke was discarded.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
setDraftPoints((previous) => {
|
|
280
|
+
if (previous.length === 0)
|
|
281
|
+
return previous;
|
|
282
|
+
const points = tool === "rectangle" && previous.length > 1
|
|
283
|
+
? rectanglePoints(previous[0], previous.at(-1))
|
|
284
|
+
: previous;
|
|
285
|
+
const totalPoints = strokes.reduce((total, stroke) => total + stroke.points.length, 0) + points.length;
|
|
286
|
+
if (strokes.length >= SKETCH_LIMITS.strokes || totalPoints > SKETCH_LIMITS.points) {
|
|
287
|
+
setError("The sketch has reached its stroke limit. Undo a mark before drawing again.");
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
if (points.length < 2)
|
|
291
|
+
return [];
|
|
292
|
+
if (tool === "rectangle" && distance(previous[0], previous.at(-1)) < 1)
|
|
293
|
+
return [];
|
|
294
|
+
const stroke = {
|
|
295
|
+
id: `stroke-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
296
|
+
kind: tool === "rectangle" ? "rectangle" : "freehand",
|
|
297
|
+
width: SKETCH_STROKE_WIDTH,
|
|
298
|
+
color: SKETCH_STROKE_COLOR,
|
|
299
|
+
outlineColor: SKETCH_STROKE_OUTLINE,
|
|
300
|
+
points: points.map((point) => ({ ...point })),
|
|
301
|
+
};
|
|
302
|
+
setStrokes((current) => [...current, stroke]);
|
|
303
|
+
setRedoStrokes([]);
|
|
304
|
+
return [];
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function undo() {
|
|
308
|
+
setStrokes((current) => {
|
|
309
|
+
const last = current.at(-1);
|
|
310
|
+
if (!last)
|
|
311
|
+
return current;
|
|
312
|
+
setRedoStrokes((redo) => [...redo, last]);
|
|
313
|
+
return current.slice(0, -1);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function redo() {
|
|
317
|
+
setRedoStrokes((current) => {
|
|
318
|
+
const last = current.at(-1);
|
|
319
|
+
if (!last)
|
|
320
|
+
return current;
|
|
321
|
+
setStrokes((existing) => [...existing, last]);
|
|
322
|
+
return current.slice(0, -1);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function onEditorKeyDown(event) {
|
|
326
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
327
|
+
if (mod && event.key.toLowerCase() === "z") {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
event.stopPropagation();
|
|
330
|
+
if (event.shiftKey)
|
|
331
|
+
redo();
|
|
332
|
+
else
|
|
333
|
+
undo();
|
|
334
|
+
}
|
|
335
|
+
else if (mod && event.key.toLowerCase() === "y") {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
event.stopPropagation();
|
|
338
|
+
redo();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function onAnnotationKeyDown(event) {
|
|
342
|
+
if (event.key === "Enter") {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
commitActiveAnnotation();
|
|
345
|
+
}
|
|
346
|
+
else if (event.key === "Escape") {
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
cancelActiveAnnotation();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function save() {
|
|
352
|
+
if (!image || !capture || saving)
|
|
353
|
+
return;
|
|
354
|
+
const committedAnnotations = commitActiveAnnotation();
|
|
355
|
+
if (committedAnnotations === null)
|
|
356
|
+
return;
|
|
357
|
+
if (strokes.length === 0 && committedAnnotations.length === 0 && description.trim() === "") {
|
|
358
|
+
setError("Add a mark or a short description before saving the sketch.");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
setSaving(true);
|
|
362
|
+
setError(null);
|
|
363
|
+
try {
|
|
364
|
+
const rendered = await renderAnnotatedPng(image, imageWidth, imageHeight, strokes, committedAnnotations);
|
|
365
|
+
await onSave({
|
|
366
|
+
...(sketchDocument ? { id: sketchDocument.id } : {}),
|
|
367
|
+
capture: {
|
|
368
|
+
...capture,
|
|
369
|
+
imageWidth: rendered.width,
|
|
370
|
+
imageHeight: rendered.height,
|
|
371
|
+
},
|
|
372
|
+
description: description.trim(),
|
|
373
|
+
strokes,
|
|
374
|
+
annotations: committedAnnotations,
|
|
375
|
+
imageWidth: rendered.width,
|
|
376
|
+
imageHeight: rendered.height,
|
|
377
|
+
originalImage: image,
|
|
378
|
+
annotatedImage: rendered.blob,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
catch (saveError) {
|
|
382
|
+
setError(saveError instanceof Error ? saveError.message : "The sketch could not be saved.");
|
|
383
|
+
setSaving(false);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const draftStroke = draftPoints.length > 0
|
|
387
|
+
? {
|
|
388
|
+
id: "draft",
|
|
389
|
+
kind: tool === "rectangle" ? "rectangle" : "freehand",
|
|
390
|
+
width: SKETCH_STROKE_WIDTH,
|
|
391
|
+
color: SKETCH_STROKE_COLOR,
|
|
392
|
+
outlineColor: SKETCH_STROKE_OUTLINE,
|
|
393
|
+
points: tool === "rectangle" && draftPoints.length > 1
|
|
394
|
+
? rectanglePoints(draftPoints[0], draftPoints.at(-1))
|
|
395
|
+
: draftPoints.map((point) => ({ ...point })),
|
|
396
|
+
}
|
|
397
|
+
: null;
|
|
398
|
+
if (!open || !image || !capture)
|
|
399
|
+
return null;
|
|
400
|
+
return (_jsx(Dialog.Root, { open: open, modal: false, disablePointerDismissal: true, onOpenChange: (nextOpen) => { if (!nextOpen && !saving)
|
|
401
|
+
onCancel(); }, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "sketch__backdrop sketch__editor-backdrop", "data-test": "sketch-editor-backdrop" }), _jsxs(Dialog.Popup, { className: "sketch__popup", "data-test": "sketch-editor", initialFocus: false, onKeyDown: onEditorKeyDown, children: [_jsx(Dialog.Title, { className: "sketch__title sketch__sr-only", children: "Annotate viewport" }), _jsx(Dialog.Description, { className: "sketch__description sketch__sr-only", children: "Draw directly over the captured UI. Select Annotate to label an area with a numbered note." }), _jsx("div", { className: "sketch__canvas-frame", children: _jsxs("div", { className: "sketch__image-wrap", children: [imageUrl ? _jsx("img", { className: "sketch__image", src: imageUrl, alt: "Captured viewport", draggable: false }) : null, _jsx("svg", { ref: svgRef, className: `sketch__canvas sketch__canvas--${tool}`, width: imageWidth, height: imageHeight, viewBox: `0 0 ${imageWidth} ${imageHeight}`, preserveAspectRatio: "none", pointerEvents: "all", tabIndex: 0, "aria-label": "Sketch over the captured viewport", onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: finishPointer, onPointerCancel: finishPointer, children: _jsx(SketchSvgLayer, { strokes: draftStroke ? [...strokes, draftStroke] : strokes, annotations: annotations }) }), activeAnnotation ? (_jsxs("div", { className: "sketch__annotation-editor", role: "dialog", "aria-label": `Annotation ${activeAnnotation.number}`, style: annotationEditorStyle(activeAnnotation), children: [_jsxs("label", { className: "sketch__sr-only", htmlFor: "sketch-editor-annotation-description", children: ["Annotation ", activeAnnotation.number, " description"] }), _jsx("input", { ref: annotationInputRef, id: "sketch-editor-annotation-description", value: annotationDraft, maxLength: SKETCH_LIMITS.description, placeholder: "Describe this area\u2026", autoComplete: "off", "data-test": "sketch-editor-annotation-description", onChange: (event) => setAnnotationDraft(event.target.value), onKeyDown: onAnnotationKeyDown }), _jsx(IconButton, { label: "Cancel annotation", variant: "quiet", size: "compact", "data-test": "sketch-editor-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-editor-annotation-save", onClick: () => { commitActiveAnnotation(); }, children: [_jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" }), "Add"] })] })) : null] }) }), _jsxs("div", { className: "sketch__floating-panel", children: [_jsxs("div", { className: "sketch__toolbar", role: "toolbar", "aria-label": "Sketch tools", children: [_jsx(IconButton, { label: "Move", title: "Move", variant: "quiet", "aria-pressed": tool === "move", "data-active": tool === "move", "data-test": "sketch-move", onClick: () => selectTool("move"), disabled: saving, children: _jsx(IconArrowsMove, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Pen", title: "Pen", variant: "quiet", "aria-pressed": tool === "pen", "data-active": tool === "pen", "data-test": "sketch-pen", onClick: () => selectTool("pen"), disabled: saving, children: _jsx(IconPencil, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Rectangle", title: "Rectangle", variant: "quiet", "aria-pressed": tool === "rectangle", "data-active": tool === "rectangle", "data-test": "sketch-rectangle", onClick: () => selectTool("rectangle"), disabled: saving, children: _jsx(IconRectangle, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Annotate", title: "Annotate", variant: "quiet", "aria-pressed": tool === "annotate", "data-active": tool === "annotate", "data-test": "sketch-annotate", onClick: () => selectTool("annotate"), disabled: saving, children: _jsx(IconMessageCirclePlus, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }), _jsx("span", { className: "sketch__toolbar-divider", "aria-hidden": "true" }), _jsx(Button, { variant: "quiet", type: "button", "data-test": "sketch-cancel", onClick: onCancel, disabled: saving, children: "Cancel" }), _jsxs(Button, { variant: "primary", type: "button", "data-test": "sketch-done", onClick: () => void save(), disabled: saving || (strokes.length === 0 && annotations.length === 0 && description.trim() === ""), children: [_jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" }), saving ? "Saving…" : "Done"] })] }), error ? _jsx(StatusCallout, { tone: "danger", "data-test": "sketch-editor-error", children: error }) : null] })] })] }) }));
|
|
402
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconPencil } from "@tabler/icons-react";
|
|
3
|
+
import { Button } from "../ui/Button.js";
|
|
4
|
+
import { isInlineTextEditingActive } from "../inline-text/inlineTextEditor.js";
|
|
5
|
+
import { useSketchInteractionActive } from "./interaction.js";
|
|
6
|
+
import { startSketchCapture } from "./SketchWorkspace.js";
|
|
7
|
+
export function SketchEntryButton({ hostElement }) {
|
|
8
|
+
const active = useSketchInteractionActive();
|
|
9
|
+
const disabled = active || isInlineTextEditingActive();
|
|
10
|
+
return (_jsxs(Button, { variant: "quiet", size: "compact", className: "panel__sketch-button", type: "button", "data-test": "sketch-entry", "aria-label": "Sketch viewport", title: "Sketch viewport", disabled: disabled || !hostElement, onClick: () => startSketchCapture(hostElement), children: [_jsx(IconPencil, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), _jsx("span", { className: "panel__sketch-label", children: "Sketch" })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { SketchDocument, SketchPoint } from "./model.js";
|
|
3
|
+
interface SketchFrameOverlayProps {
|
|
4
|
+
readonly iframe: HTMLIFrameElement | null;
|
|
5
|
+
readonly cardUrl: string;
|
|
6
|
+
readonly ready: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function sketchRouteKey(url: string): string | null;
|
|
9
|
+
export declare function projectSketchPoint(point: SketchPoint, document: SketchDocument, frameWindow: Window, width: number, height: number): SketchPoint;
|
|
10
|
+
export declare function SketchFrameOverlay({ iframe, cardUrl, ready }: SketchFrameOverlayProps): ReactElement | null;
|
|
11
|
+
export {};
|