nudge-ui 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -46
- package/dist/client.mjs +260 -61
- package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
- package/dist/css/value-semantics/colorSemantics.js +17 -0
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
- package/dist/hosts/astro/clientTransport.js +38 -32
- package/dist/hosts/astro/integration.js +5 -1
- package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
- package/dist/hosts/astro/markdownIdentity.js +81 -0
- package/dist/hosts/next/manifest.js +1 -1
- package/dist/hosts/next/sidecar.js +5 -1
- package/dist/hosts/static/manifest.js +1 -1
- package/dist/hosts/static/server.js +3 -0
- package/dist/hosts/vite/vite.d.ts +3 -0
- package/dist/hosts/vite/vite.js +24 -4
- package/dist/inspector/agent/McpConnectionDialog.js +13 -19
- package/dist/inspector/agent/activity.d.ts +18 -0
- package/dist/inspector/agent/activity.js +70 -0
- package/dist/inspector/agent/client.d.ts +2 -0
- package/dist/inspector/agent/client.js +29 -1
- package/dist/inspector/agent/connectionStatus.js +1 -1
- package/dist/inspector/agent/httpTransport.js +9 -2
- package/dist/inspector/agent/protocol.d.ts +6 -3
- package/dist/inspector/agent/protocol.js +1 -1
- package/dist/inspector/agent/verification.d.ts +15 -7
- package/dist/inspector/agent/verification.js +113 -41
- package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
- package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
- package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
- package/dist/inspector/canvas/CanvasCard.js +135 -38
- package/dist/inspector/canvas/CanvasElementOverlay.js +166 -16
- package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
- package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
- package/dist/inspector/canvas/CanvasToolbar.js +8 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +270 -122
- package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
- package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
- package/dist/inspector/canvas/agentPresentation.js +64 -52
- package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
- package/dist/inspector/canvas/canvasGestures.js +1 -1
- package/dist/inspector/canvas/canvasStore.d.ts +46 -82
- package/dist/inspector/canvas/canvasStore.js +251 -284
- package/dist/inspector/canvas/frameContent.d.ts +21 -0
- package/dist/inspector/canvas/frameContent.js +44 -0
- package/dist/inspector/canvas/frameProtocol.d.ts +21 -4
- package/dist/inspector/canvas/frameProtocol.js +3 -3
- package/dist/inspector/canvas/frameScrollbarStyles.d.ts +3 -0
- package/dist/inspector/canvas/frameScrollbarStyles.js +21 -0
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +4 -4
- package/dist/inspector/canvas/layout.d.ts +52 -0
- package/dist/inspector/canvas/layout.js +106 -0
- package/dist/inspector/canvas/projection.d.ts +15 -5
- package/dist/inspector/canvas/projection.js +69 -13
- package/dist/inspector/canvas/rendererBootstrap.js +32 -4
- package/dist/inspector/canvas/rendererElementSelector.js +47 -6
- package/dist/inspector/canvas/rendererStylesheet.js +2 -2
- package/dist/inspector/canvas/resizeHandleScale.js +1 -1
- package/dist/inspector/canvas/sessionStore.d.ts +11 -26
- package/dist/inspector/canvas/sessionStore.js +43 -183
- package/dist/inspector/canvas/staleChangeDetector.js +9 -3
- package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
- package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
- package/dist/inspector/canvas/viewStore.d.ts +38 -0
- package/dist/inspector/canvas/viewStore.js +104 -0
- package/dist/inspector/canvas/wheelPan.d.ts +8 -0
- package/dist/inspector/canvas/wheelPan.js +8 -0
- package/dist/inspector/changes/changesLog.d.ts +9 -6
- package/dist/inspector/changes/changesLog.js +48 -32
- package/dist/inspector/changes/codecs.d.ts +1 -0
- package/dist/inspector/changes/codecs.js +10 -1
- package/dist/inspector/changes/draftChanges.d.ts +58 -0
- package/dist/inspector/changes/draftChanges.js +304 -0
- package/dist/inspector/changes/editModel.d.ts +10 -0
- package/dist/inspector/changes/editModel.js +9 -2
- package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
- package/dist/inspector/changes/previewDiagnostics.js +11 -25
- package/dist/inspector/changes/types.d.ts +7 -0
- package/dist/inspector/clientManifest.d.ts +1 -1
- package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
- package/dist/inspector/comments/CommentsOverlay.js +250 -0
- package/dist/inspector/comments/element.d.ts +6 -0
- package/dist/inspector/comments/element.js +54 -0
- package/dist/inspector/comments/store.d.ts +29 -0
- package/dist/inspector/comments/store.js +121 -0
- package/dist/inspector/comments/verification.d.ts +5 -0
- package/dist/inspector/comments/verification.js +52 -0
- package/dist/inspector/componentSemantics/adapterRegistry.d.ts +6 -1
- package/dist/inspector/componentSemantics/adapterRegistry.js +23 -1
- package/dist/inspector/componentSemantics/reactRuntime.d.ts +2 -1
- package/dist/inspector/componentSemantics/reactRuntime.js +38 -0
- package/dist/inspector/componentSemantics/runtimeBridge.d.ts +4 -2
- package/dist/inspector/componentSemantics/runtimeBridge.js +17 -0
- package/dist/inspector/componentSemantics/types.d.ts +11 -0
- package/dist/inspector/drafts/model.d.ts +35 -0
- package/dist/inspector/drafts/model.js +45 -0
- package/dist/inspector/drafts/store.d.ts +28 -0
- package/dist/inspector/drafts/store.js +231 -0
- package/dist/inspector/index.js +20 -27
- package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
- package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
- package/dist/inspector/iterations/capture.d.ts +2 -0
- package/dist/inspector/iterations/capture.js +174 -0
- package/dist/inspector/iterations/client.d.ts +7 -0
- package/dist/inspector/iterations/client.js +52 -0
- package/dist/inspector/iterations/coordinator.d.ts +15 -0
- package/dist/inspector/iterations/coordinator.js +130 -0
- package/dist/inspector/iterations/prompt.d.ts +3 -0
- package/dist/inspector/iterations/prompt.js +26 -0
- package/dist/inspector/overlay/DomNavigator.js +21 -15
- package/dist/inspector/overlay/interactionStyles.js +3 -1
- package/dist/inspector/overlay/spacingGestures.d.ts +13 -1
- package/dist/inspector/overlay/spacingGestures.js +82 -5
- package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
- package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
- package/dist/inspector/projection/renderedInstance.d.ts +17 -3
- package/dist/inspector/projection/renderedInstance.js +62 -11
- package/dist/inspector/projection/structuralProjection.d.ts +2 -1
- package/dist/inspector/projection/structuralProjection.js +21 -17
- package/dist/inspector/projection/textProjection.d.ts +2 -1
- package/dist/inspector/projection/textProjection.js +64 -15
- package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
- package/dist/inspector/prompt/clipboardHandoff.d.ts +15 -18
- package/dist/inspector/prompt/clipboardHandoff.js +97 -143
- package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
- package/dist/inspector/prompt/generatePrompt.js +22 -17
- package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
- package/dist/inspector/runtime/runtimeConfig.js +24 -0
- package/dist/inspector/selection/sourceSite.d.ts +12 -0
- package/dist/inspector/selection/sourceSite.js +28 -0
- package/dist/inspector/settings/SettingsDialog.js +21 -8
- package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
- package/dist/inspector/shell/AgentStatusButton.js +36 -0
- package/dist/inspector/shell/AppShell.js +4 -2
- package/dist/inspector/shell/ChangesLog.js +46 -26
- package/dist/inspector/shell/CopyPromptButton.js +184 -93
- package/dist/inspector/shell/EmptyState.js +9 -8
- package/dist/inspector/shell/InspectorShell.js +13 -21
- package/dist/inspector/shell/PeekOriginalButton.js +24 -23
- package/dist/inspector/shell/originalPreview.js +5 -5
- package/dist/inspector/shell/shortcuts.d.ts +3 -0
- package/dist/inspector/shell/shortcuts.js +11 -0
- package/dist/inspector/sketch/SketchEditor.js +14 -14
- package/dist/inspector/sketch/SketchEntryButton.js +1 -1
- package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
- package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
- package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
- package/dist/inspector/sketch/SketchOverlay.js +7 -7
- package/dist/inspector/sketch/SketchWorkspace.js +18 -12
- package/dist/inspector/sketch/interaction.d.ts +1 -0
- package/dist/inspector/sketch/interaction.js +8 -4
- package/dist/inspector/sketch/model.d.ts +5 -0
- package/dist/inspector/sketch/model.js +6 -2
- package/dist/inspector/sketch/raster.d.ts +1 -2
- package/dist/inspector/sketch/raster.js +68 -4
- package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
- package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
- package/dist/inspector/styleEditors/BorderEditor.js +49 -78
- package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
- package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
- package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
- package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
- package/dist/inspector/styleEditors/ColorPicker.js +10 -4
- package/dist/inspector/styleEditors/GapField.js +3 -5
- package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
- package/dist/inspector/styleEditors/GridChildSection.js +18 -9
- package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
- package/dist/inspector/styleEditors/GridPicker.js +8 -2
- package/dist/inspector/styleEditors/GridSection.js +1 -1
- package/dist/inspector/styleEditors/GridValueField.js +19 -14
- package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
- package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
- package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
- package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
- package/dist/inspector/styleEditors/LayoutSection.js +32 -11
- package/dist/inspector/styleEditors/SpacingBox.js +7 -5
- package/dist/inspector/styleEditors/Typography.js +11 -3
- package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
- package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
- package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
- package/dist/inspector/styleEditors/gradientValue.js +114 -0
- package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
- package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
- package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
- package/dist/inspector/styleEditors/layoutValue.js +0 -42
- package/dist/inspector/styleEditors/nudgeValue.d.ts +2 -2
- package/dist/inspector/styleEditors/nudgeValue.js +36 -8
- package/dist/inspector/tokens/TokenField.d.ts +7 -0
- package/dist/inspector/tokens/TokenField.js +35 -25
- package/dist/inspector/tokens/TokenLabel.js +5 -14
- package/dist/inspector/tokens/editActions.d.ts +1 -1
- package/dist/inspector/tokens/editActions.js +7 -1
- package/dist/inspector/ui/AtRuleContext.js +4 -6
- package/dist/inspector/ui/Button.d.ts +6 -2
- package/dist/inspector/ui/Button.js +4 -3
- package/dist/inspector/ui/ControlSurface.d.ts +2 -2
- package/dist/inspector/ui/ControlSurface.js +4 -3
- package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
- package/dist/inspector/ui/InlineStyleWarning.js +13 -0
- package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
- package/dist/inspector/ui/InspectorPopover.js +4 -5
- package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
- package/dist/inspector/ui/PopoverListbox.js +8 -7
- package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
- package/dist/inspector/ui/SegmentedControl.js +2 -2
- package/dist/inspector/ui/Select.d.ts +6 -1
- package/dist/inspector/ui/Select.js +7 -11
- package/dist/inspector/ui/SideValuesField.js +1 -1
- package/dist/inspector/ui/Tooltip.d.ts +26 -0
- package/dist/inspector/ui/Tooltip.js +32 -0
- package/dist/inspector/ui/classNames.d.ts +2 -0
- package/dist/inspector/ui/classNames.js +4 -0
- package/dist/inspector/ui/customCursors.d.ts +23 -0
- package/dist/inspector/ui/customCursors.js +122 -0
- package/dist/inspector/ui/dragCursor.d.ts +0 -2
- package/dist/inspector/ui/dragCursor.js +2 -10
- package/dist/inspector/ui/portalContainer.d.ts +2 -0
- package/dist/inspector/ui/portalContainer.js +6 -0
- package/dist/inspector/ui/styles.js +29 -21
- package/dist/inspector/workspace/commands.d.ts +9 -0
- package/dist/inspector/workspace/commands.js +82 -0
- package/dist/inspector/workspace/controller.d.ts +4 -0
- package/dist/inspector/workspace/controller.js +66 -0
- package/dist/inspector/workspace/handoff.d.ts +42 -0
- package/dist/inspector/workspace/handoff.js +115 -0
- package/dist/inspector/workspace/sentItems.d.ts +19 -0
- package/dist/inspector/workspace/sentItems.js +51 -0
- package/dist/inspector/workspace/timeline.d.ts +30 -0
- package/dist/inspector/workspace/timeline.js +108 -0
- package/dist/internal/agent-protocol.d.ts +19 -2
- package/dist/internal/agent-protocol.js +13 -0
- package/dist/nudge-ui.mjs +558 -311
- package/dist/project/artifacts.d.ts +13 -0
- package/dist/project/artifacts.js +253 -0
- package/dist/transport/artifacts.d.ts +8 -0
- package/dist/transport/artifacts.js +9 -0
- package/dist/transport/editor.js +3 -1
- package/dist/transport/routes.d.ts +2 -0
- package/dist/transport/routes.js +2 -0
- package/package.json +7 -3
- package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
- package/dist/inspector/changes/workspaceChanges.js +0 -265
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare function isEditableTarget(target: EventTarget | null): boolean;
|
|
2
2
|
export declare function isEditableEvent(event: KeyboardEvent): boolean;
|
|
3
|
+
export declare function isMacPlatform(): boolean;
|
|
4
|
+
export declare function getPlatformModifierKey(): "⌘" | "Ctrl";
|
|
5
|
+
export declare function getInspectorToggleShortcutLabel(): string;
|
|
3
6
|
export declare const SEND_PROMPT_HOTKEY_EVENT = "nudge-ui-send-prompt-hotkey";
|
|
4
7
|
export declare function isSendPromptShortcut(event: KeyboardEvent): boolean;
|
|
5
8
|
export declare function isInspectorToggleShortcut(event: KeyboardEvent): boolean;
|
|
@@ -14,6 +14,17 @@ function deeplyFocusedElement() {
|
|
|
14
14
|
export function isEditableEvent(event) {
|
|
15
15
|
return event.composedPath().some(isEditableTarget) || isEditableTarget(deeplyFocusedElement());
|
|
16
16
|
}
|
|
17
|
+
export function isMacPlatform() {
|
|
18
|
+
if (typeof navigator === "undefined")
|
|
19
|
+
return false;
|
|
20
|
+
return /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
|
|
21
|
+
}
|
|
22
|
+
export function getPlatformModifierKey() {
|
|
23
|
+
return isMacPlatform() ? "⌘" : "Ctrl";
|
|
24
|
+
}
|
|
25
|
+
export function getInspectorToggleShortcutLabel() {
|
|
26
|
+
return `${getPlatformModifierKey()}\\`;
|
|
27
|
+
}
|
|
17
28
|
export const SEND_PROMPT_HOTKEY_EVENT = "nudge-ui-send-prompt-hotkey";
|
|
18
29
|
export function isSendPromptShortcut(event) {
|
|
19
30
|
if (event.repeat || event.altKey || isEditableEvent(event))
|
|
@@ -5,15 +5,19 @@ import { IconArrowsMove, IconCheck, IconMessageCirclePlus, IconPencil, IconRecta
|
|
|
5
5
|
import { Button } from "../ui/Button.js";
|
|
6
6
|
import { IconButton } from "../ui/IconButton.js";
|
|
7
7
|
import { StatusCallout } from "../ui/StatusCallout.js";
|
|
8
|
+
import { Tooltip } from "../ui/Tooltip.js";
|
|
8
9
|
import { renderAnnotatedPng } from "./raster.js";
|
|
9
10
|
import { SketchSvgLayer } from "./freehand.js";
|
|
10
11
|
import { findSketchElementAt, translateSketchAnnotation, translateSketchStroke, } from "./geometry.js";
|
|
11
12
|
import { SKETCH_LIMITS, SKETCH_ANNOTATION_RADIUS, SKETCH_STROKE_COLOR, SKETCH_STROKE_OUTLINE, SKETCH_STROKE_WIDTH, } from "./model.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
13
|
+
import { DRAG_CURSOR, SKETCH_CURSOR } from "../ui/customCursors.js";
|
|
14
|
+
import { portalContainer } from "../ui/portalContainer.js";
|
|
15
|
+
const SKETCH_TOOLS = [
|
|
16
|
+
{ tool: "move", label: "Move", Icon: IconArrowsMove },
|
|
17
|
+
{ tool: "pen", label: "Pen", Icon: IconPencil },
|
|
18
|
+
{ tool: "rectangle", label: "Rectangle", Icon: IconRectangle },
|
|
19
|
+
{ tool: "annotate", label: "Annotate", Icon: IconMessageCirclePlus },
|
|
20
|
+
];
|
|
17
21
|
function objectUrl(blob) {
|
|
18
22
|
if (!blob || typeof URL === "undefined" || typeof URL.createObjectURL !== "function")
|
|
19
23
|
return null;
|
|
@@ -47,8 +51,8 @@ function rectanglePoints(start, end) {
|
|
|
47
51
|
}
|
|
48
52
|
export function SketchEditor({ open, document: sketchDocument, captured, onCancel, onSave }) {
|
|
49
53
|
const image = captured?.originalImage ?? sketchDocument?.originalImage ?? null;
|
|
50
|
-
const imageWidth = captured?.imageWidth ?? sketchDocument?.imageWidth ?? 0;
|
|
51
|
-
const imageHeight = captured?.imageHeight ?? sketchDocument?.imageHeight ?? 0;
|
|
54
|
+
const imageWidth = captured?.imageWidth ?? sketchDocument?.capture.imageWidth ?? 0;
|
|
55
|
+
const imageHeight = captured?.imageHeight ?? sketchDocument?.capture.imageHeight ?? 0;
|
|
52
56
|
const capture = captured?.capture ?? sketchDocument?.capture ?? null;
|
|
53
57
|
const [description, setDescription] = useState("");
|
|
54
58
|
const [tool, setTool] = useState("pen");
|
|
@@ -361,14 +365,10 @@ export function SketchEditor({ open, document: sketchDocument, captured, onCance
|
|
|
361
365
|
setSaving(true);
|
|
362
366
|
setError(null);
|
|
363
367
|
try {
|
|
364
|
-
const rendered = await renderAnnotatedPng(image, imageWidth, imageHeight, strokes, committedAnnotations);
|
|
368
|
+
const rendered = await renderAnnotatedPng(image, imageWidth, imageHeight, strokes, committedAnnotations, description.trim());
|
|
365
369
|
await onSave({
|
|
366
370
|
...(sketchDocument ? { id: sketchDocument.id } : {}),
|
|
367
|
-
capture
|
|
368
|
-
...capture,
|
|
369
|
-
imageWidth: rendered.width,
|
|
370
|
-
imageHeight: rendered.height,
|
|
371
|
-
},
|
|
371
|
+
capture,
|
|
372
372
|
description: description.trim(),
|
|
373
373
|
strokes,
|
|
374
374
|
annotations: committedAnnotations,
|
|
@@ -398,5 +398,5 @@ export function SketchEditor({ open, document: sketchDocument, captured, onCance
|
|
|
398
398
|
if (!open || !image || !capture)
|
|
399
399
|
return null;
|
|
400
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: [
|
|
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", style: { cursor: tool === "move" ? DRAG_CURSOR : SKETCH_CURSOR }, 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(Tooltip, { children: _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: [SKETCH_TOOLS.map(({ tool: option, label, Icon }) => (_jsx(Tooltip, { stableTrigger: true, children: _jsx(IconButton, { label: label, variant: "quiet", "aria-pressed": tool === option, "data-active": tool === option, "data-test": `sketch-${option}`, onClick: () => selectTool(option), disabled: saving, children: _jsx(Icon, { size: "var(--icon-size-small)", "aria-hidden": "true" }) }) }, option))), _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
402
|
}
|
|
@@ -7,5 +7,5 @@ import { startSketchCapture } from "./SketchWorkspace.js";
|
|
|
7
7
|
export function SketchEntryButton({ hostElement }) {
|
|
8
8
|
const active = useSketchInteractionActive();
|
|
9
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",
|
|
10
|
+
return (_jsxs(Button, { variant: "quiet", size: "compact", className: "panel__sketch-button", type: "button", "data-test": "sketch-entry", "aria-label": "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
11
|
}
|
|
@@ -2,10 +2,12 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import type { SketchDocument, SketchPoint } from "./model.js";
|
|
3
3
|
interface SketchFrameOverlayProps {
|
|
4
4
|
readonly iframe: HTMLIFrameElement | null;
|
|
5
|
+
readonly cardId?: string;
|
|
5
6
|
readonly cardUrl: string;
|
|
7
|
+
readonly artifactId?: string;
|
|
6
8
|
readonly ready: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function sketchRouteKey(url: string): string | null;
|
|
9
11
|
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;
|
|
12
|
+
export declare function SketchFrameOverlay({ cardId, iframe, cardUrl, artifactId, ready }: SketchFrameOverlayProps): ReactElement | null;
|
|
11
13
|
export {};
|
|
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
3
3
|
import { SketchSvgLayer } from "./freehand.js";
|
|
4
4
|
import { useSketchStore } from "./store.js";
|
|
5
5
|
import { getSketchScrollPosition } from "./capture.js";
|
|
6
|
+
import { sketchBelongsToCard, useDrafts } from "../drafts/store.js";
|
|
6
7
|
export function sketchRouteKey(url) {
|
|
7
8
|
try {
|
|
8
9
|
const parsed = new URL(url);
|
|
@@ -27,16 +28,17 @@ function frameRoute(iframe, fallback) {
|
|
|
27
28
|
return sketchRouteKey(fallback);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
function sameFrame(document, iframe, cardUrl) {
|
|
31
|
+
function sameFrame(document, iframe, cardUrl, artifactId) {
|
|
31
32
|
const sketchRoute = sketchRouteKey(document.capture.url);
|
|
32
33
|
if (!sketchRoute)
|
|
33
34
|
return false;
|
|
34
|
-
return sketchRoute === frameRoute(iframe, cardUrl)
|
|
35
|
+
return sketchRoute === frameRoute(iframe, cardUrl)
|
|
36
|
+
|| (artifactId !== undefined && sketchRoute === sketchRouteKey(cardUrl));
|
|
35
37
|
}
|
|
36
38
|
export function projectSketchPoint(point, document, frameWindow, width, height) {
|
|
37
39
|
const capture = document.capture;
|
|
38
|
-
const imageWidth = Math.max(1, document.imageWidth);
|
|
39
|
-
const imageHeight = Math.max(1, document.imageHeight);
|
|
40
|
+
const imageWidth = Math.max(1, document.capture.imageWidth);
|
|
41
|
+
const imageHeight = Math.max(1, document.capture.imageHeight);
|
|
40
42
|
const viewportWidth = Math.max(1, frameWindow.innerWidth || width);
|
|
41
43
|
const viewportHeight = Math.max(1, frameWindow.innerHeight || height);
|
|
42
44
|
const scroll = getSketchScrollPosition(frameWindow);
|
|
@@ -53,7 +55,7 @@ export function projectSketchPoint(point, document, frameWindow, width, height)
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
function visibleStroke(stroke, document, frameWindow, width, height) {
|
|
56
|
-
const captureScale = document.capture.viewportWidth / Math.max(1, document.imageWidth);
|
|
58
|
+
const captureScale = document.capture.viewportWidth / Math.max(1, document.capture.imageWidth);
|
|
57
59
|
const currentScale = width / Math.max(1, frameWindow.innerWidth || width);
|
|
58
60
|
return {
|
|
59
61
|
...stroke,
|
|
@@ -61,12 +63,13 @@ function visibleStroke(stroke, document, frameWindow, width, height) {
|
|
|
61
63
|
points: stroke.points.map((point) => projectSketchPoint(point, document, frameWindow, width, height)),
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
|
-
export function SketchFrameOverlay({ iframe, cardUrl, ready }) {
|
|
66
|
+
export function SketchFrameOverlay({ cardId, iframe, cardUrl, artifactId, ready }) {
|
|
65
67
|
const { items } = useSketchStore();
|
|
68
|
+
useDrafts();
|
|
66
69
|
const [revision, setRevision] = useState(0);
|
|
67
70
|
const documents = useMemo(() => ready && iframe
|
|
68
|
-
? items.map((item) => item.document).filter((document) => sameFrame(document, iframe, cardUrl))
|
|
69
|
-
: [], [cardUrl, iframe, items, ready]);
|
|
71
|
+
? items.map((item) => item.document).filter((document) => sketchBelongsToCard(document, cardId ?? null) && sameFrame(document, iframe, cardUrl, artifactId))
|
|
72
|
+
: [], [artifactId, cardId, cardUrl, iframe, items, ready]);
|
|
70
73
|
useEffect(() => {
|
|
71
74
|
if (!iframe || documents.length === 0)
|
|
72
75
|
return;
|
|
@@ -6,6 +6,9 @@ import { IconButton } from "../ui/IconButton.js";
|
|
|
6
6
|
import { copyImageToClipboard } from "./raster.js";
|
|
7
7
|
import { closeSketchNote } from "./sketchNote.js";
|
|
8
8
|
import { removeSketch, useSketchStore } from "./store.js";
|
|
9
|
+
import { useFocusedCardId, useSelectedCardId } from "../canvas/canvasStore.js";
|
|
10
|
+
import { sketchBelongsToCard, useDrafts } from "../drafts/store.js";
|
|
11
|
+
import { Tooltip } from "../ui/Tooltip.js";
|
|
9
12
|
function SketchThumbnail({ item }) {
|
|
10
13
|
const [url, setUrl] = useState(null);
|
|
11
14
|
const hasMarks = item.document.strokes.length > 0 || (item.document.annotations?.length ?? 0) > 0;
|
|
@@ -22,7 +25,7 @@ function SketchThumbnail({ item }) {
|
|
|
22
25
|
}, [hasMarks, item.document.annotatedImage]);
|
|
23
26
|
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
27
|
}
|
|
25
|
-
function SketchLayer({ item, index }) {
|
|
28
|
+
function SketchLayer({ item, index, multiple, }) {
|
|
26
29
|
const [copied, setCopied] = useState(false);
|
|
27
30
|
const [error, setError] = useState(null);
|
|
28
31
|
const busy = item.status === "dispatching" || item.status === "handing-off";
|
|
@@ -51,13 +54,18 @@ function SketchLayer({ item, index }) {
|
|
|
51
54
|
setError(deleteError instanceof Error ? deleteError.message : "The sketch could not be deleted.");
|
|
52
55
|
}
|
|
53
56
|
}
|
|
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: "
|
|
57
|
+
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: "primary", type: "button", "data-test": "sketch-layer-copy-image", onClick: () => void copyImage(), disabled: busy, children: [_jsxs("span", { className: "sketch-layers__copy-label", children: [copied
|
|
55
58
|
? _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" : `
|
|
59
|
+
: _jsx(IconCopy, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), _jsx("span", { className: "sketch-layers__name", children: copied ? "Copied" : `Copy sketch note${multiple ? ` ${index + 1}` : ""}` })] }), _jsx(SketchThumbnail, { item: item })] }), _jsx(Tooltip, { children: _jsx(IconButton, { className: "sketch-layers__delete", variant: "quiet", size: "compact", label: "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
60
|
}
|
|
58
61
|
export function SketchLayersPanel() {
|
|
59
62
|
const { items } = useSketchStore();
|
|
60
|
-
|
|
63
|
+
useDrafts();
|
|
64
|
+
const selectedCardId = useSelectedCardId();
|
|
65
|
+
const focusedCardId = useFocusedCardId();
|
|
66
|
+
const cardId = selectedCardId ?? focusedCardId;
|
|
67
|
+
const visibleItems = items.filter((item) => sketchBelongsToCard(item.document, cardId));
|
|
68
|
+
if (visibleItems.length === 0)
|
|
61
69
|
return null;
|
|
62
|
-
return (
|
|
70
|
+
return (_jsx("section", { className: "sketch-layers", "data-test": "sketch-layers", "aria-label": "Sketch layers", children: visibleItems.map((item, index) => (_jsx(SketchLayer, { item: item, index: index, multiple: visibleItems.length > 1 }, `${item.document.id}:${item.document.revision}`))) }));
|
|
63
71
|
}
|
|
@@ -10,11 +10,10 @@ import { findSketchElementAt, translateSketchAnnotation, translateSketchStroke,
|
|
|
10
10
|
import { SketchPromptPanel } from "./SketchPromptPanel.js";
|
|
11
11
|
import { SketchSvgLayer } from "./freehand.js";
|
|
12
12
|
import { useInspectorOpen } from "../shell/openStore.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
13
|
+
import { DRAG_CURSOR, SKETCH_CURSOR } from "../ui/customCursors.js";
|
|
14
|
+
import { useTemporaryAppInteraction } from "../canvas/viewStore.js";
|
|
15
|
+
import { Tooltip } from "../ui/Tooltip.js";
|
|
16
|
+
import { portalContainer } from "../ui/portalContainer.js";
|
|
18
17
|
function clamp(value, maximum) {
|
|
19
18
|
return Math.max(0, Math.min(maximum, value));
|
|
20
19
|
}
|
|
@@ -68,6 +67,7 @@ function annotationEditorStyle(annotation, viewport) {
|
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
69
|
export function SketchOverlay({ open, hostElement, initialTool = "pen", onCancel, onDone }) {
|
|
70
|
+
const temporaryAppInteraction = useTemporaryAppInteraction();
|
|
71
71
|
const inspectorOpen = useInspectorOpen();
|
|
72
72
|
const [viewport, setViewport] = useState(() => getSketchViewport(hostElement));
|
|
73
73
|
const [tool, setTool] = useState("pen");
|
|
@@ -411,10 +411,10 @@ export function SketchOverlay({ open, hostElement, initialTool = "pen", onCancel
|
|
|
411
411
|
if (!open)
|
|
412
412
|
return null;
|
|
413
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: {
|
|
414
|
+
onCancel(); }, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { style: { visibility: temporaryAppInteraction ? "hidden" : undefined }, className: "sketch__backdrop sketch__live-backdrop", "data-inspector-open": inspectorOpen ? "true" : "false", "data-test": "sketch-live-backdrop" }), _jsxs(Dialog.Popup, { style: { visibility: temporaryAppInteraction ? "hidden" : undefined }, 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
415
|
left: viewport.offsetX,
|
|
416
416
|
top: viewport.offsetY,
|
|
417
417
|
width: viewport.displayWidth,
|
|
418
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 })] })] }) }));
|
|
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", style: { cursor: tool === "move" ? DRAG_CURSOR : SKETCH_CURSOR }, 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(Tooltip, { children: _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
420
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { assignSketchToDraft } from "../drafts/store.js";
|
|
3
|
+
import { getActiveDraftChanges } from "../changes/draftChanges.js";
|
|
2
4
|
import { useEffect, useState } from "react";
|
|
3
5
|
import { renderAnnotatedPng } from "./raster.js";
|
|
4
6
|
import { closeSketchEditor, completeSketchCapture, beginSketchCapture, cancelSketchInteraction, openSketchEditor, useSketchInteraction } from "./interaction.js";
|
|
@@ -76,22 +78,22 @@ export function SketchWorkspace({ hostElement, projectId }) {
|
|
|
76
78
|
? store.items.find((item) => item.document.id === note.sketchId)?.document ?? null
|
|
77
79
|
: null;
|
|
78
80
|
async function save(input) {
|
|
79
|
-
|
|
81
|
+
const owner = interaction.draftId ?? getActiveDraftChanges().draftId;
|
|
82
|
+
const saved = await saveSketch(input);
|
|
83
|
+
if (!input.id)
|
|
84
|
+
assignSketchToDraft(owner, saved.id);
|
|
80
85
|
closeSketchEditor();
|
|
81
86
|
}
|
|
82
87
|
async function saveLive(draft) {
|
|
83
88
|
if (!hostElement)
|
|
84
89
|
throw new Error("The inspector is no longer available.");
|
|
90
|
+
const owner = interaction.draftId ?? getActiveDraftChanges().draftId;
|
|
85
91
|
const captured = await completeSketchCapture(hostElement);
|
|
86
92
|
const strokes = projectLiveStrokes(draft, captured);
|
|
87
93
|
const annotations = projectLiveAnnotations(draft, captured);
|
|
88
|
-
const rendered = await renderAnnotatedPng(captured.originalImage, captured.imageWidth, captured.imageHeight, strokes, annotations);
|
|
89
|
-
await
|
|
90
|
-
capture:
|
|
91
|
-
...captured.capture,
|
|
92
|
-
imageWidth: rendered.width,
|
|
93
|
-
imageHeight: rendered.height,
|
|
94
|
-
},
|
|
94
|
+
const rendered = await renderAnnotatedPng(captured.originalImage, captured.imageWidth, captured.imageHeight, strokes, annotations, draft.description);
|
|
95
|
+
const saved = await saveSketch({
|
|
96
|
+
capture: captured.capture,
|
|
95
97
|
description: draft.description,
|
|
96
98
|
strokes,
|
|
97
99
|
annotations,
|
|
@@ -100,20 +102,24 @@ export function SketchWorkspace({ hostElement, projectId }) {
|
|
|
100
102
|
originalImage: captured.originalImage,
|
|
101
103
|
annotatedImage: rendered.blob,
|
|
102
104
|
});
|
|
105
|
+
assignSketchToDraft(owner, saved.id);
|
|
106
|
+
closeSketchEditor();
|
|
103
107
|
}
|
|
104
108
|
async function saveNote(description) {
|
|
105
109
|
if (!noteDocument)
|
|
106
110
|
return;
|
|
111
|
+
const nextDescription = description.trim();
|
|
112
|
+
const rendered = await renderAnnotatedPng(noteDocument.originalImage, noteDocument.capture.imageWidth, noteDocument.capture.imageHeight, noteDocument.strokes, noteDocument.annotations ?? [], nextDescription);
|
|
107
113
|
await saveSketch({
|
|
108
114
|
id: noteDocument.id,
|
|
109
115
|
capture: noteDocument.capture,
|
|
110
|
-
description:
|
|
116
|
+
description: nextDescription,
|
|
111
117
|
strokes: noteDocument.strokes,
|
|
112
118
|
annotations: noteDocument.annotations ?? [],
|
|
113
|
-
imageWidth:
|
|
114
|
-
imageHeight:
|
|
119
|
+
imageWidth: rendered.width,
|
|
120
|
+
imageHeight: rendered.height,
|
|
115
121
|
originalImage: noteDocument.originalImage,
|
|
116
|
-
annotatedImage:
|
|
122
|
+
annotatedImage: rendered.blob,
|
|
117
123
|
});
|
|
118
124
|
closeSketchNote();
|
|
119
125
|
}
|
|
@@ -7,6 +7,7 @@ export interface SketchInteractionSnapshot {
|
|
|
7
7
|
readonly editingId: string | null;
|
|
8
8
|
readonly error: string | null;
|
|
9
9
|
readonly initialTool: SketchEntryTool;
|
|
10
|
+
readonly draftId: string | null;
|
|
10
11
|
}
|
|
11
12
|
export declare function getSketchInteractionSnapshot(): SketchInteractionSnapshot;
|
|
12
13
|
export declare function subscribeSketchInteraction(listener: () => void): () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getActiveDraftChanges } from "../changes/draftChanges.js";
|
|
1
2
|
import { useSyncExternalStore } from "react";
|
|
2
3
|
import { captureViewport, } from "./capture.js";
|
|
3
4
|
const EMPTY_SNAPSHOT = {
|
|
@@ -6,6 +7,7 @@ const EMPTY_SNAPSHOT = {
|
|
|
6
7
|
editingId: null,
|
|
7
8
|
error: null,
|
|
8
9
|
initialTool: "pen",
|
|
10
|
+
draftId: null,
|
|
9
11
|
};
|
|
10
12
|
let snapshot = EMPTY_SNAPSHOT;
|
|
11
13
|
let captureController = null;
|
|
@@ -41,21 +43,22 @@ export function useSketchInteractionActive() {
|
|
|
41
43
|
export function beginSketchCapture(initialTool = "pen") {
|
|
42
44
|
captureController?.abort();
|
|
43
45
|
captureController = null;
|
|
44
|
-
publish({ captureState: "sketching", captured: null, editingId: null, error: null, initialTool });
|
|
46
|
+
publish({ captureState: "sketching", captured: null, editingId: null, error: null, initialTool, draftId: getActiveDraftChanges().draftId });
|
|
45
47
|
}
|
|
46
48
|
export function completeSketchCapture(hostElement) {
|
|
47
49
|
captureController?.abort();
|
|
48
50
|
const controller = new AbortController();
|
|
49
51
|
captureController = controller;
|
|
50
52
|
const initialTool = snapshot.initialTool;
|
|
51
|
-
|
|
53
|
+
const draftId = snapshot.draftId;
|
|
54
|
+
publish({ captureState: "capturing", captured: null, editingId: null, error: null, initialTool, draftId });
|
|
52
55
|
const options = { hostElement, signal: controller.signal };
|
|
53
56
|
return captureViewport(options)
|
|
54
57
|
.then((captured) => {
|
|
55
58
|
if (captureController !== controller)
|
|
56
59
|
return captured;
|
|
57
60
|
captureController = null;
|
|
58
|
-
publish({ captureState: "ready", captured, editingId: null, error: null, initialTool });
|
|
61
|
+
publish({ captureState: "ready", captured, editingId: null, error: null, initialTool, draftId });
|
|
59
62
|
return captured;
|
|
60
63
|
})
|
|
61
64
|
.catch((error) => {
|
|
@@ -72,6 +75,7 @@ export function completeSketchCapture(hostElement) {
|
|
|
72
75
|
editingId: null,
|
|
73
76
|
error: error instanceof Error ? error.message : "The viewport could not be captured.",
|
|
74
77
|
initialTool,
|
|
78
|
+
draftId,
|
|
75
79
|
});
|
|
76
80
|
throw error;
|
|
77
81
|
});
|
|
@@ -84,7 +88,7 @@ export function cancelSketchInteraction() {
|
|
|
84
88
|
export function openSketchEditor(sketchId) {
|
|
85
89
|
captureController?.abort();
|
|
86
90
|
captureController = null;
|
|
87
|
-
publish({ captureState: "idle", captured: null, editingId: sketchId, error: null, initialTool: "pen" });
|
|
91
|
+
publish({ captureState: "idle", captured: null, editingId: sketchId, error: null, initialTool: "pen", draftId: null });
|
|
88
92
|
}
|
|
89
93
|
export function closeSketchEditor() {
|
|
90
94
|
if (snapshot.captureState !== "ready" && snapshot.editingId === null)
|
|
@@ -63,6 +63,11 @@ export interface SketchDocument {
|
|
|
63
63
|
readonly strokes: readonly SketchStroke[];
|
|
64
64
|
/** Optional for compatibility with sketches saved before annotations existed. */
|
|
65
65
|
readonly annotations?: readonly SketchAnnotation[];
|
|
66
|
+
/**
|
|
67
|
+
* Size of `annotatedImage`, which can be taller than the capture when a
|
|
68
|
+
* notes panel is appended. `capture.imageWidth`/`imageHeight` size the
|
|
69
|
+
* original screenshot and the stroke coordinate space.
|
|
70
|
+
*/
|
|
66
71
|
readonly imageWidth: number;
|
|
67
72
|
readonly imageHeight: number;
|
|
68
73
|
readonly originalByteSize: number;
|
|
@@ -176,8 +176,12 @@ export function isSketchDocument(value) {
|
|
|
176
176
|
&& document.annotations.length <= SKETCH_LIMITS.annotations
|
|
177
177
|
&& document.annotations.every(isSketchAnnotation)
|
|
178
178
|
&& new Set(document.annotations.map((annotation) => annotation.number)).size === document.annotations.length))
|
|
179
|
-
&&
|
|
180
|
-
&& document.
|
|
179
|
+
&& Number.isSafeInteger(document.imageWidth)
|
|
180
|
+
&& document.imageWidth > 0
|
|
181
|
+
&& document.imageWidth <= SKETCH_LIMITS.imageEdge
|
|
182
|
+
&& Number.isSafeInteger(document.imageHeight)
|
|
183
|
+
&& document.imageHeight > 0
|
|
184
|
+
&& document.imageHeight <= SKETCH_LIMITS.imageEdge
|
|
181
185
|
&& Number.isSafeInteger(document.originalByteSize)
|
|
182
186
|
&& document.originalByteSize > 0
|
|
183
187
|
&& document.originalByteSize <= SKETCH_LIMITS.imageBytes
|
|
@@ -10,8 +10,7 @@ export declare function drawSketchStroke(ctx: CanvasRenderingContext2D, stroke:
|
|
|
10
10
|
export declare function drawSketchStrokes(ctx: CanvasRenderingContext2D, strokes: readonly SketchStroke[], scaleX?: number, scaleY?: number): void;
|
|
11
11
|
export declare function drawSketchAnnotation(ctx: CanvasRenderingContext2D, annotation: Pick<SketchAnnotation, "number" | "point">, scaleX?: number, scaleY?: number): void;
|
|
12
12
|
export declare function drawSketchAnnotations(ctx: CanvasRenderingContext2D, annotations: readonly SketchAnnotation[], scaleX?: number, scaleY?: number): void;
|
|
13
|
-
|
|
14
|
-
export declare function renderAnnotatedPng(originalImage: Blob, imageWidth: number, imageHeight: number, strokes: readonly SketchStroke[], annotations?: readonly SketchAnnotation[]): Promise<{
|
|
13
|
+
export declare function renderAnnotatedPng(originalImage: Blob, imageWidth: number, imageHeight: number, strokes: readonly SketchStroke[], annotations?: readonly SketchAnnotation[], description?: string): Promise<{
|
|
15
14
|
readonly blob: Blob;
|
|
16
15
|
readonly width: number;
|
|
17
16
|
readonly height: number;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { SKETCH_ANNOTATION_RADIUS, SKETCH_LIMITS, SKETCH_STROKE_COLOR, } from "./model.js";
|
|
2
2
|
import { sketchStrokeOutline } from "./freehand.js";
|
|
3
3
|
const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4
|
+
const SKETCH_NOTE_PADDING = 24;
|
|
5
|
+
const SKETCH_NOTE_TITLE_HEIGHT = 34;
|
|
6
|
+
const SKETCH_NOTE_LINE_HEIGHT = 32;
|
|
7
|
+
const SKETCH_NOTE_FONT = "24px system-ui, sans-serif";
|
|
8
|
+
const SKETCH_NOTE_TITLE_FONT = "600 24px system-ui, sans-serif";
|
|
4
9
|
export function readPngDimensions(bytes) {
|
|
5
10
|
if (bytes.length < 24)
|
|
6
11
|
return null;
|
|
@@ -137,8 +142,59 @@ function canvasPng(canvas) {
|
|
|
137
142
|
}, "image/png");
|
|
138
143
|
});
|
|
139
144
|
}
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
function wrapText(ctx, text, maxWidth) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
let line = "";
|
|
148
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
149
|
+
const next = line ? `${line} ${word}` : word;
|
|
150
|
+
if (line && ctx.measureText(next).width > maxWidth) {
|
|
151
|
+
lines.push(line);
|
|
152
|
+
line = word;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
line = next;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (line)
|
|
159
|
+
lines.push(line);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
/** Wraps the description and numbered annotation notes to the panel width. */
|
|
163
|
+
function wrapSketchNotes(ctx, width, description, annotations) {
|
|
164
|
+
const notes = [
|
|
165
|
+
description.trim(),
|
|
166
|
+
...annotations.map((annotation) => {
|
|
167
|
+
const text = annotation.description.trim();
|
|
168
|
+
return text ? `${annotation.number}. ${text}` : "";
|
|
169
|
+
}),
|
|
170
|
+
].filter(Boolean);
|
|
171
|
+
ctx.font = SKETCH_NOTE_FONT;
|
|
172
|
+
const textWidth = Math.max(1, width - SKETCH_NOTE_PADDING * 2);
|
|
173
|
+
return notes.flatMap((note) => wrapText(ctx, note, textWidth));
|
|
174
|
+
}
|
|
175
|
+
function sketchNotePanelHeight(lineCount) {
|
|
176
|
+
if (lineCount === 0)
|
|
177
|
+
return 0;
|
|
178
|
+
return SKETCH_NOTE_PADDING * 2 + SKETCH_NOTE_TITLE_HEIGHT + lineCount * SKETCH_NOTE_LINE_HEIGHT;
|
|
179
|
+
}
|
|
180
|
+
function drawSketchNotes(ctx, width, top, lines) {
|
|
181
|
+
ctx.save();
|
|
182
|
+
ctx.fillStyle = SKETCH_STROKE_COLOR;
|
|
183
|
+
ctx.fillRect(0, top, width, sketchNotePanelHeight(lines.length));
|
|
184
|
+
ctx.fillStyle = "#ffffff";
|
|
185
|
+
ctx.textAlign = "left";
|
|
186
|
+
ctx.textBaseline = "top";
|
|
187
|
+
ctx.font = SKETCH_NOTE_TITLE_FONT;
|
|
188
|
+
ctx.fillText("Sketch notes", SKETCH_NOTE_PADDING, top + SKETCH_NOTE_PADDING);
|
|
189
|
+
ctx.font = SKETCH_NOTE_FONT;
|
|
190
|
+
let y = top + SKETCH_NOTE_PADDING + SKETCH_NOTE_TITLE_HEIGHT;
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
ctx.fillText(line, SKETCH_NOTE_PADDING, y);
|
|
193
|
+
y += SKETCH_NOTE_LINE_HEIGHT;
|
|
194
|
+
}
|
|
195
|
+
ctx.restore();
|
|
196
|
+
}
|
|
197
|
+
export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight, strokes, annotations = [], description = "") {
|
|
142
198
|
await validatePngBlob(originalImage, { width: imageWidth, height: imageHeight });
|
|
143
199
|
if (typeof document === "undefined")
|
|
144
200
|
throw new Error("Sketch rendering requires a browser document.");
|
|
@@ -146,18 +202,26 @@ export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight,
|
|
|
146
202
|
try {
|
|
147
203
|
const canvas = document.createElement("canvas");
|
|
148
204
|
canvas.width = imageWidth;
|
|
149
|
-
canvas.height = imageHeight;
|
|
150
205
|
const context = canvas.getContext("2d");
|
|
151
206
|
if (!context)
|
|
152
207
|
throw new Error("The browser could not create a sketch canvas.");
|
|
208
|
+
const noteLines = wrapSketchNotes(context, imageWidth, description, annotations);
|
|
209
|
+
const outputHeight = imageHeight + sketchNotePanelHeight(noteLines.length);
|
|
210
|
+
if (outputHeight > SKETCH_LIMITS.imageEdge) {
|
|
211
|
+
throw new Error("Sketch notes are too long to fit below the captured image.");
|
|
212
|
+
}
|
|
213
|
+
// Resizing clears the canvas and resets context state; the context stays valid.
|
|
214
|
+
canvas.height = outputHeight;
|
|
153
215
|
context.drawImage(image.source, 0, 0, imageWidth, imageHeight);
|
|
154
216
|
drawSketchStrokes(context, strokes);
|
|
155
217
|
drawSketchAnnotations(context, annotations);
|
|
218
|
+
if (noteLines.length > 0)
|
|
219
|
+
drawSketchNotes(context, imageWidth, imageHeight, noteLines);
|
|
156
220
|
const result = await canvasPng(canvas);
|
|
157
221
|
if (result.size > SKETCH_LIMITS.imageBytes) {
|
|
158
222
|
throw new Error("This sketch is too detailed to save within the 2 MiB image limit.");
|
|
159
223
|
}
|
|
160
|
-
return { blob: result, width: imageWidth, height:
|
|
224
|
+
return { blob: result, width: imageWidth, height: outputHeight };
|
|
161
225
|
}
|
|
162
226
|
finally {
|
|
163
227
|
image.close();
|