@dryui/feedback 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/annotation-marker.svelte +163 -0
- package/dist/components/annotation-marker.svelte.d.ts +11 -0
- package/dist/components/annotation-popup.svelte +669 -0
- package/dist/components/annotation-popup.svelte.d.ts +42 -0
- package/dist/components/highlight-overlay.svelte +48 -0
- package/dist/components/highlight-overlay.svelte.d.ts +8 -0
- package/dist/components/settings-panel.svelte +446 -0
- package/dist/components/settings-panel.svelte.d.ts +24 -0
- package/dist/components/toolbar.svelte +1111 -0
- package/dist/components/toolbar.svelte.d.ts +46 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +37 -0
- package/dist/feedback.svelte +2879 -0
- package/dist/feedback.svelte.d.ts +4 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/layout-mode/catalog.d.ts +16 -0
- package/dist/layout-mode/catalog.js +81 -0
- package/dist/layout-mode/component-actions.svelte +84 -0
- package/dist/layout-mode/component-actions.svelte.d.ts +18 -0
- package/dist/layout-mode/component-picker.svelte +73 -0
- package/dist/layout-mode/component-picker.svelte.d.ts +10 -0
- package/dist/layout-mode/design-mode.svelte +1115 -0
- package/dist/layout-mode/design-mode.svelte.d.ts +24 -0
- package/dist/layout-mode/design-palette.svelte +396 -0
- package/dist/layout-mode/design-palette.svelte.d.ts +20 -0
- package/dist/layout-mode/element-heuristics.d.ts +5 -0
- package/dist/layout-mode/element-heuristics.js +51 -0
- package/dist/layout-mode/freeze.d.ts +6 -0
- package/dist/layout-mode/freeze.js +163 -0
- package/dist/layout-mode/generated-library.d.ts +940 -0
- package/dist/layout-mode/generated-library.js +1445 -0
- package/dist/layout-mode/geometry.d.ts +38 -0
- package/dist/layout-mode/geometry.js +133 -0
- package/dist/layout-mode/history.d.ts +10 -0
- package/dist/layout-mode/history.js +45 -0
- package/dist/layout-mode/index.d.ts +23 -0
- package/dist/layout-mode/index.js +18 -0
- package/dist/layout-mode/live-mount.d.ts +20 -0
- package/dist/layout-mode/live-mount.js +70 -0
- package/dist/layout-mode/output.d.ts +26 -0
- package/dist/layout-mode/output.js +550 -0
- package/dist/layout-mode/placement-skeleton.d.ts +9 -0
- package/dist/layout-mode/placement-skeleton.js +535 -0
- package/dist/layout-mode/rearrange-overlay.svelte +1293 -0
- package/dist/layout-mode/rearrange-overlay.svelte.d.ts +18 -0
- package/dist/layout-mode/responsive-bar.svelte +39 -0
- package/dist/layout-mode/responsive-bar.svelte.d.ts +8 -0
- package/dist/layout-mode/route-creator.svelte +70 -0
- package/dist/layout-mode/route-creator.svelte.d.ts +8 -0
- package/dist/layout-mode/section-detection.d.ts +6 -0
- package/dist/layout-mode/section-detection.js +214 -0
- package/dist/layout-mode/spatial.d.ts +42 -0
- package/dist/layout-mode/spatial.js +156 -0
- package/dist/layout-mode/types.d.ts +144 -0
- package/dist/layout-mode/types.js +84 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +1 -0
- package/dist/utils/dryui-detection.d.ts +1 -0
- package/dist/utils/dryui-detection.js +219 -0
- package/dist/utils/element-id.d.ts +12 -0
- package/dist/utils/element-id.js +333 -0
- package/dist/utils/freeze.d.ts +7 -0
- package/dist/utils/freeze.js +168 -0
- package/dist/utils/output.d.ts +15 -0
- package/dist/utils/output.js +245 -0
- package/dist/utils/selection.d.ts +22 -0
- package/dist/utils/selection.js +58 -0
- package/dist/utils/shadow-dom.d.ts +4 -0
- package/dist/utils/shadow-dom.js +39 -0
- package/dist/utils/storage.d.ts +30 -0
- package/dist/utils/storage.js +206 -0
- package/dist/utils/svelte-detection.d.ts +8 -0
- package/dist/utils/svelte-detection.js +86 -0
- package/dist/utils/svelte-meta.d.ts +6 -0
- package/dist/utils/svelte-meta.js +69 -0
- package/dist/utils/sync.d.ts +18 -0
- package/dist/utils/sync.js +62 -0
- package/package.json +65 -0
|
@@ -0,0 +1,2879 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { Hotkey } from '@dryui/primitives/hotkey';
|
|
4
|
+
import { Button, Portal, Slider, Stack, Text } from '@dryui/ui';
|
|
5
|
+
import type {
|
|
6
|
+
Annotation,
|
|
7
|
+
ConnectionStatus,
|
|
8
|
+
DesignPlacement,
|
|
9
|
+
FeedbackLayoutMode,
|
|
10
|
+
FeedbackProps,
|
|
11
|
+
FeedbackSettings,
|
|
12
|
+
RearrangeState,
|
|
13
|
+
Rect,
|
|
14
|
+
ThreadMessage,
|
|
15
|
+
WireframeState,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import type { CanvasPurpose, LayoutModeComponentType } from './layout-mode/index.js';
|
|
18
|
+
import {
|
|
19
|
+
getAccessibilityInfo,
|
|
20
|
+
getElementClasses,
|
|
21
|
+
getForensicComputedStyles,
|
|
22
|
+
getFullElementPath,
|
|
23
|
+
getNearbyElements,
|
|
24
|
+
getNearbyText,
|
|
25
|
+
identifyElement,
|
|
26
|
+
} from './utils/element-id.js';
|
|
27
|
+
import {
|
|
28
|
+
getPopupPosition,
|
|
29
|
+
hasMeaningfulArea,
|
|
30
|
+
intersectsRect,
|
|
31
|
+
normalizeText,
|
|
32
|
+
rectFromPoints,
|
|
33
|
+
toRect,
|
|
34
|
+
type Point,
|
|
35
|
+
unionRects,
|
|
36
|
+
uniqueLabels,
|
|
37
|
+
} from './utils/selection.js';
|
|
38
|
+
import { closestCrossingShadow, deepElementFromPoint } from './utils/shadow-dom.js';
|
|
39
|
+
import { detectDryUIComponent } from './utils/dryui-detection.js';
|
|
40
|
+
import { detectSvelteMetadata } from './utils/svelte-detection.js';
|
|
41
|
+
import {
|
|
42
|
+
clearSessionId,
|
|
43
|
+
clearDesignPlacements,
|
|
44
|
+
clearRearrangeState,
|
|
45
|
+
clearWireframeState,
|
|
46
|
+
loadAllAnnotations,
|
|
47
|
+
loadAnnotations,
|
|
48
|
+
loadDesignPlacements,
|
|
49
|
+
loadRearrangeState,
|
|
50
|
+
loadSessionId,
|
|
51
|
+
loadSettings,
|
|
52
|
+
loadToolbarHidden,
|
|
53
|
+
loadWireframeState,
|
|
54
|
+
saveAnnotations,
|
|
55
|
+
saveAnnotationsWithSyncMarker,
|
|
56
|
+
saveDesignPlacements,
|
|
57
|
+
saveRearrangeState,
|
|
58
|
+
saveSessionId,
|
|
59
|
+
saveSettings,
|
|
60
|
+
saveToolbarHidden,
|
|
61
|
+
saveWireframeState,
|
|
62
|
+
} from './utils/storage.js';
|
|
63
|
+
import { generateOutput } from './utils/output.js';
|
|
64
|
+
import { freezeAnimations, unfreezeAnimations } from './utils/freeze.js';
|
|
65
|
+
import {
|
|
66
|
+
createSession,
|
|
67
|
+
deleteAnnotation as deleteAnnotationOnServer,
|
|
68
|
+
getSession,
|
|
69
|
+
requestAction,
|
|
70
|
+
replyToAnnotation as replyToAnnotationOnServer,
|
|
71
|
+
syncAnnotation as syncAnnotationToServer,
|
|
72
|
+
updateAnnotation as updateAnnotationOnServer,
|
|
73
|
+
} from './utils/sync.js';
|
|
74
|
+
import {
|
|
75
|
+
DEFAULT_SIZES,
|
|
76
|
+
DesignMode,
|
|
77
|
+
DesignPalette,
|
|
78
|
+
RearrangeOverlay,
|
|
79
|
+
detectPageSections,
|
|
80
|
+
} from './layout-mode/index.js';
|
|
81
|
+
import ComponentActions from './layout-mode/component-actions.svelte';
|
|
82
|
+
import ComponentPicker from './layout-mode/component-picker.svelte';
|
|
83
|
+
import RouteCreator from './layout-mode/route-creator.svelte';
|
|
84
|
+
import { createHistory } from './layout-mode/history.js';
|
|
85
|
+
import { generateSketchBrief, generateEditBrief } from './layout-mode/output.js';
|
|
86
|
+
import type { ComponentAction, CanvasWidth } from './layout-mode/types.js';
|
|
87
|
+
import { DEFAULT_SETTINGS } from './constants.js';
|
|
88
|
+
import AnnotationMarker from './components/annotation-marker.svelte';
|
|
89
|
+
import AnnotationPopup from './components/annotation-popup.svelte';
|
|
90
|
+
import HighlightOverlay from './components/highlight-overlay.svelte';
|
|
91
|
+
import FeedbackToolbar from './components/toolbar.svelte';
|
|
92
|
+
|
|
93
|
+
interface PendingDraft {
|
|
94
|
+
elementLabel: string;
|
|
95
|
+
position: { x: number; y: number };
|
|
96
|
+
data: Omit<Annotation, 'id' | 'timestamp' | 'comment' | 'kind' | 'color'> & Partial<Annotation>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type CopyState = 'idle' | 'copied';
|
|
100
|
+
type SubmitState = 'idle' | 'sending' | 'sent' | 'failed';
|
|
101
|
+
|
|
102
|
+
interface MultiSelectTarget {
|
|
103
|
+
element: Element;
|
|
104
|
+
label: string;
|
|
105
|
+
path: string;
|
|
106
|
+
rect: Rect;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface MovedSectionEntry {
|
|
110
|
+
el: HTMLElement;
|
|
111
|
+
originalStyles: {
|
|
112
|
+
transform: string;
|
|
113
|
+
transformOrigin: string;
|
|
114
|
+
opacity: string;
|
|
115
|
+
position: string;
|
|
116
|
+
zIndex: string;
|
|
117
|
+
display: string;
|
|
118
|
+
transition: string;
|
|
119
|
+
};
|
|
120
|
+
ancestors: Array<{
|
|
121
|
+
el: HTMLElement;
|
|
122
|
+
overflow: string;
|
|
123
|
+
overflowX: string;
|
|
124
|
+
overflowY: string;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface RemoteAnnotationContext {
|
|
129
|
+
x?: number;
|
|
130
|
+
y?: number;
|
|
131
|
+
isFixed?: boolean;
|
|
132
|
+
boundingBox?: Rect;
|
|
133
|
+
selectedText?: string;
|
|
134
|
+
nearbyText?: string;
|
|
135
|
+
cssClasses?: string;
|
|
136
|
+
nearbyElements?: string;
|
|
137
|
+
computedStyles?: string;
|
|
138
|
+
accessibility?: string;
|
|
139
|
+
fullPath?: string;
|
|
140
|
+
reactComponents?: string;
|
|
141
|
+
svelteComponent?: string;
|
|
142
|
+
sourceFile?: string;
|
|
143
|
+
dryuiComponent?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface RemoteAnnotationPayload {
|
|
147
|
+
id?: string;
|
|
148
|
+
element?: string | null;
|
|
149
|
+
elementPath?: string;
|
|
150
|
+
element_path?: string | null;
|
|
151
|
+
comment?: string;
|
|
152
|
+
status?: Annotation['status'];
|
|
153
|
+
intent?: Annotation['intent'];
|
|
154
|
+
severity?: Annotation['severity'];
|
|
155
|
+
timestamp?: number;
|
|
156
|
+
created_at?: number;
|
|
157
|
+
kind?: Annotation['kind'];
|
|
158
|
+
color?: Annotation['color'];
|
|
159
|
+
context_json?: string | null;
|
|
160
|
+
x?: number;
|
|
161
|
+
y?: number;
|
|
162
|
+
isFixed?: boolean;
|
|
163
|
+
boundingBox?: Rect;
|
|
164
|
+
selectedText?: string;
|
|
165
|
+
nearbyText?: string;
|
|
166
|
+
cssClasses?: string;
|
|
167
|
+
nearbyElements?: string;
|
|
168
|
+
computedStyles?: string;
|
|
169
|
+
accessibility?: string;
|
|
170
|
+
fullPath?: string;
|
|
171
|
+
reactComponents?: string;
|
|
172
|
+
svelteComponent?: string;
|
|
173
|
+
sourceFile?: string;
|
|
174
|
+
dryuiComponent?: string;
|
|
175
|
+
thread?: Annotation['thread'];
|
|
176
|
+
resolvedAt?: string;
|
|
177
|
+
resolved_at?: number | string | null;
|
|
178
|
+
resolvedBy?: Annotation['resolvedBy'];
|
|
179
|
+
resolved_by?: string | null;
|
|
180
|
+
resolutionNote?: string;
|
|
181
|
+
resolution_note?: string | null;
|
|
182
|
+
sessionId?: string;
|
|
183
|
+
url?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const DRAG_THRESHOLD_PX = 8;
|
|
187
|
+
const POPUP_WIDTH_PX = 340;
|
|
188
|
+
const MAX_AREA_TARGETS = 12;
|
|
189
|
+
const CONNECTION_POLL_MS = 10_000;
|
|
190
|
+
const BLOCKED_INTERACTION_SELECTOR = [
|
|
191
|
+
'a',
|
|
192
|
+
'button',
|
|
193
|
+
'input',
|
|
194
|
+
'select',
|
|
195
|
+
'summary',
|
|
196
|
+
'textarea',
|
|
197
|
+
'[onclick]',
|
|
198
|
+
'[role="button"]',
|
|
199
|
+
'[role="link"]',
|
|
200
|
+
'[tabindex]',
|
|
201
|
+
].join(', ');
|
|
202
|
+
const HIDDEN_ANNOTATION_STATUSES = new Set(['dismissed']);
|
|
203
|
+
const INTERACTIVE_TAGS = new Set([
|
|
204
|
+
'a',
|
|
205
|
+
'article',
|
|
206
|
+
'button',
|
|
207
|
+
'img',
|
|
208
|
+
'input',
|
|
209
|
+
'label',
|
|
210
|
+
'option',
|
|
211
|
+
'section',
|
|
212
|
+
'select',
|
|
213
|
+
'summary',
|
|
214
|
+
'svg',
|
|
215
|
+
'textarea',
|
|
216
|
+
]);
|
|
217
|
+
const EMPTY_REARRANGE_STATE = (): RearrangeState => ({
|
|
218
|
+
sections: [],
|
|
219
|
+
originalOrder: [],
|
|
220
|
+
detectedAt: Date.now(),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
function isRenderableAnnotation(annotation: Annotation): boolean {
|
|
224
|
+
return !annotation.status || !HIDDEN_ANNOTATION_STATUSES.has(annotation.status);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let {
|
|
228
|
+
annotations: externalAnnotations,
|
|
229
|
+
onAnnotationAdd,
|
|
230
|
+
onAnnotationUpdate,
|
|
231
|
+
onAnnotationReply,
|
|
232
|
+
onAnnotationDelete,
|
|
233
|
+
onAnnotationsClear,
|
|
234
|
+
onCopy,
|
|
235
|
+
onSubmit,
|
|
236
|
+
onSessionCreated,
|
|
237
|
+
copyToClipboard = true,
|
|
238
|
+
defaultDetail = 'standard',
|
|
239
|
+
defaultColor = DEFAULT_SETTINGS.annotationColor,
|
|
240
|
+
defaultTheme = 'dark',
|
|
241
|
+
enableDesignMode = true,
|
|
242
|
+
enableRearrange = true,
|
|
243
|
+
enableSvelteDetection = true,
|
|
244
|
+
endpoint,
|
|
245
|
+
sessionId: providedSessionId,
|
|
246
|
+
webhookUrl,
|
|
247
|
+
shortcut = 'meta+m',
|
|
248
|
+
class: className,
|
|
249
|
+
}: FeedbackProps = $props();
|
|
250
|
+
|
|
251
|
+
const pathname = typeof window !== 'undefined' ? window.location.pathname : '/';
|
|
252
|
+
const initialSettings = (() => {
|
|
253
|
+
const stored = loadSettings();
|
|
254
|
+
return {
|
|
255
|
+
...stored,
|
|
256
|
+
outputDetail: stored.outputDetail ?? defaultDetail,
|
|
257
|
+
annotationColor: stored.annotationColor ?? defaultColor,
|
|
258
|
+
theme: stored.theme ?? defaultTheme,
|
|
259
|
+
} satisfies FeedbackSettings;
|
|
260
|
+
})();
|
|
261
|
+
|
|
262
|
+
let active = $state(false);
|
|
263
|
+
let localAnnotations = $state<Annotation[]>(loadAnnotations(pathname).filter(isRenderableAnnotation));
|
|
264
|
+
let hoverRect = $state<Rect | null>(null);
|
|
265
|
+
let hoverLabel = $state<string | undefined>(undefined);
|
|
266
|
+
let markerHoverRect = $state<Rect | null>(null);
|
|
267
|
+
let markerHoverLabel = $state<string | undefined>(undefined);
|
|
268
|
+
let pendingDraft = $state<PendingDraft | null>(null);
|
|
269
|
+
let pendingPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
270
|
+
let popupColor = $state<Annotation['color']>(initialSettings.annotationColor);
|
|
271
|
+
let editingAnnotation = $state<Annotation | null>(null);
|
|
272
|
+
let settings = $state<FeedbackSettings>(initialSettings);
|
|
273
|
+
let pointerDown = $state<Point | null>(null);
|
|
274
|
+
let dragRect = $state<Rect | null>(null);
|
|
275
|
+
let pendingMultiSelectTargets = $state<MultiSelectTarget[]>([]);
|
|
276
|
+
let suppressClick = $state(false);
|
|
277
|
+
let toolbarHidden = $state(loadToolbarHidden());
|
|
278
|
+
let paused = $state(false);
|
|
279
|
+
let showMarkers = $state(true);
|
|
280
|
+
let layoutMode = $state<FeedbackLayoutMode>('idle');
|
|
281
|
+
let paletteOpen = $state(false);
|
|
282
|
+
let overlayInteracting = $state(false);
|
|
283
|
+
let activeComponent = $state<LayoutModeComponentType | null>(null);
|
|
284
|
+
let designPlacements = $state<DesignPlacement[]>(loadDesignPlacements(pathname));
|
|
285
|
+
let rearrangeState = $state<RearrangeState | null>(loadRearrangeState(pathname));
|
|
286
|
+
let copyState = $state<CopyState>('idle');
|
|
287
|
+
let submitState = $state<SubmitState>('idle');
|
|
288
|
+
let designSelectedIds = $state<string[]>([]);
|
|
289
|
+
let rearrangeSelectedIds = $state<string[]>([]);
|
|
290
|
+
let designSelectionCount = $state(0);
|
|
291
|
+
let rearrangeSelectionCount = $state(0);
|
|
292
|
+
let designDeselectSignal = $state(0);
|
|
293
|
+
let rearrangeDeselectSignal = $state(0);
|
|
294
|
+
let designClearSignal = $state(0);
|
|
295
|
+
let rearrangeClearSignal = $state(0);
|
|
296
|
+
let canvasOpacity = $state(0.92);
|
|
297
|
+
let componentPickerOpen = $state(false);
|
|
298
|
+
let routeCreatorOpen = $state(false);
|
|
299
|
+
let componentActionsTarget = $state<{
|
|
300
|
+
name: string;
|
|
301
|
+
selector: string;
|
|
302
|
+
props: Record<string, string>;
|
|
303
|
+
position: { x: number; y: number };
|
|
304
|
+
} | null>(null);
|
|
305
|
+
let pendingComponentActions = $state<ComponentAction[]>([]);
|
|
306
|
+
let canvasWidth = $state<CanvasWidth>(1280);
|
|
307
|
+
let newPageRoute = $state<string | null>(null);
|
|
308
|
+
let newPageRecipe = $state<string | null>(null);
|
|
309
|
+
const layoutHistory = createHistory();
|
|
310
|
+
|
|
311
|
+
const initialWireframeState = loadWireframeState(pathname);
|
|
312
|
+
let canvasPurpose = $state<CanvasPurpose>(initialWireframeState ? initialWireframeState.purpose : 'replace-current');
|
|
313
|
+
let wireframePrompt = $state(initialWireframeState?.prompt ?? '');
|
|
314
|
+
let exploreStash = $state<WireframeState>({
|
|
315
|
+
rearrange: loadRearrangeState(pathname),
|
|
316
|
+
placements: loadDesignPlacements(pathname),
|
|
317
|
+
purpose: 'replace-current',
|
|
318
|
+
prompt: '',
|
|
319
|
+
});
|
|
320
|
+
let currentSessionId = $state<string | null>(null);
|
|
321
|
+
let connectionStatus = $state<ConnectionStatus>('disconnected');
|
|
322
|
+
let sessionBootstrapRequested = false;
|
|
323
|
+
let previousConnectionStatus: ConnectionStatus | null = null;
|
|
324
|
+
const placementShadowIds = new Map<string, string>();
|
|
325
|
+
const placementShadowSnapshots = new Map<string, string>();
|
|
326
|
+
const rearrangeShadowIds = new Map<string, string>();
|
|
327
|
+
const rearrangeShadowSnapshots = new Map<string, string>();
|
|
328
|
+
const movedSections = new Map<string, MovedSectionEntry>();
|
|
329
|
+
let crossDesignDragStart: Map<string, { x: number; y: number }> | null = null;
|
|
330
|
+
let crossRearrangeDragStart: Map<string, { x: number; y: number }> | null = null;
|
|
331
|
+
|
|
332
|
+
const useExternal = $derived(externalAnnotations !== undefined);
|
|
333
|
+
const annotations = $derived((useExternal ? externalAnnotations! : localAnnotations).filter(isRenderableAnnotation));
|
|
334
|
+
const outputDetail = $derived(settings.outputDetail ?? defaultDetail);
|
|
335
|
+
const annotationColor = $derived(settings.annotationColor ?? defaultColor);
|
|
336
|
+
const svelteDetectionEnabled = $derived(enableSvelteDetection && settings.svelteDetection);
|
|
337
|
+
const layoutActive = $derived(layoutMode !== 'idle');
|
|
338
|
+
const rearrangeActive = $derived(layoutMode === 'rearrange');
|
|
339
|
+
const annotatingPage = $derived(active && layoutMode === 'idle');
|
|
340
|
+
const showPopup = $derived(pendingDraft !== null || editingAnnotation !== null);
|
|
341
|
+
const blankCanvas = $derived(canvasPurpose === 'new-page');
|
|
342
|
+
const submitTargetUrl = $derived((settings.webhookUrl || webhookUrl || '').trim());
|
|
343
|
+
const shouldShowMarkers = $derived(active && showMarkers && layoutMode === 'idle');
|
|
344
|
+
const activeHighlightRect = $derived(markerHoverRect ?? hoverRect);
|
|
345
|
+
const activeHighlightLabel = $derived(markerHoverLabel ?? hoverLabel);
|
|
346
|
+
const hasLayoutOutput = $derived(
|
|
347
|
+
designPlacements.length > 0 ||
|
|
348
|
+
(rearrangeState?.sections.length ?? 0) > 0,
|
|
349
|
+
);
|
|
350
|
+
const hasOutput = $derived(annotations.length > 0 || hasLayoutOutput);
|
|
351
|
+
const copyLabel = $derived(layoutActive && blankCanvas ? 'Copy layout' : undefined);
|
|
352
|
+
const popupElement = $derived(
|
|
353
|
+
editingAnnotation ? editingAnnotation.element : pendingDraft ? pendingDraft.elementLabel : '',
|
|
354
|
+
);
|
|
355
|
+
const popupInitialValue = $derived(editingAnnotation?.comment ?? '');
|
|
356
|
+
const popupSelectedText = $derived(editingAnnotation?.selectedText ?? pendingDraft?.data.selectedText ?? undefined);
|
|
357
|
+
const popupComputedStyles = $derived(editingAnnotation?.computedStyles ?? pendingDraft?.data.computedStyles ?? undefined);
|
|
358
|
+
const canSubmit = $derived(onSubmit !== undefined || Boolean(endpoint) || submitTargetUrl.length > 0);
|
|
359
|
+
|
|
360
|
+
function persistAnnotations(updated: Annotation[]) {
|
|
361
|
+
if (!useExternal) {
|
|
362
|
+
localAnnotations = updated;
|
|
363
|
+
saveAnnotations(pathname, updated);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function pageUrl(path = pathname): string {
|
|
368
|
+
if (typeof window === 'undefined') return path;
|
|
369
|
+
return new URL(path, window.location.origin).toString();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function displayPath(): string {
|
|
373
|
+
if (typeof window === 'undefined') return pathname;
|
|
374
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function setCopyState(next: CopyState) {
|
|
378
|
+
copyState = next;
|
|
379
|
+
if (next === 'idle' || typeof window === 'undefined') return;
|
|
380
|
+
|
|
381
|
+
window.setTimeout(() => {
|
|
382
|
+
if (copyState === next) {
|
|
383
|
+
copyState = 'idle';
|
|
384
|
+
}
|
|
385
|
+
}, 2000);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function setSubmitState(next: SubmitState) {
|
|
389
|
+
submitState = next;
|
|
390
|
+
if (next === 'idle' || next === 'sending' || typeof window === 'undefined') return;
|
|
391
|
+
|
|
392
|
+
window.setTimeout(() => {
|
|
393
|
+
if (submitState === next) {
|
|
394
|
+
submitState = 'idle';
|
|
395
|
+
}
|
|
396
|
+
}, 2500);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isValidHttpUrl(value: string): boolean {
|
|
400
|
+
if (!value) return false;
|
|
401
|
+
try {
|
|
402
|
+
const parsed = new URL(value);
|
|
403
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
410
|
+
return typeof value === 'object' && value !== null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isRect(value: unknown): value is Rect {
|
|
414
|
+
return isRecord(value) &&
|
|
415
|
+
typeof value['x'] === 'number' &&
|
|
416
|
+
typeof value['y'] === 'number' &&
|
|
417
|
+
typeof value['width'] === 'number' &&
|
|
418
|
+
typeof value['height'] === 'number';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function toResolvedAt(value: RemoteAnnotationPayload['resolvedAt'] | RemoteAnnotationPayload['resolved_at']): string | undefined {
|
|
422
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
423
|
+
return value;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
427
|
+
return new Date(value).toISOString();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function toResolvedBy(value: unknown): Annotation['resolvedBy'] | undefined {
|
|
434
|
+
return value === 'human' || value === 'agent' ? value : undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseRemoteAnnotationContext(payload: RemoteAnnotationPayload): RemoteAnnotationContext {
|
|
438
|
+
const context: RemoteAnnotationContext = {};
|
|
439
|
+
|
|
440
|
+
if (typeof payload.context_json === 'string' && payload.context_json.length > 0) {
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(payload.context_json) as unknown;
|
|
443
|
+
if (isRecord(parsed)) {
|
|
444
|
+
if (typeof parsed['x'] === 'number') context.x = parsed['x'];
|
|
445
|
+
if (typeof parsed['y'] === 'number') context.y = parsed['y'];
|
|
446
|
+
if (typeof parsed['isFixed'] === 'boolean') context.isFixed = parsed['isFixed'];
|
|
447
|
+
if (isRect(parsed['boundingBox'])) context.boundingBox = parsed['boundingBox'];
|
|
448
|
+
if (typeof parsed['selectedText'] === 'string') context.selectedText = parsed['selectedText'];
|
|
449
|
+
if (typeof parsed['nearbyText'] === 'string') context.nearbyText = parsed['nearbyText'];
|
|
450
|
+
if (typeof parsed['cssClasses'] === 'string') context.cssClasses = parsed['cssClasses'];
|
|
451
|
+
if (typeof parsed['nearbyElements'] === 'string') context.nearbyElements = parsed['nearbyElements'];
|
|
452
|
+
if (typeof parsed['computedStyles'] === 'string') context.computedStyles = parsed['computedStyles'];
|
|
453
|
+
if (typeof parsed['accessibility'] === 'string') context.accessibility = parsed['accessibility'];
|
|
454
|
+
if (typeof parsed['fullPath'] === 'string') context.fullPath = parsed['fullPath'];
|
|
455
|
+
if (typeof parsed['reactComponents'] === 'string') context.reactComponents = parsed['reactComponents'];
|
|
456
|
+
if (typeof parsed['svelteComponent'] === 'string') context.svelteComponent = parsed['svelteComponent'];
|
|
457
|
+
if (typeof parsed['sourceFile'] === 'string') context.sourceFile = parsed['sourceFile'];
|
|
458
|
+
if (typeof parsed['dryuiComponent'] === 'string') context.dryuiComponent = parsed['dryuiComponent'];
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
// Ignore malformed context payloads.
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (typeof payload.x === 'number') context.x = payload.x;
|
|
466
|
+
if (typeof payload.y === 'number') context.y = payload.y;
|
|
467
|
+
if (typeof payload.isFixed === 'boolean') context.isFixed = payload.isFixed;
|
|
468
|
+
if (payload.boundingBox) context.boundingBox = payload.boundingBox;
|
|
469
|
+
if (payload.selectedText !== undefined) context.selectedText = payload.selectedText;
|
|
470
|
+
if (payload.nearbyText !== undefined) context.nearbyText = payload.nearbyText;
|
|
471
|
+
if (payload.cssClasses !== undefined) context.cssClasses = payload.cssClasses;
|
|
472
|
+
if (payload.nearbyElements !== undefined) context.nearbyElements = payload.nearbyElements;
|
|
473
|
+
if (payload.computedStyles !== undefined) context.computedStyles = payload.computedStyles;
|
|
474
|
+
if (payload.accessibility !== undefined) context.accessibility = payload.accessibility;
|
|
475
|
+
if (payload.fullPath !== undefined) context.fullPath = payload.fullPath;
|
|
476
|
+
if (payload.reactComponents !== undefined) context.reactComponents = payload.reactComponents;
|
|
477
|
+
if (payload.svelteComponent !== undefined) context.svelteComponent = payload.svelteComponent;
|
|
478
|
+
if (payload.sourceFile !== undefined) context.sourceFile = payload.sourceFile;
|
|
479
|
+
if (payload.dryuiComponent !== undefined) context.dryuiComponent = payload.dryuiComponent;
|
|
480
|
+
|
|
481
|
+
return context;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function normalizeRemoteAnnotation(payload: RemoteAnnotationPayload | Annotation, existing?: Annotation): Annotation | null {
|
|
485
|
+
const id = payload.id ?? existing?.id;
|
|
486
|
+
if (!id) return null;
|
|
487
|
+
|
|
488
|
+
const context = parseRemoteAnnotationContext(payload);
|
|
489
|
+
const resolvedAt = toResolvedAt(
|
|
490
|
+
payload.resolvedAt ?? ('resolved_at' in payload ? payload.resolved_at : undefined),
|
|
491
|
+
) ?? existing?.resolvedAt;
|
|
492
|
+
const resolvedBy = toResolvedBy(
|
|
493
|
+
payload.resolvedBy ?? ('resolved_by' in payload ? payload.resolved_by : undefined),
|
|
494
|
+
) ?? existing?.resolvedBy;
|
|
495
|
+
const resolutionNote = payload.resolutionNote
|
|
496
|
+
?? ('resolution_note' in payload ? payload.resolution_note : undefined)
|
|
497
|
+
?? existing?.resolutionNote;
|
|
498
|
+
const thread = payload.thread ?? existing?.thread;
|
|
499
|
+
const timestamp = payload.timestamp
|
|
500
|
+
?? ('created_at' in payload ? payload.created_at : undefined)
|
|
501
|
+
?? existing?.timestamp
|
|
502
|
+
?? Date.now();
|
|
503
|
+
const elementPath = payload.elementPath
|
|
504
|
+
?? ('element_path' in payload ? payload.element_path : undefined)
|
|
505
|
+
?? existing?.elementPath
|
|
506
|
+
?? '';
|
|
507
|
+
|
|
508
|
+
const annotation: Annotation = {
|
|
509
|
+
id,
|
|
510
|
+
x: context.x ?? existing?.x ?? 50,
|
|
511
|
+
y: context.y ?? existing?.y ?? 0,
|
|
512
|
+
isFixed: context.isFixed ?? existing?.isFixed ?? false,
|
|
513
|
+
timestamp,
|
|
514
|
+
element: payload.element ?? existing?.element ?? '',
|
|
515
|
+
elementPath,
|
|
516
|
+
comment: payload.comment ?? existing?.comment ?? '',
|
|
517
|
+
kind: payload.kind ?? existing?.kind ?? 'feedback',
|
|
518
|
+
color: payload.color ?? existing?.color ?? DEFAULT_SETTINGS.annotationColor,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (payload.status ?? existing?.status) annotation.status = payload.status ?? existing?.status;
|
|
522
|
+
if (payload.intent ?? existing?.intent) annotation.intent = payload.intent ?? existing?.intent;
|
|
523
|
+
if (payload.severity ?? existing?.severity) annotation.severity = payload.severity ?? existing?.severity;
|
|
524
|
+
if (context.boundingBox ?? existing?.boundingBox) {
|
|
525
|
+
annotation.boundingBox = context.boundingBox ?? existing?.boundingBox;
|
|
526
|
+
}
|
|
527
|
+
if (context.selectedText ?? existing?.selectedText) {
|
|
528
|
+
annotation.selectedText = context.selectedText ?? existing?.selectedText;
|
|
529
|
+
}
|
|
530
|
+
if (context.nearbyText ?? existing?.nearbyText) {
|
|
531
|
+
annotation.nearbyText = context.nearbyText ?? existing?.nearbyText;
|
|
532
|
+
}
|
|
533
|
+
if (context.cssClasses ?? existing?.cssClasses) {
|
|
534
|
+
annotation.cssClasses = context.cssClasses ?? existing?.cssClasses;
|
|
535
|
+
}
|
|
536
|
+
if (context.nearbyElements ?? existing?.nearbyElements) {
|
|
537
|
+
annotation.nearbyElements = context.nearbyElements ?? existing?.nearbyElements;
|
|
538
|
+
}
|
|
539
|
+
if (context.computedStyles ?? existing?.computedStyles) {
|
|
540
|
+
annotation.computedStyles = context.computedStyles ?? existing?.computedStyles;
|
|
541
|
+
}
|
|
542
|
+
if (context.accessibility ?? existing?.accessibility) {
|
|
543
|
+
annotation.accessibility = context.accessibility ?? existing?.accessibility;
|
|
544
|
+
}
|
|
545
|
+
if (context.fullPath ?? existing?.fullPath) {
|
|
546
|
+
annotation.fullPath = context.fullPath ?? existing?.fullPath;
|
|
547
|
+
}
|
|
548
|
+
if (context.reactComponents ?? existing?.reactComponents) {
|
|
549
|
+
annotation.reactComponents = context.reactComponents ?? existing?.reactComponents;
|
|
550
|
+
}
|
|
551
|
+
if (context.svelteComponent ?? existing?.svelteComponent) {
|
|
552
|
+
annotation.svelteComponent = context.svelteComponent ?? existing?.svelteComponent;
|
|
553
|
+
}
|
|
554
|
+
if (context.sourceFile ?? existing?.sourceFile) {
|
|
555
|
+
annotation.sourceFile = context.sourceFile ?? existing?.sourceFile;
|
|
556
|
+
}
|
|
557
|
+
if (context.dryuiComponent ?? existing?.dryuiComponent) {
|
|
558
|
+
annotation.dryuiComponent = context.dryuiComponent ?? existing?.dryuiComponent;
|
|
559
|
+
}
|
|
560
|
+
if (thread !== undefined) annotation.thread = thread;
|
|
561
|
+
if (resolvedAt) annotation.resolvedAt = resolvedAt;
|
|
562
|
+
if (resolvedBy) annotation.resolvedBy = resolvedBy;
|
|
563
|
+
if (resolutionNote) annotation.resolutionNote = resolutionNote;
|
|
564
|
+
if (payload.sessionId ?? existing?.sessionId) {
|
|
565
|
+
annotation.sessionId = payload.sessionId ?? existing?.sessionId;
|
|
566
|
+
}
|
|
567
|
+
if (payload.url ?? existing?.url) {
|
|
568
|
+
annotation.url = payload.url ?? existing?.url;
|
|
569
|
+
}
|
|
570
|
+
if (existing?._syncedTo) annotation._syncedTo = existing._syncedTo;
|
|
571
|
+
|
|
572
|
+
return annotation;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function normalizeRemoteAnnotations(items: ReadonlyArray<RemoteAnnotationPayload | Annotation>): Annotation[] {
|
|
576
|
+
return items
|
|
577
|
+
.map((item) => normalizeRemoteAnnotation(item))
|
|
578
|
+
.filter((item): item is Annotation => item !== null);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function parseRemoteEventData(data: string): RemoteAnnotationPayload[] {
|
|
582
|
+
try {
|
|
583
|
+
const parsed = JSON.parse(data) as unknown;
|
|
584
|
+
|
|
585
|
+
if (Array.isArray(parsed)) {
|
|
586
|
+
return parsed.filter(isRecord) as RemoteAnnotationPayload[];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!isRecord(parsed)) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (Array.isArray(parsed['payload'])) {
|
|
594
|
+
return parsed['payload'].filter(isRecord) as RemoteAnnotationPayload[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (isRecord(parsed['payload'])) {
|
|
598
|
+
return [parsed['payload'] as RemoteAnnotationPayload];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return [parsed as RemoteAnnotationPayload];
|
|
602
|
+
} catch {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function markAnnotationSynced(annotation: Annotation, sessionId: string): Annotation {
|
|
608
|
+
return {
|
|
609
|
+
...annotation,
|
|
610
|
+
_syncedTo: sessionId,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function replaceLocalAnnotation(existingId: string, nextAnnotation: Annotation) {
|
|
615
|
+
if (useExternal) return;
|
|
616
|
+
const updated = annotations.map((annotation) => (annotation.id === existingId ? nextAnnotation : annotation));
|
|
617
|
+
persistAnnotations(updated);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function updateExistingAnnotation(current: Annotation, patch: Partial<Annotation>): Annotation {
|
|
621
|
+
const updated = { ...current, ...patch };
|
|
622
|
+
const nextAnnotations = annotations.map((annotation) =>
|
|
623
|
+
annotation.id === current.id ? updated : annotation,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
persistAnnotations(nextAnnotations);
|
|
627
|
+
onAnnotationUpdate?.(updated);
|
|
628
|
+
|
|
629
|
+
if (endpoint) {
|
|
630
|
+
void updateAnnotationOnServer(endpoint, updated.id, patch).catch(() => {
|
|
631
|
+
connectionStatus = 'disconnected';
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
void fireWebhook('annotation.update', { annotation: updated });
|
|
636
|
+
return updated;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function deleteExistingAnnotation(annotation: Annotation) {
|
|
640
|
+
const nextAnnotations = annotations.filter((entry) => entry.id !== annotation.id);
|
|
641
|
+
persistAnnotations(nextAnnotations);
|
|
642
|
+
onAnnotationDelete?.(annotation);
|
|
643
|
+
|
|
644
|
+
if (endpoint) {
|
|
645
|
+
void deleteAnnotationOnServer(endpoint, annotation.id).catch(() => {
|
|
646
|
+
connectionStatus = 'disconnected';
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
void fireWebhook('annotation.delete', { annotation });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function fireWebhook(
|
|
654
|
+
event: string,
|
|
655
|
+
payload: Record<string, unknown>,
|
|
656
|
+
force: boolean = false,
|
|
657
|
+
): Promise<boolean> {
|
|
658
|
+
if (!isValidHttpUrl(submitTargetUrl) || (!settings.webhooksEnabled && !force)) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
const response = await fetch(submitTargetUrl, {
|
|
664
|
+
method: 'POST',
|
|
665
|
+
headers: { 'Content-Type': 'application/json' },
|
|
666
|
+
body: JSON.stringify({
|
|
667
|
+
event,
|
|
668
|
+
timestamp: Date.now(),
|
|
669
|
+
url: pageUrl(),
|
|
670
|
+
...payload,
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
return response.ok;
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function ensureSession(forceCreate: boolean = false): Promise<string | null> {
|
|
680
|
+
if (!endpoint) return null;
|
|
681
|
+
|
|
682
|
+
const storedSessionId = loadSessionId(pathname);
|
|
683
|
+
const targetSessionId = forceCreate ? null : currentSessionId ?? providedSessionId ?? storedSessionId;
|
|
684
|
+
|
|
685
|
+
if (targetSessionId) {
|
|
686
|
+
try {
|
|
687
|
+
const session = await getSession(endpoint, targetSessionId);
|
|
688
|
+
currentSessionId = session.id;
|
|
689
|
+
connectionStatus = 'connected';
|
|
690
|
+
saveSessionId(pathname, session.id);
|
|
691
|
+
return session.id;
|
|
692
|
+
} catch {
|
|
693
|
+
clearSessionId(pathname);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
connectionStatus = 'connecting';
|
|
699
|
+
const session = await createSession(endpoint, pageUrl());
|
|
700
|
+
currentSessionId = session.id;
|
|
701
|
+
connectionStatus = 'connected';
|
|
702
|
+
saveSessionId(pathname, session.id);
|
|
703
|
+
onSessionCreated?.(session.id);
|
|
704
|
+
return session.id;
|
|
705
|
+
} catch {
|
|
706
|
+
connectionStatus = 'disconnected';
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function syncExistingLocalAnnotations() {
|
|
712
|
+
if (!endpoint || useExternal) return;
|
|
713
|
+
|
|
714
|
+
const sessionId = await ensureSession();
|
|
715
|
+
if (!sessionId) return;
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const session = await getSession(endpoint, sessionId);
|
|
719
|
+
const serverAnnotations = normalizeRemoteAnnotations(session.annotations);
|
|
720
|
+
const serverIds = new Set(serverAnnotations.map((annotation) => annotation.id));
|
|
721
|
+
const localToMerge = loadAnnotations<Annotation>(pathname).filter(
|
|
722
|
+
(annotation) => !serverIds.has(annotation.id) && isRenderableAnnotation(annotation),
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
if (localToMerge.length === 0) {
|
|
726
|
+
persistAnnotations(serverAnnotations.map((annotation) => markAnnotationSynced(annotation, sessionId)));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const synced = await Promise.allSettled(
|
|
731
|
+
localToMerge.map((annotation) =>
|
|
732
|
+
syncAnnotationToServer(endpoint, sessionId, {
|
|
733
|
+
...annotation,
|
|
734
|
+
sessionId,
|
|
735
|
+
url: pageUrl(),
|
|
736
|
+
}),
|
|
737
|
+
),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const merged = [
|
|
741
|
+
...serverAnnotations,
|
|
742
|
+
...synced.map((result, index) =>
|
|
743
|
+
result.status === 'fulfilled'
|
|
744
|
+
? (normalizeRemoteAnnotation(result.value, localToMerge[index]) ?? localToMerge[index]!)
|
|
745
|
+
: localToMerge[index]!,
|
|
746
|
+
),
|
|
747
|
+
]
|
|
748
|
+
.filter(isRenderableAnnotation)
|
|
749
|
+
.map((annotation) => markAnnotationSynced(annotation, sessionId));
|
|
750
|
+
|
|
751
|
+
persistAnnotations(merged);
|
|
752
|
+
saveAnnotationsWithSyncMarker(pathname, merged, sessionId);
|
|
753
|
+
} catch {
|
|
754
|
+
connectionStatus = 'disconnected';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function initializeSession() {
|
|
759
|
+
if (!endpoint) return;
|
|
760
|
+
|
|
761
|
+
connectionStatus = 'connecting';
|
|
762
|
+
const joinedSessionId = providedSessionId ?? loadSessionId(pathname);
|
|
763
|
+
|
|
764
|
+
if (joinedSessionId) {
|
|
765
|
+
try {
|
|
766
|
+
const session = await getSession(endpoint, joinedSessionId);
|
|
767
|
+
const serverAnnotations = normalizeRemoteAnnotations(session.annotations);
|
|
768
|
+
currentSessionId = session.id;
|
|
769
|
+
connectionStatus = 'connected';
|
|
770
|
+
saveSessionId(pathname, session.id);
|
|
771
|
+
|
|
772
|
+
if (!useExternal) {
|
|
773
|
+
const localToMerge = loadAnnotations<Annotation>(pathname).filter(
|
|
774
|
+
(annotation) => !serverAnnotations.some((serverAnnotation) => serverAnnotation.id === annotation.id),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
if (localToMerge.length > 0) {
|
|
778
|
+
const synced = await Promise.allSettled(
|
|
779
|
+
localToMerge.map((annotation) =>
|
|
780
|
+
syncAnnotationToServer(endpoint, session.id, {
|
|
781
|
+
...annotation,
|
|
782
|
+
sessionId: session.id,
|
|
783
|
+
url: pageUrl(),
|
|
784
|
+
}),
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
const merged = [
|
|
789
|
+
...serverAnnotations,
|
|
790
|
+
...synced.map((result, index) =>
|
|
791
|
+
result.status === 'fulfilled'
|
|
792
|
+
? (normalizeRemoteAnnotation(result.value, localToMerge[index]) ?? localToMerge[index]!)
|
|
793
|
+
: localToMerge[index]!,
|
|
794
|
+
),
|
|
795
|
+
]
|
|
796
|
+
.filter(isRenderableAnnotation)
|
|
797
|
+
.map((annotation) => markAnnotationSynced(annotation, session.id));
|
|
798
|
+
|
|
799
|
+
persistAnnotations(merged);
|
|
800
|
+
saveAnnotationsWithSyncMarker(pathname, merged, session.id);
|
|
801
|
+
} else {
|
|
802
|
+
const syncedAnnotations = serverAnnotations
|
|
803
|
+
.filter(isRenderableAnnotation)
|
|
804
|
+
.map((annotation) => markAnnotationSynced(annotation, session.id));
|
|
805
|
+
persistAnnotations(syncedAnnotations);
|
|
806
|
+
saveAnnotationsWithSyncMarker(pathname, syncedAnnotations, session.id);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return;
|
|
811
|
+
} catch {
|
|
812
|
+
clearSessionId(pathname);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const sessionId = await ensureSession(true);
|
|
817
|
+
if (!sessionId || useExternal) return;
|
|
818
|
+
|
|
819
|
+
const allAnnotations = loadAllAnnotations<Annotation>();
|
|
820
|
+
for (const [pagePath, pageAnnotations] of allAnnotations) {
|
|
821
|
+
const unsynced = pageAnnotations.filter(
|
|
822
|
+
(annotation) => annotation._syncedTo !== sessionId && isRenderableAnnotation(annotation),
|
|
823
|
+
);
|
|
824
|
+
if (unsynced.length === 0) continue;
|
|
825
|
+
|
|
826
|
+
const targetSessionId =
|
|
827
|
+
pagePath === pathname
|
|
828
|
+
? sessionId
|
|
829
|
+
: (await createSession(endpoint, pageUrl(pagePath)).then((session) => session.id).catch(() => null));
|
|
830
|
+
|
|
831
|
+
if (!targetSessionId) continue;
|
|
832
|
+
|
|
833
|
+
const synced = await Promise.allSettled(
|
|
834
|
+
unsynced.map((annotation) =>
|
|
835
|
+
syncAnnotationToServer(endpoint, targetSessionId, {
|
|
836
|
+
...annotation,
|
|
837
|
+
sessionId: targetSessionId,
|
|
838
|
+
url: pageUrl(pagePath),
|
|
839
|
+
}),
|
|
840
|
+
),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const nextAnnotations = synced
|
|
844
|
+
.map((result, index) => (result.status === 'fulfilled' ? result.value : unsynced[index]!))
|
|
845
|
+
.filter(isRenderableAnnotation);
|
|
846
|
+
|
|
847
|
+
saveAnnotationsWithSyncMarker(pagePath, nextAnnotations, targetSessionId);
|
|
848
|
+
if (pagePath === pathname) {
|
|
849
|
+
persistAnnotations(nextAnnotations.map((annotation) => markAnnotationSynced(annotation, targetSessionId)));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function buildPlacementShadowComment(placement: DesignPlacement): string {
|
|
855
|
+
const text = placement.text ? ` — "${placement.text}"` : '';
|
|
856
|
+
const note = placement.note ? ` Note: ${placement.note}.` : '';
|
|
857
|
+
return `Place ${placement.type} at (${Math.round(placement.x)}, ${Math.round(placement.y)}), ${Math.round(placement.width)}x${Math.round(placement.height)}px${text}.${note}`.trim();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function buildPlacementShadowAnnotation(placement: DesignPlacement, sessionId: string): Annotation {
|
|
861
|
+
return {
|
|
862
|
+
id: placement.id,
|
|
863
|
+
x: (placement.x / window.innerWidth) * 100,
|
|
864
|
+
y: placement.y,
|
|
865
|
+
isFixed: false,
|
|
866
|
+
timestamp: placement.timestamp,
|
|
867
|
+
comment: buildPlacementShadowComment(placement),
|
|
868
|
+
element: `[design:${placement.type}]`,
|
|
869
|
+
elementPath: '[placement]',
|
|
870
|
+
kind: 'placement',
|
|
871
|
+
color: annotationColor,
|
|
872
|
+
intent: 'change',
|
|
873
|
+
severity: 'important',
|
|
874
|
+
status: 'pending',
|
|
875
|
+
sessionId,
|
|
876
|
+
url: pageUrl(),
|
|
877
|
+
placement,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function buildRearrangeShadowComment(state: RearrangeState['sections'][number]): string {
|
|
882
|
+
const { originalRect, currentRect } = state;
|
|
883
|
+
const note = state.note ? ` Note: ${state.note}.` : '';
|
|
884
|
+
return `Move ${state.label} section (${state.tagName}) — from (${Math.round(originalRect.x)}, ${Math.round(originalRect.y)}) ${Math.round(originalRect.width)}x${Math.round(originalRect.height)} to (${Math.round(currentRect.x)}, ${Math.round(currentRect.y)}) ${Math.round(currentRect.width)}x${Math.round(currentRect.height)}.${note}`.trim();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function buildRearrangeShadowAnnotation(section: RearrangeState['sections'][number], sessionId: string): Annotation {
|
|
888
|
+
return {
|
|
889
|
+
id: section.id,
|
|
890
|
+
x: (section.currentRect.x / window.innerWidth) * 100,
|
|
891
|
+
y: section.currentRect.y,
|
|
892
|
+
isFixed: section.isFixed ?? false,
|
|
893
|
+
timestamp: Date.now(),
|
|
894
|
+
comment: buildRearrangeShadowComment(section),
|
|
895
|
+
element: section.selector,
|
|
896
|
+
elementPath: '[rearrange]',
|
|
897
|
+
kind: 'rearrange',
|
|
898
|
+
color: annotationColor,
|
|
899
|
+
intent: 'change',
|
|
900
|
+
severity: 'important',
|
|
901
|
+
status: 'pending',
|
|
902
|
+
sessionId,
|
|
903
|
+
url: pageUrl(),
|
|
904
|
+
rearrange: {
|
|
905
|
+
selector: section.selector,
|
|
906
|
+
label: section.label,
|
|
907
|
+
tagName: section.tagName,
|
|
908
|
+
originalRect: section.originalRect,
|
|
909
|
+
currentRect: section.currentRect,
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function placementSnapshot(placement: DesignPlacement): string {
|
|
915
|
+
return JSON.stringify({
|
|
916
|
+
type: placement.type,
|
|
917
|
+
x: Math.round(placement.x),
|
|
918
|
+
y: Math.round(placement.y),
|
|
919
|
+
width: Math.round(placement.width),
|
|
920
|
+
height: Math.round(placement.height),
|
|
921
|
+
text: placement.text ?? null,
|
|
922
|
+
note: placement.note ?? null,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function rearrangeSnapshot(section: RearrangeState['sections'][number]): string {
|
|
927
|
+
return JSON.stringify({
|
|
928
|
+
x: Math.round(section.currentRect.x),
|
|
929
|
+
y: Math.round(section.currentRect.y),
|
|
930
|
+
width: Math.round(section.currentRect.width),
|
|
931
|
+
height: Math.round(section.currentRect.height),
|
|
932
|
+
note: section.note ?? null,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function deleteRemoteAnnotation(id: string) {
|
|
937
|
+
if (!endpoint) return;
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
await deleteAnnotationOnServer(endpoint, id);
|
|
941
|
+
} catch {
|
|
942
|
+
connectionStatus = 'disconnected';
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function clearRemoteShadowAnnotations() {
|
|
947
|
+
const ids = [
|
|
948
|
+
...Array.from(placementShadowIds.values()),
|
|
949
|
+
...Array.from(rearrangeShadowIds.values()),
|
|
950
|
+
].filter(Boolean);
|
|
951
|
+
|
|
952
|
+
placementShadowIds.clear();
|
|
953
|
+
placementShadowSnapshots.clear();
|
|
954
|
+
rearrangeShadowIds.clear();
|
|
955
|
+
rearrangeShadowSnapshots.clear();
|
|
956
|
+
|
|
957
|
+
await Promise.allSettled(ids.map((id) => deleteRemoteAnnotation(id)));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function restoreMovedSection(id: string) {
|
|
961
|
+
const entry = movedSections.get(id);
|
|
962
|
+
if (!entry) return;
|
|
963
|
+
|
|
964
|
+
const { el, originalStyles, ancestors } = entry;
|
|
965
|
+
el.style.transition = 'transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1)';
|
|
966
|
+
el.style.transform = originalStyles.transform;
|
|
967
|
+
el.style.transformOrigin = originalStyles.transformOrigin;
|
|
968
|
+
el.style.opacity = originalStyles.opacity;
|
|
969
|
+
el.style.position = originalStyles.position;
|
|
970
|
+
el.style.zIndex = originalStyles.zIndex;
|
|
971
|
+
movedSections.delete(id);
|
|
972
|
+
|
|
973
|
+
window.setTimeout(() => {
|
|
974
|
+
el.style.transition = originalStyles.transition;
|
|
975
|
+
el.style.display = originalStyles.display;
|
|
976
|
+
for (const ancestor of ancestors) {
|
|
977
|
+
ancestor.el.style.overflow = ancestor.overflow;
|
|
978
|
+
ancestor.el.style.overflowX = ancestor.overflowX;
|
|
979
|
+
ancestor.el.style.overflowY = ancestor.overflowY;
|
|
980
|
+
}
|
|
981
|
+
}, 450);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function currentWireframeState(): WireframeState {
|
|
985
|
+
return {
|
|
986
|
+
rearrange: rearrangeState,
|
|
987
|
+
placements: designPlacements,
|
|
988
|
+
purpose: canvasPurpose,
|
|
989
|
+
prompt: wireframePrompt,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function setToolbarHidden(next: boolean) {
|
|
994
|
+
toolbarHidden = next;
|
|
995
|
+
saveToolbarHidden(next);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function hideToolbarUntilRestart() {
|
|
999
|
+
setToolbarHidden(true);
|
|
1000
|
+
active = false;
|
|
1001
|
+
handlePopupCancel();
|
|
1002
|
+
closeLayoutMode();
|
|
1003
|
+
clearPointerState();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function handleSettingsChange(next: FeedbackSettings) {
|
|
1007
|
+
settings = next;
|
|
1008
|
+
saveSettings(next);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function persistLayoutState() {
|
|
1012
|
+
if (blankCanvas) {
|
|
1013
|
+
const hasWireframeContent =
|
|
1014
|
+
designPlacements.length > 0 ||
|
|
1015
|
+
(rearrangeState?.sections.length ?? 0) > 0 ||
|
|
1016
|
+
wireframePrompt.trim().length > 0;
|
|
1017
|
+
|
|
1018
|
+
if (hasWireframeContent) {
|
|
1019
|
+
saveWireframeState(pathname, currentWireframeState());
|
|
1020
|
+
} else {
|
|
1021
|
+
clearWireframeState(pathname);
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (designPlacements.length > 0) {
|
|
1027
|
+
saveDesignPlacements(pathname, designPlacements);
|
|
1028
|
+
} else {
|
|
1029
|
+
clearDesignPlacements(pathname);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (rearrangeState && rearrangeState.sections.length > 0) {
|
|
1033
|
+
saveRearrangeState(pathname, rearrangeState);
|
|
1034
|
+
} else {
|
|
1035
|
+
clearRearrangeState(pathname);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function setDesignPlacements(next: DesignPlacement[]) {
|
|
1040
|
+
designPlacements = next;
|
|
1041
|
+
persistLayoutState();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function setRearrangeLayout(next: RearrangeState | null) {
|
|
1045
|
+
rearrangeState = next;
|
|
1046
|
+
persistLayoutState();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function togglePause() {
|
|
1050
|
+
paused = !paused;
|
|
1051
|
+
if (paused) {
|
|
1052
|
+
freezeAnimations();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
unfreezeAnimations();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function toggleMarkers() {
|
|
1059
|
+
if (annotations.length === 0 || layoutMode !== 'idle') return;
|
|
1060
|
+
showMarkers = !showMarkers;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function resetLayoutSelections() {
|
|
1064
|
+
resetCrossDragState();
|
|
1065
|
+
designSelectedIds = [];
|
|
1066
|
+
rearrangeSelectedIds = [];
|
|
1067
|
+
designSelectionCount = 0;
|
|
1068
|
+
rearrangeSelectionCount = 0;
|
|
1069
|
+
designDeselectSignal += 1;
|
|
1070
|
+
rearrangeDeselectSignal += 1;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function setLayoutMode(next: FeedbackLayoutMode) {
|
|
1074
|
+
if (next === layoutMode) return;
|
|
1075
|
+
|
|
1076
|
+
if (next === 'rearrange') {
|
|
1077
|
+
ensureRearrangeState();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
layoutMode = next;
|
|
1081
|
+
if (next === 'idle') {
|
|
1082
|
+
paletteOpen = false;
|
|
1083
|
+
overlayInteracting = false;
|
|
1084
|
+
activeComponent = null;
|
|
1085
|
+
resetLayoutSelections();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
paletteOpen = true;
|
|
1090
|
+
overlayInteracting = false;
|
|
1091
|
+
activeComponent = null;
|
|
1092
|
+
resetLayoutSelections();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function closeLayoutMode() {
|
|
1096
|
+
setLayoutMode('idle');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function handleCanvasPurposeChange(next: CanvasPurpose) {
|
|
1100
|
+
if (next === canvasPurpose) return;
|
|
1101
|
+
|
|
1102
|
+
resetLayoutSelections();
|
|
1103
|
+
|
|
1104
|
+
if (next === 'new-page') {
|
|
1105
|
+
exploreStash = {
|
|
1106
|
+
rearrange: rearrangeState,
|
|
1107
|
+
placements: designPlacements,
|
|
1108
|
+
purpose: 'replace-current',
|
|
1109
|
+
prompt: '',
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const stored = loadWireframeState(pathname);
|
|
1113
|
+
designPlacements = stored?.placements ?? [];
|
|
1114
|
+
rearrangeState = stored?.rearrange ?? EMPTY_REARRANGE_STATE();
|
|
1115
|
+
wireframePrompt = stored?.prompt ?? '';
|
|
1116
|
+
canvasPurpose = 'new-page';
|
|
1117
|
+
activeComponent = null;
|
|
1118
|
+
persistLayoutState();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
saveWireframeState(pathname, currentWireframeState());
|
|
1123
|
+
designPlacements = exploreStash.placements;
|
|
1124
|
+
rearrangeState = exploreStash.rearrange;
|
|
1125
|
+
wireframePrompt = exploreStash.prompt;
|
|
1126
|
+
canvasPurpose = 'replace-current';
|
|
1127
|
+
activeComponent = null;
|
|
1128
|
+
persistLayoutState();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function ensureRearrangeState(): RearrangeState {
|
|
1132
|
+
if (rearrangeState) return rearrangeState;
|
|
1133
|
+
|
|
1134
|
+
const nextState = EMPTY_REARRANGE_STATE();
|
|
1135
|
+
setRearrangeLayout(nextState);
|
|
1136
|
+
return nextState;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function clearLayoutContent() {
|
|
1140
|
+
resetCrossDragState();
|
|
1141
|
+
designPlacements = [];
|
|
1142
|
+
activeComponent = null;
|
|
1143
|
+
designSelectedIds = [];
|
|
1144
|
+
rearrangeSelectedIds = [];
|
|
1145
|
+
designSelectionCount = 0;
|
|
1146
|
+
rearrangeSelectionCount = 0;
|
|
1147
|
+
designClearSignal += 1;
|
|
1148
|
+
rearrangeClearSignal += 1;
|
|
1149
|
+
designDeselectSignal += 1;
|
|
1150
|
+
rearrangeDeselectSignal += 1;
|
|
1151
|
+
|
|
1152
|
+
if (blankCanvas) {
|
|
1153
|
+
rearrangeState = EMPTY_REARRANGE_STATE();
|
|
1154
|
+
wireframePrompt = '';
|
|
1155
|
+
clearWireframeState(pathname);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
rearrangeState = null;
|
|
1160
|
+
clearDesignPlacements(pathname);
|
|
1161
|
+
clearRearrangeState(pathname);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function clearAllFeedback() {
|
|
1165
|
+
const clearedAnnotations = [...annotations];
|
|
1166
|
+
persistAnnotations([]);
|
|
1167
|
+
clearLayoutContent();
|
|
1168
|
+
if (endpoint) {
|
|
1169
|
+
clearedAnnotations.forEach((annotation) => {
|
|
1170
|
+
void deleteAnnotationOnServer(endpoint, annotation.id).catch(() => {
|
|
1171
|
+
connectionStatus = 'disconnected';
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
void clearRemoteShadowAnnotations();
|
|
1175
|
+
}
|
|
1176
|
+
void fireWebhook('annotations.clear', { annotations: clearedAnnotations });
|
|
1177
|
+
onAnnotationsClear?.(clearedAnnotations);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function buildOutput(mode: 'copy' | 'submit' = 'copy'): string {
|
|
1181
|
+
return generateOutput(mode === 'copy' && blankCanvas ? [] : annotations, displayPath(), outputDetail, {
|
|
1182
|
+
designPlacements,
|
|
1183
|
+
rearrangeState,
|
|
1184
|
+
blankCanvas,
|
|
1185
|
+
wireframePurpose: wireframePrompt || undefined,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function clearDesignPreviewTransforms() {
|
|
1190
|
+
for (const id of designSelectedIds) {
|
|
1191
|
+
const el = document.querySelector(`[data-placement-id="${id}"]`);
|
|
1192
|
+
if (el instanceof HTMLElement) {
|
|
1193
|
+
el.style.transform = '';
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function clearRearrangePreviewTransforms() {
|
|
1199
|
+
for (const id of rearrangeSelectedIds) {
|
|
1200
|
+
const el = document.querySelector(`[data-rearrange-section="${id}"]`);
|
|
1201
|
+
if (el instanceof HTMLElement) {
|
|
1202
|
+
el.style.transform = '';
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function resetCrossDragState() {
|
|
1208
|
+
clearDesignPreviewTransforms();
|
|
1209
|
+
clearRearrangePreviewTransforms();
|
|
1210
|
+
crossDesignDragStart = null;
|
|
1211
|
+
crossRearrangeDragStart = null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function previewRearrangeSelection(dx: number, dy: number) {
|
|
1215
|
+
if (rearrangeSelectedIds.length === 0 || !rearrangeState) return;
|
|
1216
|
+
|
|
1217
|
+
if (!crossDesignDragStart) {
|
|
1218
|
+
crossDesignDragStart = new Map();
|
|
1219
|
+
for (const section of rearrangeState.sections) {
|
|
1220
|
+
if (rearrangeSelectedIds.includes(section.id)) {
|
|
1221
|
+
crossDesignDragStart.set(section.id, {
|
|
1222
|
+
x: section.currentRect.x,
|
|
1223
|
+
y: section.currentRect.y,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
for (const id of rearrangeSelectedIds) {
|
|
1230
|
+
const el = document.querySelector(`[data-rearrange-section="${id}"]`);
|
|
1231
|
+
if (el instanceof HTMLElement) {
|
|
1232
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function commitRearrangeSelection(dx: number, dy: number, committed: boolean) {
|
|
1238
|
+
const starts = crossDesignDragStart;
|
|
1239
|
+
clearRearrangePreviewTransforms();
|
|
1240
|
+
crossDesignDragStart = null;
|
|
1241
|
+
|
|
1242
|
+
if (!committed || !starts || !rearrangeState) return;
|
|
1243
|
+
|
|
1244
|
+
setRearrangeLayout({
|
|
1245
|
+
...rearrangeState,
|
|
1246
|
+
sections: rearrangeState.sections.map((section) => {
|
|
1247
|
+
const start = starts.get(section.id);
|
|
1248
|
+
if (!start) return section;
|
|
1249
|
+
return {
|
|
1250
|
+
...section,
|
|
1251
|
+
currentRect: {
|
|
1252
|
+
...section.currentRect,
|
|
1253
|
+
x: Math.max(0, start.x + dx),
|
|
1254
|
+
y: Math.max(0, start.y + dy),
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
}),
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function previewDesignSelection(dx: number, dy: number) {
|
|
1262
|
+
if (designSelectedIds.length === 0) return;
|
|
1263
|
+
|
|
1264
|
+
if (!crossRearrangeDragStart) {
|
|
1265
|
+
crossRearrangeDragStart = new Map();
|
|
1266
|
+
for (const placement of designPlacements) {
|
|
1267
|
+
if (designSelectedIds.includes(placement.id)) {
|
|
1268
|
+
crossRearrangeDragStart.set(placement.id, {
|
|
1269
|
+
x: placement.x,
|
|
1270
|
+
y: placement.y,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
for (const id of designSelectedIds) {
|
|
1277
|
+
const el = document.querySelector(`[data-placement-id="${id}"]`);
|
|
1278
|
+
if (el instanceof HTMLElement) {
|
|
1279
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function commitDesignSelection(dx: number, dy: number, committed: boolean) {
|
|
1285
|
+
const starts = crossRearrangeDragStart;
|
|
1286
|
+
clearDesignPreviewTransforms();
|
|
1287
|
+
crossRearrangeDragStart = null;
|
|
1288
|
+
|
|
1289
|
+
if (!committed || !starts) return;
|
|
1290
|
+
|
|
1291
|
+
setDesignPlacements(
|
|
1292
|
+
designPlacements.map((placement) => {
|
|
1293
|
+
const start = starts.get(placement.id);
|
|
1294
|
+
if (!start) return placement;
|
|
1295
|
+
return {
|
|
1296
|
+
...placement,
|
|
1297
|
+
x: Math.max(0, start.x + dx),
|
|
1298
|
+
y: Math.max(0, start.y + dy),
|
|
1299
|
+
};
|
|
1300
|
+
}),
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function handleDetectSections() {
|
|
1305
|
+
const existing = rearrangeState?.sections ?? [];
|
|
1306
|
+
const existingSelectors = new Set(existing.map((section) => section.selector));
|
|
1307
|
+
const newSections = detectPageSections().filter((section) => !existingSelectors.has(section.selector));
|
|
1308
|
+
|
|
1309
|
+
setRearrangeLayout({
|
|
1310
|
+
sections: [...existing, ...newSections],
|
|
1311
|
+
originalOrder: [...(rearrangeState?.originalOrder ?? []), ...newSections.map((section) => section.id)],
|
|
1312
|
+
detectedAt: Date.now(),
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function handlePaletteDragStart(type: LayoutModeComponentType, event: MouseEvent) {
|
|
1317
|
+
if (event.button !== 0) return;
|
|
1318
|
+
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
const size = DEFAULT_SIZES[type];
|
|
1321
|
+
let preview: HTMLDivElement | null = null;
|
|
1322
|
+
let didDrag = false;
|
|
1323
|
+
const startX = event.clientX;
|
|
1324
|
+
const startY = event.clientY;
|
|
1325
|
+
const toolbar = (event.target as HTMLElement).closest('[data-feedback-toolbar]');
|
|
1326
|
+
const toolbarTop = toolbar?.getBoundingClientRect().top ?? window.innerHeight;
|
|
1327
|
+
|
|
1328
|
+
const handleMove = (moveEvent: MouseEvent) => {
|
|
1329
|
+
const deltaX = moveEvent.clientX - startX;
|
|
1330
|
+
const deltaY = moveEvent.clientY - startY;
|
|
1331
|
+
|
|
1332
|
+
if (!didDrag && (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)) {
|
|
1333
|
+
didDrag = true;
|
|
1334
|
+
preview = document.createElement('div');
|
|
1335
|
+
const accent = blankCanvas ? '#f97316' : 'var(--dry-color-fill-brand, #7c3aed)';
|
|
1336
|
+
preview.style.position = 'fixed';
|
|
1337
|
+
preview.style.borderRadius = '12px';
|
|
1338
|
+
preview.style.border = `1px solid ${accent}`;
|
|
1339
|
+
preview.style.background = `color-mix(in srgb, ${accent} 8%, transparent)`;
|
|
1340
|
+
preview.style.pointerEvents = 'none';
|
|
1341
|
+
preview.style.zIndex = '1006';
|
|
1342
|
+
preview.style.display = 'flex';
|
|
1343
|
+
preview.style.alignItems = 'center';
|
|
1344
|
+
preview.style.justifyContent = 'center';
|
|
1345
|
+
preview.style.padding = '8px';
|
|
1346
|
+
preview.style.color = 'var(--dry-color-text-strong, #111)';
|
|
1347
|
+
preview.style.fontSize = '12px';
|
|
1348
|
+
preview.style.fontWeight = '600';
|
|
1349
|
+
preview.dataset.layoutDragPreview = type;
|
|
1350
|
+
document.body.appendChild(preview);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (!preview) return;
|
|
1354
|
+
|
|
1355
|
+
const dist = Math.max(0, toolbarTop - moveEvent.clientY);
|
|
1356
|
+
const progress = Math.min(1, dist / 180);
|
|
1357
|
+
const eased = 1 - Math.pow(1 - progress, 2);
|
|
1358
|
+
|
|
1359
|
+
const minWidth = 28;
|
|
1360
|
+
const minHeight = 20;
|
|
1361
|
+
const maxWidth = Math.min(140, size.width * 0.18);
|
|
1362
|
+
const maxHeight = Math.min(90, size.height * 0.18);
|
|
1363
|
+
const width = minWidth + (maxWidth - minWidth) * eased;
|
|
1364
|
+
const height = minHeight + (maxHeight - minHeight) * eased;
|
|
1365
|
+
|
|
1366
|
+
preview.style.width = `${width}px`;
|
|
1367
|
+
preview.style.height = `${height}px`;
|
|
1368
|
+
preview.style.left = `${moveEvent.clientX - width / 2}px`;
|
|
1369
|
+
preview.style.top = `${moveEvent.clientY - height / 2}px`;
|
|
1370
|
+
preview.style.opacity = `${0.5 + 0.5 * eased}`;
|
|
1371
|
+
preview.textContent = eased > 0.25 ? type : '';
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
const handleUp = (moveEvent: MouseEvent) => {
|
|
1375
|
+
window.removeEventListener('mousemove', handleMove);
|
|
1376
|
+
window.removeEventListener('mouseup', handleUp);
|
|
1377
|
+
preview?.remove();
|
|
1378
|
+
|
|
1379
|
+
if (!didDrag) return;
|
|
1380
|
+
|
|
1381
|
+
const placement: DesignPlacement = {
|
|
1382
|
+
id: `dp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
1383
|
+
type,
|
|
1384
|
+
x: Math.max(0, moveEvent.clientX - size.width / 2),
|
|
1385
|
+
y: Math.max(0, moveEvent.clientY - size.height / 2) + window.scrollY,
|
|
1386
|
+
width: size.width,
|
|
1387
|
+
height: size.height,
|
|
1388
|
+
scrollY: window.scrollY,
|
|
1389
|
+
timestamp: Date.now(),
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
setDesignPlacements([...designPlacements, placement]);
|
|
1393
|
+
activeComponent = null;
|
|
1394
|
+
designSelectedIds = [];
|
|
1395
|
+
designSelectionCount = 0;
|
|
1396
|
+
rearrangeSelectedIds = [];
|
|
1397
|
+
rearrangeSelectionCount = 0;
|
|
1398
|
+
rearrangeDeselectSignal += 1;
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
window.addEventListener('mousemove', handleMove);
|
|
1402
|
+
window.addEventListener('mouseup', handleUp);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function toggleLayoutMode() {
|
|
1406
|
+
if (!enableDesignMode) return;
|
|
1407
|
+
if (layoutMode !== 'idle') {
|
|
1408
|
+
closeLayoutMode();
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
setLayoutMode('design');
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function handleToolbarLayoutToggle() {
|
|
1416
|
+
if (showPopup || pendingMultiSelectTargets.length > 0) {
|
|
1417
|
+
handlePopupCancel();
|
|
1418
|
+
}
|
|
1419
|
+
toggleLayoutMode();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function handleWireframePromptChange(next: string) {
|
|
1423
|
+
wireframePrompt = next;
|
|
1424
|
+
persistLayoutState();
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function toggleRearrangeMode() {
|
|
1428
|
+
if (!enableRearrange) return;
|
|
1429
|
+
if (layoutMode === 'rearrange') {
|
|
1430
|
+
setLayoutMode(enableDesignMode ? 'design' : 'idle');
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
setLayoutMode('rearrange');
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
$effect(() => {
|
|
1438
|
+
if (providedSessionId && providedSessionId !== currentSessionId) {
|
|
1439
|
+
currentSessionId = providedSessionId;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (!endpoint) {
|
|
1443
|
+
connectionStatus = 'disconnected';
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
$effect(() => {
|
|
1449
|
+
if (!endpoint || sessionBootstrapRequested) return;
|
|
1450
|
+
connectionStatus = currentSessionId ? 'connected' : 'connecting';
|
|
1451
|
+
sessionBootstrapRequested = true;
|
|
1452
|
+
void initializeSession();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
$effect(() => {
|
|
1456
|
+
if (!endpoint) {
|
|
1457
|
+
connectionStatus = 'disconnected';
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
let cancelled = false;
|
|
1462
|
+
|
|
1463
|
+
async function checkHealth() {
|
|
1464
|
+
try {
|
|
1465
|
+
const response = await fetch(`${endpoint}/health`);
|
|
1466
|
+
if (cancelled) return;
|
|
1467
|
+
connectionStatus = response.ok ? 'connected' : 'disconnected';
|
|
1468
|
+
} catch {
|
|
1469
|
+
if (cancelled) return;
|
|
1470
|
+
connectionStatus = 'disconnected';
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
void checkHealth();
|
|
1475
|
+
const interval = window.setInterval(() => {
|
|
1476
|
+
void checkHealth();
|
|
1477
|
+
}, CONNECTION_POLL_MS);
|
|
1478
|
+
|
|
1479
|
+
return () => {
|
|
1480
|
+
cancelled = true;
|
|
1481
|
+
window.clearInterval(interval);
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
$effect(() => {
|
|
1486
|
+
if (!endpoint || !currentSessionId || typeof EventSource === 'undefined') return;
|
|
1487
|
+
|
|
1488
|
+
const source = new EventSource(`${endpoint}/sessions/${currentSessionId}/events`);
|
|
1489
|
+
const handleEvent = (event: MessageEvent<string>) => {
|
|
1490
|
+
const payloads = parseRemoteEventData(event.data);
|
|
1491
|
+
if (payloads.length === 0) return;
|
|
1492
|
+
|
|
1493
|
+
let nextAnnotations = [...annotations];
|
|
1494
|
+
let changed = false;
|
|
1495
|
+
|
|
1496
|
+
for (const payload of payloads) {
|
|
1497
|
+
const existing = nextAnnotations.find((annotation) => annotation.id === payload.id);
|
|
1498
|
+
const normalized = normalizeRemoteAnnotation(payload, existing);
|
|
1499
|
+
if (!normalized) continue;
|
|
1500
|
+
|
|
1501
|
+
if (normalized.kind === 'placement' && normalized.status && !isRenderableAnnotation(normalized)) {
|
|
1502
|
+
placementShadowIds.delete(normalized.id);
|
|
1503
|
+
placementShadowSnapshots.delete(normalized.id);
|
|
1504
|
+
setDesignPlacements(designPlacements.filter((placement) => placement.id !== normalized.id));
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (normalized.kind === 'rearrange' && normalized.status && !isRenderableAnnotation(normalized)) {
|
|
1509
|
+
rearrangeShadowIds.delete(normalized.id);
|
|
1510
|
+
rearrangeShadowSnapshots.delete(normalized.id);
|
|
1511
|
+
if (rearrangeState) {
|
|
1512
|
+
setRearrangeLayout({
|
|
1513
|
+
...rearrangeState,
|
|
1514
|
+
sections: rearrangeState.sections.filter((section) => section.id !== normalized.id),
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (normalized.status && !isRenderableAnnotation(normalized)) {
|
|
1521
|
+
nextAnnotations = nextAnnotations.filter((annotation) => annotation.id !== normalized.id);
|
|
1522
|
+
changed = true;
|
|
1523
|
+
if (editingAnnotation?.id === normalized.id) {
|
|
1524
|
+
editingAnnotation = null;
|
|
1525
|
+
}
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const synced = currentSessionId ? markAnnotationSynced(normalized, currentSessionId) : normalized;
|
|
1530
|
+
const index = nextAnnotations.findIndex((annotation) => annotation.id === synced.id);
|
|
1531
|
+
if (index === -1) {
|
|
1532
|
+
nextAnnotations = [...nextAnnotations, synced];
|
|
1533
|
+
} else {
|
|
1534
|
+
nextAnnotations[index] = synced;
|
|
1535
|
+
}
|
|
1536
|
+
if (editingAnnotation?.id === synced.id) {
|
|
1537
|
+
editingAnnotation = synced;
|
|
1538
|
+
}
|
|
1539
|
+
changed = true;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (changed) {
|
|
1543
|
+
persistAnnotations(nextAnnotations);
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
const lifecycleEvents = [
|
|
1548
|
+
'annotation:created',
|
|
1549
|
+
'annotation.created',
|
|
1550
|
+
'annotation:updated',
|
|
1551
|
+
'annotation.updated',
|
|
1552
|
+
'annotation:acknowledged',
|
|
1553
|
+
'annotation:resolved',
|
|
1554
|
+
'annotation:dismissed',
|
|
1555
|
+
'annotation:batch',
|
|
1556
|
+
];
|
|
1557
|
+
|
|
1558
|
+
for (const eventName of lifecycleEvents) {
|
|
1559
|
+
source.addEventListener(eventName, handleEvent as EventListener);
|
|
1560
|
+
}
|
|
1561
|
+
source.onerror = () => {
|
|
1562
|
+
connectionStatus = 'disconnected';
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
return () => {
|
|
1566
|
+
for (const eventName of lifecycleEvents) {
|
|
1567
|
+
source.removeEventListener(eventName, handleEvent as EventListener);
|
|
1568
|
+
}
|
|
1569
|
+
source.close();
|
|
1570
|
+
};
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
$effect(() => {
|
|
1574
|
+
if (!endpoint) {
|
|
1575
|
+
previousConnectionStatus = connectionStatus;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const shouldResync = previousConnectionStatus === 'disconnected' && connectionStatus === 'connected';
|
|
1580
|
+
previousConnectionStatus = connectionStatus;
|
|
1581
|
+
|
|
1582
|
+
if (shouldResync) {
|
|
1583
|
+
void syncExistingLocalAnnotations();
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
$effect(() => {
|
|
1588
|
+
if (!endpoint) return;
|
|
1589
|
+
|
|
1590
|
+
let cancelled = false;
|
|
1591
|
+
|
|
1592
|
+
void (async () => {
|
|
1593
|
+
const sessionId = await ensureSession();
|
|
1594
|
+
if (!sessionId || cancelled) return;
|
|
1595
|
+
|
|
1596
|
+
const currentIds = new Set(designPlacements.map((placement) => placement.id));
|
|
1597
|
+
for (const placement of designPlacements) {
|
|
1598
|
+
const snapshot = placementSnapshot(placement);
|
|
1599
|
+
if (placementShadowSnapshots.get(placement.id) === snapshot && placementShadowIds.get(placement.id)) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const existingAnnotationId = placementShadowIds.get(placement.id);
|
|
1604
|
+
const nextAnnotation = buildPlacementShadowAnnotation(placement, sessionId);
|
|
1605
|
+
placementShadowSnapshots.set(placement.id, snapshot);
|
|
1606
|
+
|
|
1607
|
+
if (existingAnnotationId) {
|
|
1608
|
+
void updateAnnotationOnServer(endpoint, existingAnnotationId, {
|
|
1609
|
+
comment: nextAnnotation.comment,
|
|
1610
|
+
placement: nextAnnotation.placement,
|
|
1611
|
+
x: nextAnnotation.x,
|
|
1612
|
+
y: nextAnnotation.y,
|
|
1613
|
+
}).catch(() => {
|
|
1614
|
+
connectionStatus = 'disconnected';
|
|
1615
|
+
});
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
placementShadowIds.set(placement.id, '');
|
|
1620
|
+
void syncAnnotationToServer(endpoint, sessionId, nextAnnotation)
|
|
1621
|
+
.then((serverAnnotation) => {
|
|
1622
|
+
if (cancelled) return;
|
|
1623
|
+
placementShadowIds.set(placement.id, serverAnnotation.id);
|
|
1624
|
+
})
|
|
1625
|
+
.catch(() => {
|
|
1626
|
+
if (cancelled) return;
|
|
1627
|
+
connectionStatus = 'disconnected';
|
|
1628
|
+
placementShadowIds.delete(placement.id);
|
|
1629
|
+
placementShadowSnapshots.delete(placement.id);
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
for (const [placementId, annotationId] of placementShadowIds) {
|
|
1634
|
+
if (currentIds.has(placementId)) continue;
|
|
1635
|
+
placementShadowIds.delete(placementId);
|
|
1636
|
+
placementShadowSnapshots.delete(placementId);
|
|
1637
|
+
if (annotationId) {
|
|
1638
|
+
void deleteRemoteAnnotation(annotationId);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
})();
|
|
1642
|
+
|
|
1643
|
+
return () => {
|
|
1644
|
+
cancelled = true;
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
$effect(() => {
|
|
1649
|
+
if (!endpoint) return;
|
|
1650
|
+
|
|
1651
|
+
let cancelled = false;
|
|
1652
|
+
|
|
1653
|
+
void (async () => {
|
|
1654
|
+
const sessionId = await ensureSession();
|
|
1655
|
+
if (!sessionId || cancelled) return;
|
|
1656
|
+
|
|
1657
|
+
const movedSectionsState = rearrangeState?.sections.filter((section) => {
|
|
1658
|
+
const original = section.originalRect;
|
|
1659
|
+
const current = section.currentRect;
|
|
1660
|
+
return (
|
|
1661
|
+
Math.abs(original.x - current.x) > 1 ||
|
|
1662
|
+
Math.abs(original.y - current.y) > 1 ||
|
|
1663
|
+
Math.abs(original.width - current.width) > 1 ||
|
|
1664
|
+
Math.abs(original.height - current.height) > 1
|
|
1665
|
+
);
|
|
1666
|
+
}) ?? [];
|
|
1667
|
+
|
|
1668
|
+
const currentIds = new Set(movedSectionsState.map((section) => section.id));
|
|
1669
|
+
for (const section of movedSectionsState) {
|
|
1670
|
+
const snapshot = rearrangeSnapshot(section);
|
|
1671
|
+
if (rearrangeShadowSnapshots.get(section.id) === snapshot && rearrangeShadowIds.get(section.id)) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const existingAnnotationId = rearrangeShadowIds.get(section.id);
|
|
1676
|
+
const nextAnnotation = buildRearrangeShadowAnnotation(section, sessionId);
|
|
1677
|
+
rearrangeShadowSnapshots.set(section.id, snapshot);
|
|
1678
|
+
|
|
1679
|
+
if (existingAnnotationId) {
|
|
1680
|
+
void updateAnnotationOnServer(endpoint, existingAnnotationId, {
|
|
1681
|
+
comment: nextAnnotation.comment,
|
|
1682
|
+
rearrange: nextAnnotation.rearrange,
|
|
1683
|
+
x: nextAnnotation.x,
|
|
1684
|
+
y: nextAnnotation.y,
|
|
1685
|
+
}).catch(() => {
|
|
1686
|
+
connectionStatus = 'disconnected';
|
|
1687
|
+
});
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
rearrangeShadowIds.set(section.id, '');
|
|
1692
|
+
void syncAnnotationToServer(endpoint, sessionId, nextAnnotation)
|
|
1693
|
+
.then((serverAnnotation) => {
|
|
1694
|
+
if (cancelled) return;
|
|
1695
|
+
rearrangeShadowIds.set(section.id, serverAnnotation.id);
|
|
1696
|
+
})
|
|
1697
|
+
.catch(() => {
|
|
1698
|
+
if (cancelled) return;
|
|
1699
|
+
connectionStatus = 'disconnected';
|
|
1700
|
+
rearrangeShadowIds.delete(section.id);
|
|
1701
|
+
rearrangeShadowSnapshots.delete(section.id);
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
for (const [sectionId, annotationId] of rearrangeShadowIds) {
|
|
1706
|
+
if (currentIds.has(sectionId)) continue;
|
|
1707
|
+
rearrangeShadowIds.delete(sectionId);
|
|
1708
|
+
rearrangeShadowSnapshots.delete(sectionId);
|
|
1709
|
+
if (annotationId) {
|
|
1710
|
+
void deleteRemoteAnnotation(annotationId);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
})();
|
|
1714
|
+
|
|
1715
|
+
return () => {
|
|
1716
|
+
cancelled = true;
|
|
1717
|
+
};
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
$effect(() => {
|
|
1721
|
+
if (typeof window === 'undefined') return;
|
|
1722
|
+
|
|
1723
|
+
const activeIds = new Set<string>();
|
|
1724
|
+
const activeSections = layoutActive ? rearrangeState?.sections ?? [] : [];
|
|
1725
|
+
|
|
1726
|
+
for (const section of activeSections) {
|
|
1727
|
+
activeIds.add(section.id);
|
|
1728
|
+
|
|
1729
|
+
try {
|
|
1730
|
+
const el = document.querySelector(section.selector);
|
|
1731
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
1732
|
+
|
|
1733
|
+
const dx = section.currentRect.x - section.originalRect.x;
|
|
1734
|
+
const dy = section.currentRect.y - section.originalRect.y;
|
|
1735
|
+
const scaleX =
|
|
1736
|
+
section.originalRect.width > 0 ? section.currentRect.width / section.originalRect.width : 1;
|
|
1737
|
+
const scaleY =
|
|
1738
|
+
section.originalRect.height > 0 ? section.currentRect.height / section.originalRect.height : 1;
|
|
1739
|
+
const transform = `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`;
|
|
1740
|
+
|
|
1741
|
+
if (!movedSections.has(section.id)) {
|
|
1742
|
+
const ancestors: MovedSectionEntry['ancestors'] = [];
|
|
1743
|
+
let parent = el.parentElement;
|
|
1744
|
+
while (parent && parent !== document.body) {
|
|
1745
|
+
const computed = window.getComputedStyle(parent);
|
|
1746
|
+
if (
|
|
1747
|
+
computed.overflow !== 'visible' ||
|
|
1748
|
+
computed.overflowX !== 'visible' ||
|
|
1749
|
+
computed.overflowY !== 'visible'
|
|
1750
|
+
) {
|
|
1751
|
+
ancestors.push({
|
|
1752
|
+
el: parent,
|
|
1753
|
+
overflow: parent.style.overflow,
|
|
1754
|
+
overflowX: parent.style.overflowX,
|
|
1755
|
+
overflowY: parent.style.overflowY,
|
|
1756
|
+
});
|
|
1757
|
+
parent.style.overflow = 'visible';
|
|
1758
|
+
parent.style.overflowX = 'visible';
|
|
1759
|
+
parent.style.overflowY = 'visible';
|
|
1760
|
+
}
|
|
1761
|
+
parent = parent.parentElement;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const computed = window.getComputedStyle(el);
|
|
1765
|
+
if (computed.display === 'inline') {
|
|
1766
|
+
el.style.display = 'inline-block';
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
movedSections.set(section.id, {
|
|
1770
|
+
el,
|
|
1771
|
+
originalStyles: {
|
|
1772
|
+
transform: el.style.transform,
|
|
1773
|
+
transformOrigin: el.style.transformOrigin,
|
|
1774
|
+
opacity: el.style.opacity,
|
|
1775
|
+
position: el.style.position,
|
|
1776
|
+
zIndex: el.style.zIndex,
|
|
1777
|
+
display: el.style.display,
|
|
1778
|
+
transition: el.style.transition,
|
|
1779
|
+
},
|
|
1780
|
+
ancestors,
|
|
1781
|
+
});
|
|
1782
|
+
el.style.transformOrigin = 'top left';
|
|
1783
|
+
el.style.zIndex = '9999';
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
el.style.transition = overlayInteracting
|
|
1787
|
+
? 'none'
|
|
1788
|
+
: 'transform 0.2s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s cubic-bezier(0.22, 1, 0.36, 1)';
|
|
1789
|
+
el.style.transform = transform;
|
|
1790
|
+
} catch {
|
|
1791
|
+
// Ignore invalid selectors from captured sections.
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
for (const id of Array.from(movedSections.keys())) {
|
|
1796
|
+
if (!activeIds.has(id)) {
|
|
1797
|
+
restoreMovedSection(id);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
onDestroy(() => {
|
|
1803
|
+
resetCrossDragState();
|
|
1804
|
+
for (const id of Array.from(movedSections.keys())) {
|
|
1805
|
+
restoreMovedSection(id);
|
|
1806
|
+
}
|
|
1807
|
+
unfreezeAnimations();
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
function generateId(): string {
|
|
1811
|
+
return `fb-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function isFeedbackElement(value: EventTarget | null): boolean {
|
|
1815
|
+
return value instanceof Element && Boolean(value.closest('[data-dryui-feedback]'));
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function isPrimaryButton(event: MouseEvent): boolean {
|
|
1819
|
+
return event.button === 0;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function isMultiSelectModifier(event: MouseEvent): boolean {
|
|
1823
|
+
return (event.metaKey || event.ctrlKey) && !event.altKey;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function isElementFixed(el: Element): boolean {
|
|
1827
|
+
let current: Element | null = el;
|
|
1828
|
+
while (current instanceof HTMLElement) {
|
|
1829
|
+
const style = window.getComputedStyle(current);
|
|
1830
|
+
if (style.position === 'fixed' || style.position === 'sticky') {
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
current = current.parentElement;
|
|
1834
|
+
}
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function clearPointerState() {
|
|
1839
|
+
pointerDown = null;
|
|
1840
|
+
dragRect = null;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function clearMarkerHighlight() {
|
|
1844
|
+
markerHoverRect = null;
|
|
1845
|
+
markerHoverLabel = undefined;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function resetHighlights() {
|
|
1849
|
+
hoverRect = null;
|
|
1850
|
+
hoverLabel = undefined;
|
|
1851
|
+
clearMarkerHighlight();
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function clearSelection() {
|
|
1855
|
+
window.getSelection()?.removeAllRanges();
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function clearMultiSelect() {
|
|
1859
|
+
pendingMultiSelectTargets = [];
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function stopCapturedEvent(event: Event) {
|
|
1863
|
+
event.preventDefault();
|
|
1864
|
+
event.stopPropagation();
|
|
1865
|
+
event.stopImmediatePropagation();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function isBlockedInteractionTarget(target: EventTarget | null): boolean {
|
|
1869
|
+
return target instanceof Element && closestCrossingShadow(target, BLOCKED_INTERACTION_SELECTOR) !== null;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function captureElementContext(el: Element): Partial<Annotation> {
|
|
1873
|
+
const svelteMetadata = svelteDetectionEnabled ? detectSvelteMetadata(el) : null;
|
|
1874
|
+
|
|
1875
|
+
return {
|
|
1876
|
+
accessibility: getAccessibilityInfo(el) || undefined,
|
|
1877
|
+
computedStyles: getForensicComputedStyles(el) || undefined,
|
|
1878
|
+
cssClasses: getElementClasses(el) || undefined,
|
|
1879
|
+
dryuiComponent: detectDryUIComponent(el),
|
|
1880
|
+
fullPath: getFullElementPath(el) || undefined,
|
|
1881
|
+
nearbyElements: getNearbyElements(el) || undefined,
|
|
1882
|
+
nearbyText: getNearbyText(el) || undefined,
|
|
1883
|
+
sourceFile: svelteMetadata?.sourceFile,
|
|
1884
|
+
svelteComponent: svelteMetadata?.svelteComponent,
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function getAnnotationHighlightRect(annotation: Annotation): Rect | null {
|
|
1889
|
+
if (annotation.elementBoundingBoxes?.length) {
|
|
1890
|
+
return unionRects(annotation.elementBoundingBoxes);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return annotation.boundingBox ?? null;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function armClickSuppression() {
|
|
1897
|
+
suppressClick = true;
|
|
1898
|
+
window.setTimeout(() => {
|
|
1899
|
+
suppressClick = false;
|
|
1900
|
+
}, 0);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function openPendingDraft(draft: PendingDraft, preserveHighlight: boolean = false) {
|
|
1904
|
+
pendingDraft = draft;
|
|
1905
|
+
pendingPosition = draft.position;
|
|
1906
|
+
popupColor = annotationColor;
|
|
1907
|
+
if (!preserveHighlight) {
|
|
1908
|
+
resetHighlights();
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function buildDraftFromElement(el: Element, rect: Rect, selectedText?: string): PendingDraft {
|
|
1913
|
+
const { name, path } = identifyElement(el);
|
|
1914
|
+
const fixed = isElementFixed(el);
|
|
1915
|
+
const label = selectedText ? `Selected text in ${name}` : name;
|
|
1916
|
+
|
|
1917
|
+
return {
|
|
1918
|
+
elementLabel: label,
|
|
1919
|
+
position: getPopupPosition(rect, window.innerWidth, POPUP_WIDTH_PX),
|
|
1920
|
+
data: {
|
|
1921
|
+
x: ((rect.x + rect.width / 2) / window.innerWidth) * 100,
|
|
1922
|
+
y: fixed ? rect.y : rect.y + window.scrollY,
|
|
1923
|
+
isFixed: fixed,
|
|
1924
|
+
element: label,
|
|
1925
|
+
elementPath: path,
|
|
1926
|
+
selectedText,
|
|
1927
|
+
boundingBox: rect,
|
|
1928
|
+
...captureElementContext(el),
|
|
1929
|
+
},
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function createElementDraft(el: Element): PendingDraft {
|
|
1934
|
+
return buildDraftFromElement(el, toRect(el.getBoundingClientRect()));
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function createSelectionDraft(): PendingDraft | null {
|
|
1938
|
+
const selection = window.getSelection();
|
|
1939
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
|
|
1940
|
+
|
|
1941
|
+
const selectedText = normalizeText(selection.toString());
|
|
1942
|
+
if (!selectedText) return null;
|
|
1943
|
+
|
|
1944
|
+
const range = selection.getRangeAt(0);
|
|
1945
|
+
const rect = range.getBoundingClientRect();
|
|
1946
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
1947
|
+
|
|
1948
|
+
const container =
|
|
1949
|
+
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
|
|
1950
|
+
? (range.commonAncestorContainer as Element)
|
|
1951
|
+
: range.commonAncestorContainer.parentElement;
|
|
1952
|
+
if (!container || isFeedbackElement(container)) return null;
|
|
1953
|
+
|
|
1954
|
+
return buildDraftFromElement(container, toRect(rect), selectedText);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function isSelectableAreaTarget(value: unknown): value is HTMLElement | SVGElement {
|
|
1958
|
+
const el = value instanceof HTMLElement || value instanceof SVGElement ? value : null;
|
|
1959
|
+
if (!el || isFeedbackElement(el)) return false;
|
|
1960
|
+
|
|
1961
|
+
const rect = el.getBoundingClientRect();
|
|
1962
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
1963
|
+
|
|
1964
|
+
if (el instanceof HTMLElement) {
|
|
1965
|
+
const style = window.getComputedStyle(el);
|
|
1966
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
const tag = el.tagName.toLowerCase();
|
|
1970
|
+
if (INTERACTIVE_TAGS.has(tag)) return true;
|
|
1971
|
+
if (el.children.length === 0) return normalizeText(el.textContent ?? '').length > 0;
|
|
1972
|
+
return normalizeText(el.textContent ?? '').length > 0 && el.children.length <= 2;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function collectAreaTargets(selectionRect: Rect): Element[] {
|
|
1976
|
+
const candidates = Array.from(document.body.querySelectorAll('*')).filter((el) => {
|
|
1977
|
+
if (!isSelectableAreaTarget(el)) return false;
|
|
1978
|
+
return intersectsRect(selectionRect, toRect(el.getBoundingClientRect()));
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
const deepest = candidates.filter((candidate) => !candidates.some((other) => other !== candidate && candidate.contains(other)));
|
|
1982
|
+
return deepest.slice(0, MAX_AREA_TARGETS);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function createAggregateDraft(candidates: Element[], selectionRect: Rect, groupPath: string): PendingDraft {
|
|
1986
|
+
const primary = candidates[0];
|
|
1987
|
+
const primaryFixed = primary ? isElementFixed(primary) : false;
|
|
1988
|
+
const labels = uniqueLabels(candidates.map((el) => identifyElement(el).name));
|
|
1989
|
+
const label =
|
|
1990
|
+
candidates.length === 0
|
|
1991
|
+
? 'Selected area'
|
|
1992
|
+
: candidates.length === 1
|
|
1993
|
+
? labels[0] ?? 'Selected area'
|
|
1994
|
+
: `${candidates.length} selected elements`;
|
|
1995
|
+
|
|
1996
|
+
return {
|
|
1997
|
+
elementLabel: label,
|
|
1998
|
+
position: getPopupPosition(selectionRect, window.innerWidth, POPUP_WIDTH_PX),
|
|
1999
|
+
data: {
|
|
2000
|
+
x: ((selectionRect.x + selectionRect.width / 2) / window.innerWidth) * 100,
|
|
2001
|
+
y: primaryFixed ? selectionRect.y : selectionRect.y + window.scrollY,
|
|
2002
|
+
isFixed: primaryFixed,
|
|
2003
|
+
element: label,
|
|
2004
|
+
elementPath: candidates.length === 1 && primary ? identifyElement(primary).path : groupPath,
|
|
2005
|
+
boundingBox: selectionRect,
|
|
2006
|
+
...(primary ? captureElementContext(primary) : {}),
|
|
2007
|
+
nearbyText:
|
|
2008
|
+
candidates
|
|
2009
|
+
.map((el) => getNearbyText(el))
|
|
2010
|
+
.filter(Boolean)
|
|
2011
|
+
.slice(0, 5)
|
|
2012
|
+
.join(', ') || undefined,
|
|
2013
|
+
nearbyElements: labels.join(', ') || undefined,
|
|
2014
|
+
},
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function createAreaDraft(selectionRect: Rect): PendingDraft {
|
|
2019
|
+
return createAggregateDraft(collectAreaTargets(selectionRect), selectionRect, 'selection area');
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function createMultiSelectDraft(targets: MultiSelectTarget[], selectionRect: Rect): PendingDraft {
|
|
2023
|
+
return createAggregateDraft(
|
|
2024
|
+
targets.map(({ element }) => element),
|
|
2025
|
+
selectionRect,
|
|
2026
|
+
'multi selection',
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function syncMultiSelectTargets(targets: MultiSelectTarget[]) {
|
|
2031
|
+
pendingMultiSelectTargets = targets;
|
|
2032
|
+
|
|
2033
|
+
const selectionRect = unionRects(targets.map(({ rect }) => rect));
|
|
2034
|
+
if (!selectionRect) {
|
|
2035
|
+
pendingDraft = null;
|
|
2036
|
+
resetHighlights();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
hoverRect = selectionRect;
|
|
2041
|
+
hoverLabel = targets.length === 1 ? targets[0]?.label : `${targets.length} selected elements`;
|
|
2042
|
+
|
|
2043
|
+
if (targets.length > 1) {
|
|
2044
|
+
openPendingDraft(createMultiSelectDraft(targets, selectionRect), true);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
pendingDraft = null;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function toggleMultiSelectTarget(el: Element) {
|
|
2052
|
+
const { name, path } = identifyElement(el);
|
|
2053
|
+
const existing = pendingMultiSelectTargets.findIndex((target) => target.element === el);
|
|
2054
|
+
|
|
2055
|
+
if (existing >= 0) {
|
|
2056
|
+
syncMultiSelectTargets(pendingMultiSelectTargets.filter((target) => target.path !== path));
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
syncMultiSelectTargets([
|
|
2061
|
+
...pendingMultiSelectTargets,
|
|
2062
|
+
{
|
|
2063
|
+
element: el,
|
|
2064
|
+
label: name,
|
|
2065
|
+
path,
|
|
2066
|
+
rect: toRect(el.getBoundingClientRect()),
|
|
2067
|
+
},
|
|
2068
|
+
]);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function handleCapturedClick(event: MouseEvent) {
|
|
2072
|
+
if (isFeedbackElement(event.target)) return;
|
|
2073
|
+
|
|
2074
|
+
if (isMultiSelectModifier(event) && !editingAnnotation && (!pendingDraft || pendingMultiSelectTargets.length > 0)) {
|
|
2075
|
+
const el =
|
|
2076
|
+
event.target instanceof Element && !isFeedbackElement(event.target)
|
|
2077
|
+
? event.target
|
|
2078
|
+
: deepElementFromPoint(event.clientX, event.clientY);
|
|
2079
|
+
if (!(el instanceof Element) || isFeedbackElement(el)) return;
|
|
2080
|
+
|
|
2081
|
+
clearSelection();
|
|
2082
|
+
toggleMultiSelectTarget(el);
|
|
2083
|
+
event.preventDefault();
|
|
2084
|
+
event.stopPropagation();
|
|
2085
|
+
event.stopImmediatePropagation();
|
|
2086
|
+
suppressClick = false;
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (!suppressClick && !settings.blockInteractions && !showPopup) return;
|
|
2091
|
+
stopCapturedEvent(event);
|
|
2092
|
+
suppressClick = false;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function handleCapturedPointerDown(event: PointerEvent) {
|
|
2096
|
+
if (!settings.blockInteractions || isFeedbackElement(event.target)) return;
|
|
2097
|
+
if (!isBlockedInteractionTarget(event.target)) return;
|
|
2098
|
+
event.stopPropagation();
|
|
2099
|
+
event.stopImmediatePropagation();
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function handleCapturedSubmit(event: Event) {
|
|
2103
|
+
if (!settings.blockInteractions || isFeedbackElement(event.target)) return;
|
|
2104
|
+
stopCapturedEvent(event);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
function handleMouseMove(event: MouseEvent) {
|
|
2108
|
+
if (!active || layoutActive || rearrangeActive || pendingDraft || editingAnnotation) return;
|
|
2109
|
+
if (pendingMultiSelectTargets.length > 0) return;
|
|
2110
|
+
|
|
2111
|
+
if (pointerDown) {
|
|
2112
|
+
const nextRect = rectFromPoints(pointerDown, { x: event.clientX, y: event.clientY });
|
|
2113
|
+
dragRect = hasMeaningfulArea(nextRect, DRAG_THRESHOLD_PX) ? nextRect : null;
|
|
2114
|
+
hoverRect = dragRect;
|
|
2115
|
+
hoverLabel = dragRect ? 'Selected area' : undefined;
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
const el = deepElementFromPoint(event.clientX, event.clientY);
|
|
2120
|
+
if (!(el instanceof Element) || isFeedbackElement(el)) {
|
|
2121
|
+
resetHighlights();
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
const rect = el.getBoundingClientRect();
|
|
2126
|
+
hoverRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
2127
|
+
hoverLabel = identifyElement(el).name;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function handleMouseDown(event: MouseEvent) {
|
|
2131
|
+
if (!active || layoutActive || rearrangeActive || editingAnnotation || !isPrimaryButton(event)) return;
|
|
2132
|
+
if (pendingDraft && (!isMultiSelectModifier(event) || pendingMultiSelectTargets.length === 0)) return;
|
|
2133
|
+
if (isFeedbackElement(event.target)) return;
|
|
2134
|
+
const blockedInteraction = settings.blockInteractions && isBlockedInteractionTarget(event.target);
|
|
2135
|
+
|
|
2136
|
+
if (!isMultiSelectModifier(event) && pendingMultiSelectTargets.length > 0) {
|
|
2137
|
+
clearMultiSelect();
|
|
2138
|
+
resetHighlights();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
pointerDown = { x: event.clientX, y: event.clientY };
|
|
2142
|
+
dragRect = null;
|
|
2143
|
+
if (pendingMultiSelectTargets.length === 0) {
|
|
2144
|
+
resetHighlights();
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
if (blockedInteraction) {
|
|
2148
|
+
stopCapturedEvent(event);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function handleMouseUp(event: MouseEvent) {
|
|
2153
|
+
if (!active || layoutActive || rearrangeActive || editingAnnotation || !pointerDown || !isPrimaryButton(event)) return;
|
|
2154
|
+
if (pendingDraft && (!isMultiSelectModifier(event) || pendingMultiSelectTargets.length === 0)) return;
|
|
2155
|
+
const blockedInteraction = settings.blockInteractions && isBlockedInteractionTarget(event.target);
|
|
2156
|
+
if (isFeedbackElement(event.target)) {
|
|
2157
|
+
clearPointerState();
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (isMultiSelectModifier(event)) {
|
|
2162
|
+
clearPointerState();
|
|
2163
|
+
clearSelection();
|
|
2164
|
+
if (blockedInteraction) {
|
|
2165
|
+
stopCapturedEvent(event);
|
|
2166
|
+
}
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const selectionDraft = createSelectionDraft();
|
|
2171
|
+
if (selectionDraft) {
|
|
2172
|
+
clearMultiSelect();
|
|
2173
|
+
openPendingDraft(selectionDraft);
|
|
2174
|
+
clearPointerState();
|
|
2175
|
+
clearSelection();
|
|
2176
|
+
armClickSuppression();
|
|
2177
|
+
if (blockedInteraction) {
|
|
2178
|
+
stopCapturedEvent(event);
|
|
2179
|
+
}
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if (dragRect && hasMeaningfulArea(dragRect, DRAG_THRESHOLD_PX)) {
|
|
2184
|
+
clearMultiSelect();
|
|
2185
|
+
openPendingDraft(createAreaDraft(dragRect));
|
|
2186
|
+
clearPointerState();
|
|
2187
|
+
armClickSuppression();
|
|
2188
|
+
if (blockedInteraction) {
|
|
2189
|
+
stopCapturedEvent(event);
|
|
2190
|
+
}
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
const el = deepElementFromPoint(event.clientX, event.clientY);
|
|
2195
|
+
clearPointerState();
|
|
2196
|
+
if (!(el instanceof Element) || isFeedbackElement(el)) {
|
|
2197
|
+
if (blockedInteraction) {
|
|
2198
|
+
stopCapturedEvent(event);
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const dryuiResult = detectDryUIComponent(el);
|
|
2204
|
+
if (dryuiResult) {
|
|
2205
|
+
const parts = dryuiResult.split(' ');
|
|
2206
|
+
const componentName = parts[0] ?? dryuiResult;
|
|
2207
|
+
const props: Record<string, string> = {};
|
|
2208
|
+
for (const part of parts.slice(1)) {
|
|
2209
|
+
const eqIdx = part.indexOf('=');
|
|
2210
|
+
if (eqIdx > 0) {
|
|
2211
|
+
props[part.slice(0, eqIdx)] = part.slice(eqIdx + 1);
|
|
2212
|
+
} else {
|
|
2213
|
+
props[part] = '';
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
componentActionsTarget = {
|
|
2217
|
+
name: componentName,
|
|
2218
|
+
selector: getFullElementPath(el),
|
|
2219
|
+
props,
|
|
2220
|
+
position: { x: event.clientX, y: event.clientY },
|
|
2221
|
+
};
|
|
2222
|
+
armClickSuppression();
|
|
2223
|
+
if (blockedInteraction) {
|
|
2224
|
+
stopCapturedEvent(event);
|
|
2225
|
+
}
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
clearMultiSelect();
|
|
2230
|
+
openPendingDraft(createElementDraft(el));
|
|
2231
|
+
armClickSuppression();
|
|
2232
|
+
if (blockedInteraction) {
|
|
2233
|
+
stopCapturedEvent(event);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function handlePopupSubmit(comment: string, meta?: { resolutionNote?: string }) {
|
|
2238
|
+
if (!pendingDraft && !editingAnnotation) return;
|
|
2239
|
+
|
|
2240
|
+
if (editingAnnotation) {
|
|
2241
|
+
updateExistingAnnotation(editingAnnotation, {
|
|
2242
|
+
color: popupColor,
|
|
2243
|
+
comment,
|
|
2244
|
+
resolutionNote: meta?.resolutionNote,
|
|
2245
|
+
});
|
|
2246
|
+
editingAnnotation = null;
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const annotation: Annotation = {
|
|
2251
|
+
...pendingDraft!.data,
|
|
2252
|
+
id: generateId(),
|
|
2253
|
+
timestamp: Date.now(),
|
|
2254
|
+
comment,
|
|
2255
|
+
kind: 'feedback',
|
|
2256
|
+
color: popupColor,
|
|
2257
|
+
...(endpoint
|
|
2258
|
+
? {
|
|
2259
|
+
sessionId: currentSessionId ?? undefined,
|
|
2260
|
+
status: 'pending' as const,
|
|
2261
|
+
url: pageUrl(),
|
|
2262
|
+
}
|
|
2263
|
+
: {}),
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
persistAnnotations([...annotations, annotation]);
|
|
2267
|
+
onAnnotationAdd?.(annotation);
|
|
2268
|
+
void fireWebhook('annotation.add', { annotation });
|
|
2269
|
+
if (endpoint) {
|
|
2270
|
+
void (async () => {
|
|
2271
|
+
const sessionId = await ensureSession();
|
|
2272
|
+
if (!sessionId) return;
|
|
2273
|
+
|
|
2274
|
+
try {
|
|
2275
|
+
const serverAnnotation = await syncAnnotationToServer(endpoint, sessionId, {
|
|
2276
|
+
...annotation,
|
|
2277
|
+
sessionId,
|
|
2278
|
+
url: pageUrl(),
|
|
2279
|
+
});
|
|
2280
|
+
const normalized = normalizeRemoteAnnotation(serverAnnotation, annotation);
|
|
2281
|
+
if (!normalized) return;
|
|
2282
|
+
replaceLocalAnnotation(annotation.id, markAnnotationSynced(normalized, sessionId));
|
|
2283
|
+
} catch {
|
|
2284
|
+
connectionStatus = 'disconnected';
|
|
2285
|
+
}
|
|
2286
|
+
})();
|
|
2287
|
+
}
|
|
2288
|
+
pendingDraft = null;
|
|
2289
|
+
clearMultiSelect();
|
|
2290
|
+
resetHighlights();
|
|
2291
|
+
clearSelection();
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
function handlePopupCancel() {
|
|
2295
|
+
pendingDraft = null;
|
|
2296
|
+
editingAnnotation = null;
|
|
2297
|
+
popupColor = annotationColor;
|
|
2298
|
+
clearMultiSelect();
|
|
2299
|
+
resetHighlights();
|
|
2300
|
+
clearSelection();
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
function handlePopupDelete() {
|
|
2304
|
+
const current = editingAnnotation;
|
|
2305
|
+
if (!current) return;
|
|
2306
|
+
deleteExistingAnnotation(current);
|
|
2307
|
+
editingAnnotation = null;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
function handlePopupAcknowledge(meta?: { resolutionNote?: string }) {
|
|
2311
|
+
const current = editingAnnotation;
|
|
2312
|
+
if (!current) return;
|
|
2313
|
+
|
|
2314
|
+
updateExistingAnnotation(current, {
|
|
2315
|
+
status: 'acknowledged',
|
|
2316
|
+
resolutionNote: meta?.resolutionNote ?? current.resolutionNote,
|
|
2317
|
+
});
|
|
2318
|
+
editingAnnotation = null;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
function handlePopupResolve(meta?: { resolutionNote?: string }) {
|
|
2322
|
+
const current = editingAnnotation;
|
|
2323
|
+
if (!current) return;
|
|
2324
|
+
|
|
2325
|
+
updateExistingAnnotation(current, {
|
|
2326
|
+
status: 'resolved',
|
|
2327
|
+
resolvedAt: new Date().toISOString(),
|
|
2328
|
+
resolvedBy: 'human',
|
|
2329
|
+
resolutionNote: meta?.resolutionNote ?? current.resolutionNote,
|
|
2330
|
+
});
|
|
2331
|
+
editingAnnotation = null;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
function handlePopupDismiss(meta?: { resolutionNote?: string }) {
|
|
2335
|
+
const current = editingAnnotation;
|
|
2336
|
+
if (!current) return;
|
|
2337
|
+
|
|
2338
|
+
updateExistingAnnotation(current, {
|
|
2339
|
+
status: 'dismissed',
|
|
2340
|
+
resolvedAt: new Date().toISOString(),
|
|
2341
|
+
resolvedBy: 'human',
|
|
2342
|
+
resolutionNote: meta?.resolutionNote ?? current.resolutionNote,
|
|
2343
|
+
});
|
|
2344
|
+
editingAnnotation = null;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function handlePopupReply(content: string) {
|
|
2348
|
+
const current = editingAnnotation;
|
|
2349
|
+
if (!current) return;
|
|
2350
|
+
|
|
2351
|
+
const message: ThreadMessage = {
|
|
2352
|
+
id: crypto.randomUUID(),
|
|
2353
|
+
role: 'human',
|
|
2354
|
+
content,
|
|
2355
|
+
timestamp: Date.now(),
|
|
2356
|
+
};
|
|
2357
|
+
const updated: Annotation = {
|
|
2358
|
+
...current,
|
|
2359
|
+
thread: [...(current.thread ?? []), message],
|
|
2360
|
+
};
|
|
2361
|
+
const nextAnnotations = annotations.map((annotation) =>
|
|
2362
|
+
annotation.id === current.id ? updated : annotation,
|
|
2363
|
+
);
|
|
2364
|
+
|
|
2365
|
+
persistAnnotations(nextAnnotations);
|
|
2366
|
+
editingAnnotation = updated;
|
|
2367
|
+
onAnnotationUpdate?.(updated);
|
|
2368
|
+
onAnnotationReply?.(updated, message);
|
|
2369
|
+
|
|
2370
|
+
if (endpoint) {
|
|
2371
|
+
void replyToAnnotationOnServer(endpoint, updated.id, message)
|
|
2372
|
+
.then((serverAnnotation) => {
|
|
2373
|
+
const normalized = normalizeRemoteAnnotation(serverAnnotation, updated);
|
|
2374
|
+
if (!normalized) return;
|
|
2375
|
+
const synced = currentSessionId ? markAnnotationSynced(normalized, currentSessionId) : normalized;
|
|
2376
|
+
replaceLocalAnnotation(updated.id, synced);
|
|
2377
|
+
if (editingAnnotation?.id === synced.id) {
|
|
2378
|
+
editingAnnotation = synced;
|
|
2379
|
+
}
|
|
2380
|
+
})
|
|
2381
|
+
.catch(() => {
|
|
2382
|
+
connectionStatus = 'disconnected';
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
void fireWebhook('thread.message', { annotation: updated, message });
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function handleMarkerEnter(annotation: Annotation) {
|
|
2390
|
+
const rect = getAnnotationHighlightRect(annotation);
|
|
2391
|
+
if (!rect) return;
|
|
2392
|
+
markerHoverRect = rect;
|
|
2393
|
+
markerHoverLabel = annotation.element;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function handleMarkerLeave() {
|
|
2397
|
+
clearMarkerHighlight();
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function handleMarkerClick(annotation: Annotation) {
|
|
2401
|
+
clearMarkerHighlight();
|
|
2402
|
+
if (settings.markerClickBehavior === 'delete') {
|
|
2403
|
+
deleteExistingAnnotation(annotation);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
editingAnnotation = annotation;
|
|
2408
|
+
popupColor = annotation.color;
|
|
2409
|
+
pendingPosition = {
|
|
2410
|
+
x: Math.min((annotation.x / 100) * window.innerWidth + 30, window.innerWidth - POPUP_WIDTH_PX),
|
|
2411
|
+
y: annotation.isFixed ? annotation.y : annotation.y - window.scrollY,
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
function handleCopy() {
|
|
2416
|
+
if (!hasOutput) return;
|
|
2417
|
+
|
|
2418
|
+
const output = buildOutput('copy');
|
|
2419
|
+
if (!output) return;
|
|
2420
|
+
|
|
2421
|
+
if (copyToClipboard && typeof navigator !== 'undefined') {
|
|
2422
|
+
navigator.clipboard.writeText(output).catch(() => {});
|
|
2423
|
+
}
|
|
2424
|
+
onCopy?.(output);
|
|
2425
|
+
setCopyState('copied');
|
|
2426
|
+
if (settings.autoClearAfterCopy) {
|
|
2427
|
+
window.setTimeout(() => {
|
|
2428
|
+
clearAllFeedback();
|
|
2429
|
+
}, 500);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
async function handleSubmit() {
|
|
2434
|
+
if (!hasOutput || submitState === 'sending') return;
|
|
2435
|
+
|
|
2436
|
+
const output = buildOutput('submit');
|
|
2437
|
+
if (!output) return;
|
|
2438
|
+
|
|
2439
|
+
setSubmitState('sending');
|
|
2440
|
+
onSubmit?.(output, annotations);
|
|
2441
|
+
|
|
2442
|
+
let delivered = onSubmit !== undefined;
|
|
2443
|
+
if (endpoint) {
|
|
2444
|
+
const sessionId = await ensureSession();
|
|
2445
|
+
if (sessionId) {
|
|
2446
|
+
try {
|
|
2447
|
+
await requestAction(endpoint, sessionId, output);
|
|
2448
|
+
delivered = true;
|
|
2449
|
+
} catch {
|
|
2450
|
+
connectionStatus = 'disconnected';
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const webhookDelivered = await fireWebhook('submit', { output, annotations }, true);
|
|
2456
|
+
delivered = delivered || webhookDelivered;
|
|
2457
|
+
setSubmitState(delivered ? 'sent' : 'failed');
|
|
2458
|
+
|
|
2459
|
+
if (settings.autoClearAfterCopy && delivered) {
|
|
2460
|
+
window.setTimeout(() => {
|
|
2461
|
+
clearAllFeedback();
|
|
2462
|
+
}, 500);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function handleClear() {
|
|
2467
|
+
clearAllFeedback();
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function handleToggleActive() {
|
|
2471
|
+
if (toolbarHidden) return;
|
|
2472
|
+
|
|
2473
|
+
active = !active;
|
|
2474
|
+
if (!active) {
|
|
2475
|
+
closeLayoutMode();
|
|
2476
|
+
clearMultiSelect();
|
|
2477
|
+
resetHighlights();
|
|
2478
|
+
clearPointerState();
|
|
2479
|
+
pendingDraft = null;
|
|
2480
|
+
editingAnnotation = null;
|
|
2481
|
+
clearSelection();
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
2486
|
+
if (event.key === 'Escape') {
|
|
2487
|
+
if (layoutMode !== 'idle') {
|
|
2488
|
+
event.preventDefault();
|
|
2489
|
+
if (activeComponent) {
|
|
2490
|
+
activeComponent = null;
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
closeLayoutMode();
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
if (showPopup || pendingMultiSelectTargets.length > 0) {
|
|
2498
|
+
event.preventDefault();
|
|
2499
|
+
handlePopupCancel();
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
event.preventDefault();
|
|
2504
|
+
handleToggleActive();
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
const target = event.target as HTMLElement | null;
|
|
2509
|
+
const isTyping = target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA' || target?.isContentEditable;
|
|
2510
|
+
|
|
2511
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'z' && !event.shiftKey && layoutActive) {
|
|
2512
|
+
event.preventDefault();
|
|
2513
|
+
const prev = layoutHistory.undo();
|
|
2514
|
+
if (prev) designPlacements = prev;
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'z' && layoutActive) {
|
|
2519
|
+
event.preventDefault();
|
|
2520
|
+
const next = layoutHistory.redo();
|
|
2521
|
+
if (next) designPlacements = next;
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
if (isTyping || event.metaKey || event.ctrlKey) return;
|
|
2526
|
+
|
|
2527
|
+
if (event.key === '/' && layoutActive && !isTyping) {
|
|
2528
|
+
event.preventDefault();
|
|
2529
|
+
componentPickerOpen = true;
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
if (event.key === 'p' || event.key === 'P') {
|
|
2534
|
+
event.preventDefault();
|
|
2535
|
+
togglePause();
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
if (event.key === 'l' || event.key === 'L') {
|
|
2540
|
+
event.preventDefault();
|
|
2541
|
+
if (showPopup || pendingMultiSelectTargets.length > 0) {
|
|
2542
|
+
handlePopupCancel();
|
|
2543
|
+
}
|
|
2544
|
+
toggleLayoutMode();
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
if (event.key === 'r' || event.key === 'R') {
|
|
2549
|
+
event.preventDefault();
|
|
2550
|
+
if (showPopup || pendingMultiSelectTargets.length > 0) {
|
|
2551
|
+
handlePopupCancel();
|
|
2552
|
+
}
|
|
2553
|
+
toggleRearrangeMode();
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
if ((event.key === 'h' || event.key === 'H') && layoutMode === 'idle') {
|
|
2558
|
+
event.preventDefault();
|
|
2559
|
+
toggleMarkers();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
if (event.key === 'c' || event.key === 'C') {
|
|
2564
|
+
if (!hasOutput) return;
|
|
2565
|
+
event.preventDefault();
|
|
2566
|
+
handleCopy();
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
if (event.key === 'x' || event.key === 'X') {
|
|
2571
|
+
if (!hasOutput) return;
|
|
2572
|
+
event.preventDefault();
|
|
2573
|
+
handleClear();
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
if (event.key === 's' || event.key === 'S') {
|
|
2578
|
+
if (!canSubmit || !hasOutput || submitState === 'sending') return;
|
|
2579
|
+
event.preventDefault();
|
|
2580
|
+
void handleSubmit();
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
</script>
|
|
2584
|
+
|
|
2585
|
+
{#if !toolbarHidden}
|
|
2586
|
+
<Hotkey keys={shortcut} handler={handleToggleActive} />
|
|
2587
|
+
{/if}
|
|
2588
|
+
|
|
2589
|
+
<svelte:window onkeydown={active ? handleKeyDown : undefined} />
|
|
2590
|
+
|
|
2591
|
+
<svelte:document
|
|
2592
|
+
onpointerdowncapture={annotatingPage ? handleCapturedPointerDown : undefined}
|
|
2593
|
+
onmousedowncapture={annotatingPage ? handleMouseDown : undefined}
|
|
2594
|
+
onmousemove={annotatingPage ? handleMouseMove : undefined}
|
|
2595
|
+
onmouseupcapture={annotatingPage ? handleMouseUp : undefined}
|
|
2596
|
+
onclickcapture={annotatingPage ? handleCapturedClick : undefined}
|
|
2597
|
+
onsubmitcapture={annotatingPage ? handleCapturedSubmit : undefined}
|
|
2598
|
+
/>
|
|
2599
|
+
|
|
2600
|
+
<Portal>
|
|
2601
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
2602
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
2603
|
+
<div
|
|
2604
|
+
class={className}
|
|
2605
|
+
data-dryui-feedback
|
|
2606
|
+
data-feedback-shortcut={shortcut}
|
|
2607
|
+
data-feedback-svelte-detection={svelteDetectionEnabled ? 'enabled' : 'disabled'}
|
|
2608
|
+
data-feedback-theme={settings.theme}
|
|
2609
|
+
onclick={(e) => e.stopPropagation()}
|
|
2610
|
+
onmousedown={(e) => e.stopPropagation()}
|
|
2611
|
+
>
|
|
2612
|
+
{#if annotatingPage}
|
|
2613
|
+
<HighlightOverlay rect={activeHighlightRect} label={activeHighlightLabel} />
|
|
2614
|
+
{/if}
|
|
2615
|
+
|
|
2616
|
+
{#if layoutActive}
|
|
2617
|
+
<DesignPalette
|
|
2618
|
+
bind:open={paletteOpen}
|
|
2619
|
+
bind:value={activeComponent}
|
|
2620
|
+
purpose={canvasPurpose}
|
|
2621
|
+
wireframePrompt={wireframePrompt}
|
|
2622
|
+
wireframe={blankCanvas}
|
|
2623
|
+
placementCount={designPlacements.length}
|
|
2624
|
+
sectionCount={rearrangeState?.sections.length ?? 0}
|
|
2625
|
+
onSelect={(type) => {
|
|
2626
|
+
activeComponent = type;
|
|
2627
|
+
}}
|
|
2628
|
+
onDetectSections={handleDetectSections}
|
|
2629
|
+
onPurposeChange={handleCanvasPurposeChange}
|
|
2630
|
+
onWireframePromptChange={handleWireframePromptChange}
|
|
2631
|
+
onDragStart={handlePaletteDragStart}
|
|
2632
|
+
onClear={clearLayoutContent}
|
|
2633
|
+
/>
|
|
2634
|
+
{/if}
|
|
2635
|
+
|
|
2636
|
+
{#if layoutActive}
|
|
2637
|
+
<DesignMode
|
|
2638
|
+
placements={designPlacements}
|
|
2639
|
+
bind:activeComponent
|
|
2640
|
+
wireframe={blankCanvas}
|
|
2641
|
+
passthrough={rearrangeActive && activeComponent === null}
|
|
2642
|
+
extraSnapRects={rearrangeState?.sections.map((section) => section.currentRect)}
|
|
2643
|
+
clearSignal={designClearSignal}
|
|
2644
|
+
deselectSignal={designDeselectSignal}
|
|
2645
|
+
{canvasWidth}
|
|
2646
|
+
onChange={(next) => {
|
|
2647
|
+
setDesignPlacements(next);
|
|
2648
|
+
}}
|
|
2649
|
+
onInteractionChange={(next) => {
|
|
2650
|
+
overlayInteracting = next;
|
|
2651
|
+
}}
|
|
2652
|
+
onSelectionChange={(selected, isShift) => {
|
|
2653
|
+
resetCrossDragState();
|
|
2654
|
+
designSelectedIds = Array.from(selected);
|
|
2655
|
+
designSelectionCount = selected.size;
|
|
2656
|
+
if (!isShift && selected.size > 0) {
|
|
2657
|
+
rearrangeSelectedIds = [];
|
|
2658
|
+
rearrangeSelectionCount = 0;
|
|
2659
|
+
rearrangeDeselectSignal += 1;
|
|
2660
|
+
}
|
|
2661
|
+
}}
|
|
2662
|
+
onDragMove={(dx, dy) => {
|
|
2663
|
+
previewRearrangeSelection(dx, dy);
|
|
2664
|
+
}}
|
|
2665
|
+
onDragEnd={(dx, dy, committed) => {
|
|
2666
|
+
commitRearrangeSelection(dx, dy, committed);
|
|
2667
|
+
}}
|
|
2668
|
+
onCanvasWidthChange={(w) => { canvasWidth = w; }}
|
|
2669
|
+
onHistoryPush={() => {
|
|
2670
|
+
layoutHistory.push(designPlacements);
|
|
2671
|
+
}}
|
|
2672
|
+
/>
|
|
2673
|
+
{/if}
|
|
2674
|
+
|
|
2675
|
+
{#if layoutActive && blankCanvas}
|
|
2676
|
+
<div
|
|
2677
|
+
data-wireframe-canvas
|
|
2678
|
+
style={`position: fixed; inset: 0; z-index: 999; pointer-events: none; opacity: ${canvasOpacity}; background:
|
|
2679
|
+
radial-gradient(circle at top left, rgba(249, 115, 22, 0.16), transparent 34%),
|
|
2680
|
+
radial-gradient(circle, rgba(249, 115, 22, 0.18) 1px, transparent 1px),
|
|
2681
|
+
linear-gradient(180deg, rgba(255, 247, 237, 0.94), rgba(255, 255, 255, 0.72));
|
|
2682
|
+
background-size: auto, 24px 24px, auto;
|
|
2683
|
+
background-position: 0 0, 12px 12px, 0 0;`}
|
|
2684
|
+
></div>
|
|
2685
|
+
|
|
2686
|
+
<div
|
|
2687
|
+
data-wireframe-notice
|
|
2688
|
+
style="position: fixed; left: 16px; bottom: 16px; z-index: 1005; pointer-events: auto; max-width: 320px;"
|
|
2689
|
+
>
|
|
2690
|
+
<Stack gap="sm">
|
|
2691
|
+
<Stack direction="horizontal" gap="sm" align="center">
|
|
2692
|
+
<Text as="span" size="sm">Toggle opacity</Text>
|
|
2693
|
+
<Slider bind:value={canvasOpacity} min={0} max={1} step={0.01} aria-label="Wireframe canvas opacity" />
|
|
2694
|
+
</Stack>
|
|
2695
|
+
<Stack direction="horizontal" gap="sm" align="center">
|
|
2696
|
+
<Text as="span" size="md">Wireframe mode</Text>
|
|
2697
|
+
<Button variant="ghost" size="sm" onclick={clearLayoutContent}>Start over</Button>
|
|
2698
|
+
</Stack>
|
|
2699
|
+
<Text as="div" size="sm" color="secondary">
|
|
2700
|
+
Drag components onto the canvas. Copied output will only include the wireframed layout.
|
|
2701
|
+
</Text>
|
|
2702
|
+
</Stack>
|
|
2703
|
+
</div>
|
|
2704
|
+
{/if}
|
|
2705
|
+
|
|
2706
|
+
{#if rearrangeActive && rearrangeState}
|
|
2707
|
+
<RearrangeOverlay
|
|
2708
|
+
{rearrangeState}
|
|
2709
|
+
blankCanvas={blankCanvas}
|
|
2710
|
+
extraSnapRects={designPlacements.map((placement) => ({
|
|
2711
|
+
x: placement.x,
|
|
2712
|
+
y: placement.y,
|
|
2713
|
+
width: placement.width,
|
|
2714
|
+
height: placement.height,
|
|
2715
|
+
}))}
|
|
2716
|
+
clearSignal={rearrangeClearSignal}
|
|
2717
|
+
deselectSignal={rearrangeDeselectSignal}
|
|
2718
|
+
onChange={(next) => {
|
|
2719
|
+
setRearrangeLayout(next);
|
|
2720
|
+
}}
|
|
2721
|
+
onInteractionChange={(next) => {
|
|
2722
|
+
overlayInteracting = next;
|
|
2723
|
+
}}
|
|
2724
|
+
onSelectionChange={(selected, isShift) => {
|
|
2725
|
+
resetCrossDragState();
|
|
2726
|
+
rearrangeSelectedIds = Array.from(selected);
|
|
2727
|
+
rearrangeSelectionCount = selected.size;
|
|
2728
|
+
if (!isShift && selected.size > 0) {
|
|
2729
|
+
designSelectedIds = [];
|
|
2730
|
+
designSelectionCount = 0;
|
|
2731
|
+
designDeselectSignal += 1;
|
|
2732
|
+
}
|
|
2733
|
+
}}
|
|
2734
|
+
onDragMove={(dx, dy) => {
|
|
2735
|
+
previewDesignSelection(dx, dy);
|
|
2736
|
+
}}
|
|
2737
|
+
onDragEnd={(dx, dy, committed) => {
|
|
2738
|
+
commitDesignSelection(dx, dy, committed);
|
|
2739
|
+
}}
|
|
2740
|
+
/>
|
|
2741
|
+
{/if}
|
|
2742
|
+
|
|
2743
|
+
{#if shouldShowMarkers}
|
|
2744
|
+
{#each annotations as annotation, i (annotation.id)}
|
|
2745
|
+
<AnnotationMarker
|
|
2746
|
+
{annotation}
|
|
2747
|
+
index={i + 1}
|
|
2748
|
+
onclick={handleMarkerClick}
|
|
2749
|
+
onmouseenter={handleMarkerEnter}
|
|
2750
|
+
onmouseleave={handleMarkerLeave}
|
|
2751
|
+
/>
|
|
2752
|
+
{/each}
|
|
2753
|
+
{/if}
|
|
2754
|
+
|
|
2755
|
+
{#if showPopup}
|
|
2756
|
+
{#key editingAnnotation?.id ?? pendingDraft?.data.elementPath ?? popupElement}
|
|
2757
|
+
<AnnotationPopup
|
|
2758
|
+
element={popupElement}
|
|
2759
|
+
initialValue={popupInitialValue}
|
|
2760
|
+
selectedText={popupSelectedText}
|
|
2761
|
+
computedStyles={popupComputedStyles}
|
|
2762
|
+
color={popupColor}
|
|
2763
|
+
status={editingAnnotation?.status}
|
|
2764
|
+
thread={editingAnnotation?.thread}
|
|
2765
|
+
resolvedAt={editingAnnotation?.resolvedAt}
|
|
2766
|
+
resolvedBy={editingAnnotation?.resolvedBy}
|
|
2767
|
+
resolutionNote={editingAnnotation?.resolutionNote}
|
|
2768
|
+
showDelete={editingAnnotation !== null}
|
|
2769
|
+
showStatusActions={editingAnnotation !== null}
|
|
2770
|
+
position={pendingPosition}
|
|
2771
|
+
oncolorchange={(color) => {
|
|
2772
|
+
popupColor = color;
|
|
2773
|
+
}}
|
|
2774
|
+
onsubmit={handlePopupSubmit}
|
|
2775
|
+
oncancel={handlePopupCancel}
|
|
2776
|
+
ondelete={handlePopupDelete}
|
|
2777
|
+
onacknowledge={handlePopupAcknowledge}
|
|
2778
|
+
onresolve={handlePopupResolve}
|
|
2779
|
+
ondismiss={handlePopupDismiss}
|
|
2780
|
+
onreply={editingAnnotation ? handlePopupReply : undefined}
|
|
2781
|
+
/>
|
|
2782
|
+
{/key}
|
|
2783
|
+
{/if}
|
|
2784
|
+
|
|
2785
|
+
{#if componentPickerOpen}
|
|
2786
|
+
<ComponentPicker
|
|
2787
|
+
bind:open={componentPickerOpen}
|
|
2788
|
+
onSelect={(entry) => {
|
|
2789
|
+
activeComponent = entry.name as LayoutModeComponentType;
|
|
2790
|
+
componentPickerOpen = false;
|
|
2791
|
+
}}
|
|
2792
|
+
onClose={() => { componentPickerOpen = false; }}
|
|
2793
|
+
/>
|
|
2794
|
+
{/if}
|
|
2795
|
+
|
|
2796
|
+
{#if routeCreatorOpen}
|
|
2797
|
+
<RouteCreator
|
|
2798
|
+
bind:open={routeCreatorOpen}
|
|
2799
|
+
onCreate={(routePath, recipeName) => {
|
|
2800
|
+
newPageRoute = routePath;
|
|
2801
|
+
newPageRecipe = recipeName;
|
|
2802
|
+
canvasPurpose = 'new-page';
|
|
2803
|
+
routeCreatorOpen = false;
|
|
2804
|
+
if (layoutMode !== 'design') setLayoutMode('design');
|
|
2805
|
+
}}
|
|
2806
|
+
onClose={() => { routeCreatorOpen = false; }}
|
|
2807
|
+
/>
|
|
2808
|
+
{/if}
|
|
2809
|
+
|
|
2810
|
+
{#if componentActionsTarget}
|
|
2811
|
+
<ComponentActions
|
|
2812
|
+
component={componentActionsTarget}
|
|
2813
|
+
position={componentActionsTarget.position}
|
|
2814
|
+
onSwap={() => {
|
|
2815
|
+
componentPickerOpen = true;
|
|
2816
|
+
componentActionsTarget = null;
|
|
2817
|
+
}}
|
|
2818
|
+
onRefine={(comment) => {
|
|
2819
|
+
pendingComponentActions = [...pendingComponentActions, {
|
|
2820
|
+
kind: 'refine',
|
|
2821
|
+
targetSelector: componentActionsTarget!.selector,
|
|
2822
|
+
component: componentActionsTarget!.name,
|
|
2823
|
+
comment,
|
|
2824
|
+
}];
|
|
2825
|
+
componentActionsTarget = null;
|
|
2826
|
+
}}
|
|
2827
|
+
onDelete={() => {
|
|
2828
|
+
pendingComponentActions = [...pendingComponentActions, {
|
|
2829
|
+
kind: 'delete',
|
|
2830
|
+
targetSelector: componentActionsTarget!.selector,
|
|
2831
|
+
component: componentActionsTarget!.name,
|
|
2832
|
+
}];
|
|
2833
|
+
componentActionsTarget = null;
|
|
2834
|
+
}}
|
|
2835
|
+
onClose={() => { componentActionsTarget = null; }}
|
|
2836
|
+
/>
|
|
2837
|
+
{/if}
|
|
2838
|
+
|
|
2839
|
+
<FeedbackToolbar
|
|
2840
|
+
annotationCount={annotations.length}
|
|
2841
|
+
{hasOutput}
|
|
2842
|
+
placementCount={designPlacements.length}
|
|
2843
|
+
sectionCount={rearrangeState?.sections.length ?? 0}
|
|
2844
|
+
{copyLabel}
|
|
2845
|
+
{copyState}
|
|
2846
|
+
copyDisabled={!hasOutput}
|
|
2847
|
+
{submitState}
|
|
2848
|
+
submitDisabled={!hasOutput || submitState === 'sending'}
|
|
2849
|
+
{active}
|
|
2850
|
+
hidden={toolbarHidden}
|
|
2851
|
+
{paused}
|
|
2852
|
+
markersVisible={showMarkers}
|
|
2853
|
+
settings={settings}
|
|
2854
|
+
layoutActive={layoutActive}
|
|
2855
|
+
rearrangeActive={rearrangeActive}
|
|
2856
|
+
{connectionStatus}
|
|
2857
|
+
{endpoint}
|
|
2858
|
+
sessionId={currentSessionId}
|
|
2859
|
+
{webhookUrl}
|
|
2860
|
+
{shortcut}
|
|
2861
|
+
onToggleActive={handleToggleActive}
|
|
2862
|
+
onCopy={handleCopy}
|
|
2863
|
+
{canSubmit}
|
|
2864
|
+
onSubmit={canSubmit ? handleSubmit : undefined}
|
|
2865
|
+
onClear={handleClear}
|
|
2866
|
+
onSettingsChange={handleSettingsChange}
|
|
2867
|
+
onLayout={handleToolbarLayoutToggle}
|
|
2868
|
+
onRearrange={toggleRearrangeMode}
|
|
2869
|
+
onPause={togglePause}
|
|
2870
|
+
onToggleMarkers={toggleMarkers}
|
|
2871
|
+
onHide={hideToolbarUntilRestart}
|
|
2872
|
+
canUndo={layoutHistory.canUndo()}
|
|
2873
|
+
canRedo={layoutHistory.canRedo()}
|
|
2874
|
+
onUndo={() => { const prev = layoutHistory.undo(); if (prev) designPlacements = prev; }}
|
|
2875
|
+
onRedo={() => { const next = layoutHistory.redo(); if (next) designPlacements = next; }}
|
|
2876
|
+
onNewPage={() => { routeCreatorOpen = true; }}
|
|
2877
|
+
/>
|
|
2878
|
+
</div>
|
|
2879
|
+
</Portal>
|