nudge-ui 0.2.2 → 0.2.3
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 +252 -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 +11 -1
- package/dist/inspector/agent/verification.js +53 -25
- 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 +133 -38
- package/dist/inspector/canvas/CanvasElementOverlay.js +152 -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 +225 -57
- 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/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 +6 -5
- package/dist/inspector/changes/changesLog.js +40 -32
- package/dist/inspector/changes/draftChanges.d.ts +56 -0
- package/dist/inspector/changes/draftChanges.js +300 -0
- package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
- package/dist/inspector/changes/previewDiagnostics.js +11 -25
- 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 +47 -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/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 +173 -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 +2 -1
- package/dist/inspector/projection/renderedInstance.js +9 -5
- package/dist/inspector/projection/structuralProjection.d.ts +2 -1
- package/dist/inspector/projection/structuralProjection.js +19 -15
- package/dist/inspector/projection/textProjection.d.ts +2 -1
- package/dist/inspector/projection/textProjection.js +8 -4
- package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
- package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
- package/dist/inspector/prompt/clipboardHandoff.js +92 -143
- package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
- package/dist/inspector/prompt/generatePrompt.js +17 -16
- package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
- package/dist/inspector/runtime/runtimeConfig.js +24 -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 +138 -89
- 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 +29 -21
- package/dist/inspector/tokens/TokenLabel.js +5 -14
- package/dist/inspector/tokens/editActions.d.ts +1 -1
- package/dist/inspector/tokens/editActions.js +1 -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/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 +6 -2
- package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
- package/dist/inspector/changes/workspaceChanges.js +0 -265
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { IconCheck, IconTrash } from "@tabler/icons-react";
|
|
4
|
+
import { useCanvasMode } from "../canvas/viewStore.js";
|
|
5
|
+
import { getRegisteredFrames } from "../canvas/projection.js";
|
|
6
|
+
import { subscribeCanvasRendererMessages } from "../canvas/rendererMessageRouter.js";
|
|
7
|
+
import { RENDERER_ELEMENT_ID_ATTR } from "../canvas/rendererCidIndex.js";
|
|
8
|
+
import { captureRenderedInstance } from "../projection/renderedInstance.js";
|
|
9
|
+
import { IconButton } from "../ui/IconButton.js";
|
|
10
|
+
import { TextInput } from "../ui/TextInput.js";
|
|
11
|
+
import { commentFingerprint, commentViewport, resolveCommentElement } from "./element.js";
|
|
12
|
+
import { commentRoute, getCommentClearRevision, isCommentToolActive, removeComment, saveComment, subscribeComments, useComments, useCommentToolActive } from "./store.js";
|
|
13
|
+
import { isReadingCommentSource, readCommentSource, reconcileComments } from "./verification.js";
|
|
14
|
+
const styles = ".comment-pin {\n position: fixed;\n z-index: 10;\n display: grid;\n place-items: center;\n width: var(--icon-size-large);\n height: var(--icon-size-large);\n padding: 0;\n border: 2px solid var(--surface-raised-2x);\n border-radius: 50%;\n background: var(--accent);\n box-shadow: var(--shadow-popup);\n cursor: pointer;\n pointer-events: auto;\n}\n\n.comment-editor-backdrop {\n position: fixed;\n inset: 0;\n z-index: 11;\n pointer-events: auto;\n}\n\n.comment-editor {\n position: fixed;\n z-index: 12;\n display: flex;\n align-items: center;\n gap: var(--space-4);\n width: min(384px, calc(100vw - 16px));\n box-sizing: border-box;\n padding: var(--space-8);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.comment-editor .text-input { flex: 1; min-width: 0; padding-inline: var(--space-8); }\n.comment-editor__error { position: absolute; top: 100%; left: 0; padding: var(--space-8); background: var(--surface-raised-2x); color: var(--text-primary); border-radius: var(--radius-control); }\n";
|
|
15
|
+
function sameAnchor(left, right) {
|
|
16
|
+
return left?.left === right?.left && left?.top === right?.top;
|
|
17
|
+
}
|
|
18
|
+
function anchorFor(iframe, element) {
|
|
19
|
+
if (!element.isConnected || iframe.contentDocument !== element.ownerDocument)
|
|
20
|
+
return null;
|
|
21
|
+
// Focus view keeps other frames mounted, but their pins must stay hidden.
|
|
22
|
+
if (getComputedStyle(iframe).visibility !== "visible")
|
|
23
|
+
return null;
|
|
24
|
+
const frame = iframe.getBoundingClientRect();
|
|
25
|
+
const rect = element.getBoundingClientRect();
|
|
26
|
+
const scale = frame.width / (iframe.clientWidth || frame.width);
|
|
27
|
+
const left = frame.left + rect.right * scale;
|
|
28
|
+
const top = frame.top + rect.top * scale;
|
|
29
|
+
if (top < frame.top || top > frame.bottom || left < frame.left || left > frame.right)
|
|
30
|
+
return null;
|
|
31
|
+
return { left, top };
|
|
32
|
+
}
|
|
33
|
+
export function CommentsOverlay() {
|
|
34
|
+
const mode = useCanvasMode();
|
|
35
|
+
const comments = useComments();
|
|
36
|
+
const commentActive = useCommentToolActive();
|
|
37
|
+
const [draft, setDraft] = useState(null);
|
|
38
|
+
const [note, setNote] = useState("");
|
|
39
|
+
const [saving, setSaving] = useState(false);
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [anchors, setAnchors] = useState(new Map());
|
|
42
|
+
const [draftAnchor, setDraftAnchor] = useState(null);
|
|
43
|
+
const draftRef = useRef(draft);
|
|
44
|
+
draftRef.current = draft;
|
|
45
|
+
const bindings = useRef(new Map());
|
|
46
|
+
const observations = useRef(new Map());
|
|
47
|
+
function openDraft(next) {
|
|
48
|
+
setDraft(next);
|
|
49
|
+
setNote(next.existing?.note ?? "");
|
|
50
|
+
setError(null);
|
|
51
|
+
}
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!commentActive)
|
|
54
|
+
setDraft(null);
|
|
55
|
+
}, [commentActive]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let revision = getCommentClearRevision();
|
|
58
|
+
return subscribeComments(() => {
|
|
59
|
+
if (revision === getCommentClearRevision())
|
|
60
|
+
return;
|
|
61
|
+
revision = getCommentClearRevision();
|
|
62
|
+
setDraft(null);
|
|
63
|
+
});
|
|
64
|
+
}, []);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!draft)
|
|
67
|
+
return;
|
|
68
|
+
function dismissOnEscape(event) {
|
|
69
|
+
if (event.key !== "Escape")
|
|
70
|
+
return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
event.stopPropagation();
|
|
73
|
+
setDraft(null);
|
|
74
|
+
}
|
|
75
|
+
window.addEventListener("keydown", dismissOnEscape, true);
|
|
76
|
+
return () => window.removeEventListener("keydown", dismissOnEscape, true);
|
|
77
|
+
}, [draft]);
|
|
78
|
+
useEffect(() => subscribeCanvasRendererMessages(({ iframe, message }) => {
|
|
79
|
+
if (message.type === "element-click" && isCommentToolActive()) {
|
|
80
|
+
const element = iframe.contentDocument?.querySelector(`[${RENDERER_ELEMENT_ID_ATTR}="${message.elementId}"]`);
|
|
81
|
+
if (element && element.getAttribute("data-cid") === message.cid && (element.getAttribute("data-src") ?? "") === message.src) {
|
|
82
|
+
openDraft({ element, iframe });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (message.type === "element-deselect")
|
|
86
|
+
setDraft(null);
|
|
87
|
+
if (message.type === "frame-ready" && iframe.contentDocument) {
|
|
88
|
+
void reconcileComments(iframe.contentDocument).catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
}), []);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (comments.length === 0 && !draft) {
|
|
93
|
+
setAnchors(new Map());
|
|
94
|
+
setDraftAnchor(null);
|
|
95
|
+
bindings.current.clear();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let animationFrame = 0;
|
|
99
|
+
let previousAnchors = null;
|
|
100
|
+
let previousDraftAnchor;
|
|
101
|
+
const refresh = () => {
|
|
102
|
+
const next = new Map();
|
|
103
|
+
const offsets = new Map();
|
|
104
|
+
for (const key of bindings.current.keys()) {
|
|
105
|
+
if (!comments.some((comment) => key.startsWith(`${comment.id}:`)))
|
|
106
|
+
bindings.current.delete(key);
|
|
107
|
+
}
|
|
108
|
+
for (const comment of comments) {
|
|
109
|
+
for (const [cardId, iframe] of getRegisteredFrames()) {
|
|
110
|
+
const doc = iframe.contentDocument;
|
|
111
|
+
if (!doc || commentRoute(doc) !== comment.route)
|
|
112
|
+
continue;
|
|
113
|
+
const key = `${comment.id}:${cardId}`;
|
|
114
|
+
const bound = bindings.current.get(key);
|
|
115
|
+
const element = bound?.iframe === iframe && bound.element.isConnected && bound.element.ownerDocument === doc
|
|
116
|
+
? bound.element : resolveCommentElement(doc, comment.target);
|
|
117
|
+
if (!element)
|
|
118
|
+
continue;
|
|
119
|
+
bindings.current.set(key, { element, iframe });
|
|
120
|
+
const anchor = anchorFor(iframe, element);
|
|
121
|
+
if (anchor) {
|
|
122
|
+
const positionKey = `${cardId}:${anchor.left}:${anchor.top}`;
|
|
123
|
+
const offset = offsets.get(positionKey) ?? 0;
|
|
124
|
+
next.set(key, { ...anchor, top: anchor.top + offset });
|
|
125
|
+
offsets.set(positionKey, offset + 30);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!previousAnchors || previousAnchors.size !== next.size
|
|
130
|
+
|| [...next].some(([key, anchor]) => !sameAnchor(previousAnchors?.get(key), anchor))) {
|
|
131
|
+
previousAnchors = next;
|
|
132
|
+
setAnchors(next);
|
|
133
|
+
}
|
|
134
|
+
const current = draftRef.current;
|
|
135
|
+
const nextDraftAnchor = current ? anchorFor(current.iframe, current.element) : null;
|
|
136
|
+
if (previousDraftAnchor === undefined || !sameAnchor(previousDraftAnchor, nextDraftAnchor)) {
|
|
137
|
+
previousDraftAnchor = nextDraftAnchor;
|
|
138
|
+
setDraftAnchor(nextDraftAnchor);
|
|
139
|
+
}
|
|
140
|
+
if (current && (!current.element.isConnected || current.iframe.contentDocument !== current.element.ownerDocument))
|
|
141
|
+
setDraft(null);
|
|
142
|
+
animationFrame = requestAnimationFrame(refresh);
|
|
143
|
+
};
|
|
144
|
+
refresh();
|
|
145
|
+
return () => cancelAnimationFrame(animationFrame);
|
|
146
|
+
}, [comments, draft]);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!comments.some((comment) => comment.handedOff))
|
|
149
|
+
return;
|
|
150
|
+
const refresh = () => {
|
|
151
|
+
const documents = new Set([...getRegisteredFrames()].map(([, iframe]) => iframe.contentDocument).filter((doc) => doc !== null));
|
|
152
|
+
for (const [doc, observation] of observations.current) {
|
|
153
|
+
if (documents.has(doc))
|
|
154
|
+
continue;
|
|
155
|
+
observation.observer.disconnect();
|
|
156
|
+
if (observation.timer)
|
|
157
|
+
clearTimeout(observation.timer);
|
|
158
|
+
observations.current.delete(doc);
|
|
159
|
+
}
|
|
160
|
+
for (const doc of documents) {
|
|
161
|
+
if (observations.current.has(doc) || !comments.some((item) => item.handedOff && item.route === commentRoute(doc)))
|
|
162
|
+
continue;
|
|
163
|
+
const observation = { observer: new MutationObserver((records) => {
|
|
164
|
+
if (isReadingCommentSource(doc))
|
|
165
|
+
return;
|
|
166
|
+
const sourceChanged = records.some((record) => {
|
|
167
|
+
const element = record.target.nodeType === 1 ? record.target : record.target.parentElement;
|
|
168
|
+
if (element?.closest('[data-nudge-ui], #nudge-ui-root'))
|
|
169
|
+
return false;
|
|
170
|
+
if (record.type === "attributes" && (record.attributeName?.startsWith("data-nudge") || record.attributeName?.startsWith("data-projection") || record.attributeName === "data-renderer-id"))
|
|
171
|
+
return false;
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
if (!sourceChanged)
|
|
175
|
+
return;
|
|
176
|
+
if (observation.timer)
|
|
177
|
+
clearTimeout(observation.timer);
|
|
178
|
+
observation.timer = setTimeout(() => { void reconcileComments(doc).catch(() => undefined); }, 200);
|
|
179
|
+
}), timer: null };
|
|
180
|
+
observation.observer.observe(doc.documentElement, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
181
|
+
observations.current.set(doc, observation);
|
|
182
|
+
void reconcileComments(doc).catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
refresh();
|
|
186
|
+
const timer = setInterval(refresh, 500);
|
|
187
|
+
return () => {
|
|
188
|
+
clearInterval(timer);
|
|
189
|
+
for (const observation of observations.current.values()) {
|
|
190
|
+
observation.observer.disconnect();
|
|
191
|
+
if (observation.timer)
|
|
192
|
+
clearTimeout(observation.timer);
|
|
193
|
+
}
|
|
194
|
+
observations.current.clear();
|
|
195
|
+
};
|
|
196
|
+
}, [comments]);
|
|
197
|
+
async function confirm() {
|
|
198
|
+
if (!draft || saving || !note.trim())
|
|
199
|
+
return;
|
|
200
|
+
const currentDraft = draft;
|
|
201
|
+
const clearRevision = getCommentClearRevision();
|
|
202
|
+
const text = note.trim();
|
|
203
|
+
setSaving(true);
|
|
204
|
+
setError(null);
|
|
205
|
+
try {
|
|
206
|
+
const comment = await readCommentSource(draft.element.ownerDocument, () => {
|
|
207
|
+
if (!draft.element.isConnected)
|
|
208
|
+
return null;
|
|
209
|
+
const target = captureRenderedInstance(draft.element);
|
|
210
|
+
if (!target)
|
|
211
|
+
return null;
|
|
212
|
+
return {
|
|
213
|
+
id: draft.existing?.id ?? crypto.randomUUID(),
|
|
214
|
+
route: commentRoute(draft.element.ownerDocument),
|
|
215
|
+
target,
|
|
216
|
+
tag: draft.element.tagName.toLowerCase(),
|
|
217
|
+
note: text,
|
|
218
|
+
baseline: commentFingerprint(draft.element),
|
|
219
|
+
viewport: commentViewport(draft.element.ownerDocument),
|
|
220
|
+
handedOff: false,
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
if (getCommentClearRevision() !== clearRevision)
|
|
224
|
+
return;
|
|
225
|
+
if (!comment) {
|
|
226
|
+
setError("The element changed. Select it again to save your comment.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
saveComment(comment);
|
|
230
|
+
if (draftRef.current === currentDraft)
|
|
231
|
+
setDraft(null);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
setError("Couldn't save this comment. Try again.");
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
setSaving(false);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (mode !== "canvas")
|
|
241
|
+
return null;
|
|
242
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: styles }), comments.flatMap((comment) => [...anchors].filter(([key]) => key.startsWith(`${comment.id}:`)).map(([key, anchor]) => (_jsx("button", { type: "button", className: "comment-pin", "data-test": "comment-pin", "aria-label": `Comment: ${comment.note}`, title: comment.note, style: anchor, onClick: () => {
|
|
243
|
+
const bound = bindings.current.get(key);
|
|
244
|
+
if (bound)
|
|
245
|
+
openDraft({ ...bound, existing: comment });
|
|
246
|
+
} }, key)))), draft && draftAnchor ? _jsx("div", { className: "comment-editor-backdrop", "aria-hidden": "true", onClick: () => setDraft(null) }) : null, draft && draftAnchor ? _jsxs("form", { className: "comment-editor", "data-test": "comment-editor", role: "dialog", "aria-label": "Element comment", style: { left: Math.max(8, Math.min(draftAnchor.left + 12, window.innerWidth - 400)), top: Math.max(8, Math.min(draftAnchor.top, window.innerHeight - 90)) }, onSubmit: (event) => { event.preventDefault(); void confirm(); }, children: [_jsx(TextInput, { appearance: "embedded", autoFocus: true, "aria-label": "Comment", "data-test": "comment-input", placeholder: "Leave a comment", maxLength: 2000, value: note, disabled: saving, onChange: (event) => setNote(event.target.value) }), _jsx(IconButton, { label: "Confirm comment", variant: "primary", type: "submit", disabled: saving || !note.trim(), "data-test": "comment-confirm", children: _jsx(IconCheck, { size: 18 }) }), draft.existing ? _jsx(IconButton, { label: "Delete comment", variant: "quiet", disabled: saving, onClick: () => {
|
|
247
|
+
removeComment(draft.existing.id);
|
|
248
|
+
setDraft(null);
|
|
249
|
+
}, children: _jsx(IconTrash, { size: 16 }) }) : null, error ? _jsx("span", { className: "comment-editor__error", role: "alert", children: error }) : null] }) : null] });
|
|
250
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RenderedInstanceRef } from "../changes/editModel.js";
|
|
2
|
+
/** Retains a changed target only when its source site still identifies one output. */
|
|
3
|
+
export declare function resolveCommentElement(doc: Document, target: RenderedInstanceRef): HTMLElement | null;
|
|
4
|
+
export declare function commentViewport(doc: Document): string;
|
|
5
|
+
/** Captures rendered content and appearance, excluding inspector identity and transient cursors. */
|
|
6
|
+
export declare function commentFingerprint(element: HTMLElement): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveRenderedInstance } from "../projection/renderedInstance.js";
|
|
2
|
+
import { sourceSiteSelector } from "../selection/sourceSite.js";
|
|
3
|
+
/** Retains a changed target only when its source site still identifies one output. */
|
|
4
|
+
export function resolveCommentElement(doc, target) {
|
|
5
|
+
const exact = resolveRenderedInstance(doc, target);
|
|
6
|
+
if (exact.status === "resolved")
|
|
7
|
+
return exact.element;
|
|
8
|
+
const selector = sourceSiteSelector(target.sourceSite.cid, target.sourceSite.src);
|
|
9
|
+
if (!selector)
|
|
10
|
+
return null;
|
|
11
|
+
const candidates = Array.from(doc.querySelectorAll(selector));
|
|
12
|
+
if (candidates.length === 1)
|
|
13
|
+
return candidates[0];
|
|
14
|
+
const { props, ariaLabel } = target.locator;
|
|
15
|
+
if (!props && !ariaLabel)
|
|
16
|
+
return null;
|
|
17
|
+
const matching = candidates.filter((element) => element.getAttribute("data-cprops") === props
|
|
18
|
+
&& element.getAttribute("aria-label") === (ariaLabel ?? null));
|
|
19
|
+
return matching.length === 1 ? matching[0] : null;
|
|
20
|
+
}
|
|
21
|
+
export function commentViewport(doc) {
|
|
22
|
+
return `${doc.defaultView?.innerWidth}:${doc.defaultView?.innerHeight}`;
|
|
23
|
+
}
|
|
24
|
+
/** Captures rendered content and appearance, excluding inspector identity and transient cursors. */
|
|
25
|
+
export function commentFingerprint(element) {
|
|
26
|
+
const clone = element.cloneNode(true);
|
|
27
|
+
for (const node of [clone, ...clone.querySelectorAll("*")]) {
|
|
28
|
+
for (const attribute of [...node.attributes]) {
|
|
29
|
+
if (attribute.name.startsWith("data-nudge") || attribute.name.startsWith("data-projection")
|
|
30
|
+
|| ["data-cid", "data-src", "data-cprops", "data-renderer-id"].includes(attribute.name))
|
|
31
|
+
node.removeAttribute(attribute.name);
|
|
32
|
+
}
|
|
33
|
+
if (node instanceof element.ownerDocument.defaultView.HTMLElement) {
|
|
34
|
+
node.style.removeProperty("cursor");
|
|
35
|
+
if (node.getAttribute("style") === "")
|
|
36
|
+
node.removeAttribute("style");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
40
|
+
const properties = ["color", "background-color", "font-family", "font-size", "font-weight", "line-height",
|
|
41
|
+
"padding", "margin", "gap", "display", "border", "border-radius", "box-shadow", "opacity", "width", "height"];
|
|
42
|
+
const value = JSON.stringify([clone.outerHTML, properties.map((property) => style?.getPropertyValue(property) ?? "")]);
|
|
43
|
+
let hash = 2166136261;
|
|
44
|
+
for (let index = 0; index < value.length; index += 1)
|
|
45
|
+
hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
|
|
46
|
+
return (hash >>> 0).toString(16);
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type RenderedInstanceRef } from "../changes/editModel.js";
|
|
2
|
+
import { type DraftTarget } from "../drafts/model.js";
|
|
3
|
+
export interface ElementComment {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly route: string;
|
|
6
|
+
readonly target: RenderedInstanceRef;
|
|
7
|
+
readonly tag: string;
|
|
8
|
+
readonly note: string;
|
|
9
|
+
readonly baseline: string;
|
|
10
|
+
readonly viewport: string;
|
|
11
|
+
readonly handedOff: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function getComments(): readonly ElementComment[];
|
|
14
|
+
export declare function loadComments(): void;
|
|
15
|
+
export declare function subscribeComments(listener: () => void): () => void;
|
|
16
|
+
export declare function useComments(): readonly ElementComment[];
|
|
17
|
+
export declare function getCommentsForTarget(target: DraftTarget): readonly ElementComment[];
|
|
18
|
+
export declare function useFrameComments(): readonly ElementComment[];
|
|
19
|
+
export declare function saveComment(comment: ElementComment): void;
|
|
20
|
+
export declare function removeComment(id: string): void;
|
|
21
|
+
export declare function getCommentClearRevision(): number;
|
|
22
|
+
/** Clears saved notes and invalidates any open or saving comment draft. */
|
|
23
|
+
export declare function clearComments(target?: DraftTarget): void;
|
|
24
|
+
/** Arms only the exact notes successfully copied or accepted by an agent. */
|
|
25
|
+
export declare function markCommentsHandedOff(sent: readonly ElementComment[]): void;
|
|
26
|
+
export declare function setCommentToolActive(active: boolean): void;
|
|
27
|
+
export declare function isCommentToolActive(): boolean;
|
|
28
|
+
export declare function useCommentToolActive(): boolean;
|
|
29
|
+
export declare function commentRoute(doc: Document): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { isRenderedInstanceRef } from "../changes/editModel.js";
|
|
3
|
+
import { useCanvasCards, useFocusedCardId, useSelectedCardId } from "../canvas/canvasStore.js";
|
|
4
|
+
import { canWriteWorkspace } from "../canvas/workspaceLease.js";
|
|
5
|
+
import { contentEditTarget } from "../canvas/frameContent.js";
|
|
6
|
+
import { documentTarget, targetKey } from "../drafts/model.js";
|
|
7
|
+
import { getNudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
|
|
8
|
+
let comments = [];
|
|
9
|
+
let loadedKey = null;
|
|
10
|
+
let loadedDemo = false;
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
const toolListeners = new Set();
|
|
13
|
+
let commentToolActive = false;
|
|
14
|
+
let clearRevision = 0;
|
|
15
|
+
function storageKey() {
|
|
16
|
+
return `nudge-ui:${getNudgeUiRuntimeConfig().projectId}:comments:v1`;
|
|
17
|
+
}
|
|
18
|
+
function isComment(value) {
|
|
19
|
+
if (!value || typeof value !== "object")
|
|
20
|
+
return false;
|
|
21
|
+
const item = value;
|
|
22
|
+
return ["id", "route", "tag", "note", "baseline", "viewport"].every((key) => typeof item[key] === "string")
|
|
23
|
+
&& typeof item.handedOff === "boolean" && isRenderedInstanceRef(item.target);
|
|
24
|
+
}
|
|
25
|
+
export function getComments() {
|
|
26
|
+
const key = storageKey();
|
|
27
|
+
const demo = getNudgeUiRuntimeConfig().demo === true;
|
|
28
|
+
if (loadedKey !== key || loadedDemo !== demo) {
|
|
29
|
+
loadedKey = key;
|
|
30
|
+
loadedDemo = demo;
|
|
31
|
+
try {
|
|
32
|
+
const saved = demo ? [] : JSON.parse(localStorage.getItem(key) ?? "[]");
|
|
33
|
+
comments = Array.isArray(saved) ? saved.filter(isComment) : [];
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
comments = [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return comments;
|
|
40
|
+
}
|
|
41
|
+
export function loadComments() {
|
|
42
|
+
loadedKey = null;
|
|
43
|
+
clearRevision += 1;
|
|
44
|
+
getComments();
|
|
45
|
+
for (const listener of listeners)
|
|
46
|
+
listener();
|
|
47
|
+
}
|
|
48
|
+
function publish(next) {
|
|
49
|
+
if (!canWriteWorkspace())
|
|
50
|
+
return;
|
|
51
|
+
comments = next;
|
|
52
|
+
if (getNudgeUiRuntimeConfig().demo !== true) {
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(storageKey(), JSON.stringify(next));
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
for (const listener of listeners)
|
|
59
|
+
listener();
|
|
60
|
+
}
|
|
61
|
+
export function subscribeComments(listener) {
|
|
62
|
+
listeners.add(listener);
|
|
63
|
+
return () => listeners.delete(listener);
|
|
64
|
+
}
|
|
65
|
+
export function useComments() {
|
|
66
|
+
return useSyncExternalStore(subscribeComments, getComments, getComments);
|
|
67
|
+
}
|
|
68
|
+
export function getCommentsForTarget(target) {
|
|
69
|
+
return getComments().filter((comment) => targetKey(documentTarget(comment.route)) === targetKey(target));
|
|
70
|
+
}
|
|
71
|
+
export function useFrameComments() {
|
|
72
|
+
useComments();
|
|
73
|
+
const cards = useCanvasCards();
|
|
74
|
+
const selected = useSelectedCardId();
|
|
75
|
+
const focused = useFocusedCardId();
|
|
76
|
+
const card = cards.find((card) => card.id === selected) ?? cards.find((card) => card.id === focused) ?? cards[0];
|
|
77
|
+
return getCommentsForTarget(card ? contentEditTarget(card.content) : documentTarget(window.location.href));
|
|
78
|
+
}
|
|
79
|
+
export function saveComment(comment) {
|
|
80
|
+
const current = getComments();
|
|
81
|
+
publish([...current.filter((item) => item.id !== comment.id), comment]);
|
|
82
|
+
}
|
|
83
|
+
export function removeComment(id) {
|
|
84
|
+
publish(getComments().filter((item) => item.id !== id));
|
|
85
|
+
}
|
|
86
|
+
export function getCommentClearRevision() { return clearRevision; }
|
|
87
|
+
/** Clears saved notes and invalidates any open or saving comment draft. */
|
|
88
|
+
export function clearComments(target) {
|
|
89
|
+
if (!canWriteWorkspace())
|
|
90
|
+
return;
|
|
91
|
+
getComments();
|
|
92
|
+
clearRevision += 1;
|
|
93
|
+
const cleared = new Set((target ? getCommentsForTarget(target) : getComments()).map((comment) => comment.id));
|
|
94
|
+
publish(getComments().filter((comment) => !cleared.has(comment.id)));
|
|
95
|
+
}
|
|
96
|
+
/** Arms only the exact notes successfully copied or accepted by an agent. */
|
|
97
|
+
export function markCommentsHandedOff(sent) {
|
|
98
|
+
publish(getComments().map((item) => sent.some((candidate) => candidate.id === item.id && candidate.note === item.note)
|
|
99
|
+
? { ...item, handedOff: true } : item));
|
|
100
|
+
}
|
|
101
|
+
export function setCommentToolActive(active) {
|
|
102
|
+
if (commentToolActive === active)
|
|
103
|
+
return;
|
|
104
|
+
commentToolActive = active;
|
|
105
|
+
for (const listener of toolListeners)
|
|
106
|
+
listener();
|
|
107
|
+
}
|
|
108
|
+
export function isCommentToolActive() { return commentToolActive; }
|
|
109
|
+
export function useCommentToolActive() {
|
|
110
|
+
return useSyncExternalStore((listener) => {
|
|
111
|
+
toolListeners.add(listener);
|
|
112
|
+
return () => toolListeners.delete(listener);
|
|
113
|
+
}, isCommentToolActive, isCommentToolActive);
|
|
114
|
+
}
|
|
115
|
+
export function commentRoute(doc) {
|
|
116
|
+
const url = new URL(doc.location.href);
|
|
117
|
+
url.hash = "";
|
|
118
|
+
url.searchParams.delete("nudge-ui");
|
|
119
|
+
url.searchParams.sort();
|
|
120
|
+
return url.href;
|
|
121
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isReadingCommentSource(doc: Document): boolean;
|
|
2
|
+
/** Serializes clean reads and restores the latest previews even if the read fails. */
|
|
3
|
+
export declare function readCommentSource<T>(doc: Document, read: () => T): Promise<T | null>;
|
|
4
|
+
/** Changed, uniquely located elements resolve after handoff; missing and ambiguous targets remain. */
|
|
5
|
+
export declare function reconcileComments(doc: Document): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getDraftChanges } from "../changes/draftChanges.js";
|
|
2
|
+
import { documentTarget, targetKey } from "../drafts/model.js";
|
|
3
|
+
import { isCanvasProjectionRevisionCurrent, projectDraftToDocument } from "../canvas/projection.js";
|
|
4
|
+
import { commentFingerprint, commentViewport, resolveCommentElement } from "./element.js";
|
|
5
|
+
import { commentRoute, getCommentClearRevision, getComments, removeComment } from "./store.js";
|
|
6
|
+
const documentReads = new WeakMap();
|
|
7
|
+
const readingDocuments = new WeakSet();
|
|
8
|
+
export function isReadingCommentSource(doc) { return readingDocuments.has(doc); }
|
|
9
|
+
/** Serializes clean reads and restores the latest previews even if the read fails. */
|
|
10
|
+
export function readCommentSource(doc, read) {
|
|
11
|
+
const previous = documentReads.get(doc) ?? Promise.resolve();
|
|
12
|
+
const operation = previous.catch(() => undefined).then(async () => {
|
|
13
|
+
readingDocuments.add(doc);
|
|
14
|
+
try {
|
|
15
|
+
const revision = await projectDraftToDocument(doc, { changes: [], structuralChanges: [] });
|
|
16
|
+
if (revision === null || !isCanvasProjectionRevisionCurrent(doc, revision))
|
|
17
|
+
return null;
|
|
18
|
+
return read();
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
try {
|
|
22
|
+
await projectDraftToDocument(doc, getDraftChanges(targetKey(documentTarget(doc.location.href))));
|
|
23
|
+
// Deliver projection mutation records before accepting another source signal.
|
|
24
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
readingDocuments.delete(doc);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
documentReads.set(doc, operation);
|
|
32
|
+
return operation;
|
|
33
|
+
}
|
|
34
|
+
/** Changed, uniquely located elements resolve after handoff; missing and ambiguous targets remain. */
|
|
35
|
+
export async function reconcileComments(doc) {
|
|
36
|
+
const revision = getCommentClearRevision();
|
|
37
|
+
const sent = getComments().filter((item) => item.handedOff && item.route === commentRoute(doc));
|
|
38
|
+
if (sent.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
const resolved = await readCommentSource(doc, () => sent.filter((item) => {
|
|
41
|
+
if (item.viewport !== commentViewport(doc))
|
|
42
|
+
return false;
|
|
43
|
+
const element = resolveCommentElement(doc, item.target);
|
|
44
|
+
return element !== null && commentFingerprint(element) !== item.baseline;
|
|
45
|
+
}));
|
|
46
|
+
if (revision !== getCommentClearRevision())
|
|
47
|
+
return;
|
|
48
|
+
for (const item of resolved ?? []) {
|
|
49
|
+
if (getComments().some((current) => current.id === item.id && current.note === item.note && current.handedOff))
|
|
50
|
+
removeComment(item.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DraftContents } from "../changes/draftChanges.js";
|
|
2
|
+
export type DraftTarget = {
|
|
3
|
+
readonly kind: "application";
|
|
4
|
+
readonly route: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "html";
|
|
7
|
+
readonly artifactId: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
};
|
|
10
|
+
/** A draft's ID is its target key, so every frame showing the same page or iteration edits one draft. */
|
|
11
|
+
export interface Draft {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly target: DraftTarget;
|
|
14
|
+
readonly revision: number;
|
|
15
|
+
readonly contents: DraftContents;
|
|
16
|
+
readonly sketchIds: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface DraftSnapshot {
|
|
19
|
+
readonly projectId: string;
|
|
20
|
+
readonly drafts: readonly Draft[];
|
|
21
|
+
}
|
|
22
|
+
export declare function applicationTarget(url?: string): {
|
|
23
|
+
kind: "application";
|
|
24
|
+
route: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function htmlTarget(artifactId: string): {
|
|
27
|
+
kind: "html";
|
|
28
|
+
artifactId: string;
|
|
29
|
+
path: string;
|
|
30
|
+
};
|
|
31
|
+
/** Maps a captured document URL to the draft that owns it: an iteration preview or an application page. */
|
|
32
|
+
export declare function documentTarget(url: string): DraftTarget;
|
|
33
|
+
export declare function targetKey(target: DraftTarget): string;
|
|
34
|
+
export declare function targetFromKey(key: string): DraftTarget | null;
|
|
35
|
+
export declare function isDraftTarget(value: unknown): value is DraftTarget;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalizeUrl, normalizedUrlKey } from "../canvas/normalizeUrl.js";
|
|
2
|
+
const ARTIFACT_PATH = /^\/__nudge_ui__\/artifacts\/([a-f0-9-]{36})\//i;
|
|
3
|
+
export function applicationTarget(url = window.location.href) {
|
|
4
|
+
const page = new URL(url, window.location.href);
|
|
5
|
+
page.searchParams.delete("nudge-ui");
|
|
6
|
+
page.searchParams.sort();
|
|
7
|
+
const normalized = normalizeUrl(page.href);
|
|
8
|
+
return { kind: "application", route: normalizedUrlKey(normalized) };
|
|
9
|
+
}
|
|
10
|
+
export function htmlTarget(artifactId) {
|
|
11
|
+
return { kind: "html", artifactId, path: `.nudge/artifacts/${artifactId}/document.html` };
|
|
12
|
+
}
|
|
13
|
+
/** Maps a captured document URL to the draft that owns it: an iteration preview or an application page. */
|
|
14
|
+
export function documentTarget(url) {
|
|
15
|
+
const artifactId = ARTIFACT_PATH.exec(new URL(url, window.location.href).pathname)?.[1];
|
|
16
|
+
return artifactId ? htmlTarget(artifactId) : applicationTarget(url);
|
|
17
|
+
}
|
|
18
|
+
export function targetKey(target) {
|
|
19
|
+
return target.kind === "application" ? `application:${target.route}` : `html:${target.artifactId}`;
|
|
20
|
+
}
|
|
21
|
+
export function targetFromKey(key) {
|
|
22
|
+
if (key.startsWith("html:"))
|
|
23
|
+
return htmlTarget(key.slice("html:".length));
|
|
24
|
+
if (!key.startsWith("application:"))
|
|
25
|
+
return null;
|
|
26
|
+
const target = { kind: "application", route: key.slice("application:".length) };
|
|
27
|
+
return isDraftTarget(target) ? target : null;
|
|
28
|
+
}
|
|
29
|
+
export function isDraftTarget(value) {
|
|
30
|
+
if (!value || typeof value !== "object")
|
|
31
|
+
return false;
|
|
32
|
+
const target = value;
|
|
33
|
+
if (target.kind === "application") {
|
|
34
|
+
if (typeof target.route !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
return applicationTarget(target.route).route === target.route;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return target.kind === "html" && typeof target.artifactId === "string"
|
|
44
|
+
&& target.path === `.nudge/artifacts/${target.artifactId}/document.html`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FrameContent } from "../canvas/frameContent.js";
|
|
2
|
+
import { type DraftContents } from "../changes/draftChanges.js";
|
|
3
|
+
import type { SketchDocument } from "../sketch/model.js";
|
|
4
|
+
import { type Draft, type DraftSnapshot } from "./model.js";
|
|
5
|
+
export declare function draftIdForContent(content: FrameContent): string;
|
|
6
|
+
export declare function draftIdForCard(cardId: string): string | null;
|
|
7
|
+
export declare function sketchOwner(document: SketchDocument): string | null;
|
|
8
|
+
export declare function getDraft(id: string): Draft | null;
|
|
9
|
+
export declare function getDraftForCard(cardId: string): Draft | null;
|
|
10
|
+
export declare function getDraftContentsForCard(cardId: string): DraftContents | null;
|
|
11
|
+
export declare function persistDrafts(): void;
|
|
12
|
+
/** Replaces every in-memory draft with the stored drafts, so a tab that takes over never writes stale edits. */
|
|
13
|
+
export declare function loadDrafts(nextProjectId: string, options?: {
|
|
14
|
+
persistent?: boolean;
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function activateDraftForCard(cardId: string): void;
|
|
17
|
+
export declare function activateDraft(draftId: string, preferredCardId?: string): void;
|
|
18
|
+
export declare function clearSavedIterationDraft(cardId: string, expectedRevision?: number): boolean;
|
|
19
|
+
/** Returns the first preferred frame that exists, then the first frame. */
|
|
20
|
+
export declare function getEditableCardId(...preferredCardIds: readonly (string | null | undefined)[]): string | null;
|
|
21
|
+
export declare function assignSketchToDraft(draftId: string, sketchId: string): void;
|
|
22
|
+
export declare function sketchBelongsToCard(document: SketchDocument, cardId: string | null): boolean;
|
|
23
|
+
export declare function subscribeDrafts(listener: () => void): () => void;
|
|
24
|
+
export declare function getDraftsSnapshot(): DraftSnapshot;
|
|
25
|
+
export declare function useDrafts(): DraftSnapshot;
|
|
26
|
+
/** Discards stored and in-memory drafts for the current project. */
|
|
27
|
+
export declare function clearDrafts(): void;
|
|
28
|
+
export declare function resetDrafts(): void;
|