agentation-vue 0.2.0

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.
Files changed (111) hide show
  1. package/LICENSE +27 -0
  2. package/dist/AgentationVue.d.vue.ts +27 -0
  3. package/dist/AgentationVue.vue +839 -0
  4. package/dist/AgentationVue.vue.d.ts +27 -0
  5. package/dist/components/AgentationToolbar.d.vue.ts +36 -0
  6. package/dist/components/AgentationToolbar.vue +233 -0
  7. package/dist/components/AgentationToolbar.vue.d.ts +36 -0
  8. package/dist/components/AnnotationInput.d.vue.ts +21 -0
  9. package/dist/components/AnnotationInput.vue +105 -0
  10. package/dist/components/AnnotationInput.vue.d.ts +21 -0
  11. package/dist/components/AnnotationMarker.d.vue.ts +15 -0
  12. package/dist/components/AnnotationMarker.vue +46 -0
  13. package/dist/components/AnnotationMarker.vue.d.ts +15 -0
  14. package/dist/components/ComponentChain.d.vue.ts +10 -0
  15. package/dist/components/ComponentChain.vue +91 -0
  16. package/dist/components/ComponentChain.vue.d.ts +10 -0
  17. package/dist/components/ElementHighlight.d.vue.ts +9 -0
  18. package/dist/components/ElementHighlight.vue +33 -0
  19. package/dist/components/ElementHighlight.vue.d.ts +9 -0
  20. package/dist/components/SettingsPanel.d.vue.ts +10 -0
  21. package/dist/components/SettingsPanel.vue +143 -0
  22. package/dist/components/SettingsPanel.vue.d.ts +10 -0
  23. package/dist/components/SettingsPopover.d.vue.ts +14 -0
  24. package/dist/components/SettingsPopover.vue +235 -0
  25. package/dist/components/SettingsPopover.vue.d.ts +14 -0
  26. package/dist/components/VaButton.d.vue.ts +23 -0
  27. package/dist/components/VaButton.vue +19 -0
  28. package/dist/components/VaButton.vue.d.ts +23 -0
  29. package/dist/components/VaIcon.d.vue.ts +6 -0
  30. package/dist/components/VaIcon.vue +18 -0
  31. package/dist/components/VaIcon.vue.d.ts +6 -0
  32. package/dist/components/VaIconButton.d.vue.ts +25 -0
  33. package/dist/components/VaIconButton.vue +43 -0
  34. package/dist/components/VaIconButton.vue.d.ts +25 -0
  35. package/dist/components/VaToggle.d.vue.ts +10 -0
  36. package/dist/components/VaToggle.vue +23 -0
  37. package/dist/components/VaToggle.vue.d.ts +10 -0
  38. package/dist/composables/useAnimationPause.d.ts +7 -0
  39. package/dist/composables/useAnimationPause.js +52 -0
  40. package/dist/composables/useAnimationPause.mjs +43 -0
  41. package/dist/composables/useAnnotations.d.ts +105 -0
  42. package/dist/composables/useAnnotations.js +106 -0
  43. package/dist/composables/useAnnotations.mjs +108 -0
  44. package/dist/composables/useAreaSelect.d.ts +21 -0
  45. package/dist/composables/useAreaSelect.js +62 -0
  46. package/dist/composables/useAreaSelect.mjs +41 -0
  47. package/dist/composables/useElementDetection.d.ts +22 -0
  48. package/dist/composables/useElementDetection.js +85 -0
  49. package/dist/composables/useElementDetection.mjs +82 -0
  50. package/dist/composables/useInteractionMode.d.ts +5 -0
  51. package/dist/composables/useInteractionMode.js +29 -0
  52. package/dist/composables/useInteractionMode.mjs +20 -0
  53. package/dist/composables/useKeyboardShortcuts.d.ts +43 -0
  54. package/dist/composables/useKeyboardShortcuts.js +202 -0
  55. package/dist/composables/useKeyboardShortcuts.mjs +223 -0
  56. package/dist/composables/useMarkerPositions.d.ts +5 -0
  57. package/dist/composables/useMarkerPositions.js +45 -0
  58. package/dist/composables/useMarkerPositions.mjs +36 -0
  59. package/dist/composables/useMultiSelect.d.ts +20 -0
  60. package/dist/composables/useMultiSelect.js +108 -0
  61. package/dist/composables/useMultiSelect.mjs +85 -0
  62. package/dist/composables/useOutputFormatter.d.ts +5 -0
  63. package/dist/composables/useOutputFormatter.js +100 -0
  64. package/dist/composables/useOutputFormatter.mjs +91 -0
  65. package/dist/composables/useSettings.d.ts +14 -0
  66. package/dist/composables/useSettings.js +48 -0
  67. package/dist/composables/useSettings.mjs +38 -0
  68. package/dist/composables/useTextSelection.d.ts +11 -0
  69. package/dist/composables/useTextSelection.js +33 -0
  70. package/dist/composables/useTextSelection.mjs +22 -0
  71. package/dist/composables/useToolbarAutoHide.d.ts +19 -0
  72. package/dist/composables/useToolbarAutoHide.js +270 -0
  73. package/dist/composables/useToolbarAutoHide.mjs +208 -0
  74. package/dist/composables/useToolbarDragSnap.d.ts +30 -0
  75. package/dist/composables/useToolbarDragSnap.js +296 -0
  76. package/dist/composables/useToolbarDragSnap.mjs +245 -0
  77. package/dist/constants.d.ts +2 -0
  78. package/dist/constants.js +8 -0
  79. package/dist/constants.mjs +2 -0
  80. package/dist/directives/vaTooltip.d.ts +22 -0
  81. package/dist/directives/vaTooltip.js +241 -0
  82. package/dist/directives/vaTooltip.mjs +257 -0
  83. package/dist/icons.d.ts +16 -0
  84. package/dist/icons.js +21 -0
  85. package/dist/icons.mjs +15 -0
  86. package/dist/index.d.ts +31 -0
  87. package/dist/index.js +168 -0
  88. package/dist/index.mjs +30 -0
  89. package/dist/styles/agentation.css +1 -0
  90. package/dist/types.d.ts +70 -0
  91. package/dist/types.js +1 -0
  92. package/dist/types.mjs +0 -0
  93. package/dist/utils/clipboard.d.ts +1 -0
  94. package/dist/utils/clipboard.js +22 -0
  95. package/dist/utils/clipboard.mjs +16 -0
  96. package/dist/utils/dom-inspector.d.ts +7 -0
  97. package/dist/utils/dom-inspector.js +168 -0
  98. package/dist/utils/dom-inspector.mjs +242 -0
  99. package/dist/utils/math.d.ts +1 -0
  100. package/dist/utils/math.js +9 -0
  101. package/dist/utils/math.mjs +3 -0
  102. package/dist/utils/portal.d.ts +2 -0
  103. package/dist/utils/portal.js +18 -0
  104. package/dist/utils/portal.mjs +11 -0
  105. package/dist/utils/selectors.d.ts +3 -0
  106. package/dist/utils/selectors.js +103 -0
  107. package/dist/utils/selectors.mjs +105 -0
  108. package/dist/utils/style.d.ts +2 -0
  109. package/dist/utils/style.js +14 -0
  110. package/dist/utils/style.mjs +8 -0
  111. package/package.json +49 -0
@@ -0,0 +1,839 @@
1
+ <script setup lang="ts">
2
+ import type { KeyboardShortcutConfig } from './composables/useKeyboardShortcuts'
3
+ import type { Annotation, OutputDetail, Settings } from './types'
4
+ import { isVue2 as _isVue2, computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue-demi'
5
+ import AgentationToolbar from './components/AgentationToolbar.vue'
6
+ import AnnotationInput from './components/AnnotationInput.vue'
7
+ import AnnotationMarker from './components/AnnotationMarker.vue'
8
+ import ElementHighlight from './components/ElementHighlight.vue'
9
+ import SettingsPopover from './components/SettingsPopover.vue'
10
+ import { useAnimationPause } from './composables/useAnimationPause'
11
+ import { useAnnotations } from './composables/useAnnotations'
12
+ import { useAreaSelect } from './composables/useAreaSelect'
13
+ import { useElementDetection } from './composables/useElementDetection'
14
+ import { useInteractionMode } from './composables/useInteractionMode'
15
+ import { DEFAULT_SHORTCUT_CONFIG, useKeyboardShortcuts } from './composables/useKeyboardShortcuts'
16
+ import { useMarkerPositions } from './composables/useMarkerPositions'
17
+ import { useMultiSelect } from './composables/useMultiSelect'
18
+ import { useOutputFormatter } from './composables/useOutputFormatter'
19
+ import { useSettings } from './composables/useSettings'
20
+ import { useTextSelection } from './composables/useTextSelection'
21
+ import { VA_DATA_ATTR_SELECTOR } from './constants'
22
+ import { copyToClipboard } from './utils/clipboard'
23
+ import { isFixed as checkIsFixed, detectVueComponents, getAccessibilityInfo, getComputedStylesSummary, getNearbyElements, getNearbyText, getRelevantComputedStyles } from './utils/dom-inspector'
24
+ import { createPortalContainer, destroyPortalContainer } from './utils/portal'
25
+ import { getElementName, getElementPath } from './utils/selectors'
26
+ import { boundingBoxToStyle } from './utils/style'
27
+
28
+ const props = withDefaults(defineProps<{
29
+ outputDetail?: OutputDetail
30
+ markerColor?: string
31
+ copyToClipboard?: boolean
32
+ blockPageInteractions?: boolean
33
+ autoHideToolbar?: boolean
34
+ pageUrl?: string
35
+ theme?: 'light' | 'dark' | 'auto'
36
+ activationKey?: 'none' | 'Meta' | 'Alt' | 'Shift'
37
+ }>(), {
38
+ copyToClipboard: true,
39
+ })
40
+
41
+ const emit = defineEmits<{
42
+ 'annotation-add': [annotation: Annotation]
43
+ 'annotation-delete': [annotation: Annotation]
44
+ 'annotation-update': [annotation: Annotation]
45
+ 'annotations-clear': [annotations: Annotation[]]
46
+ 'copy': [markdown: string]
47
+ }>()
48
+
49
+ const HISTORY_CHANGE_EVENT = 'va:history-change'
50
+
51
+ interface VaWindow extends Window {
52
+ __vaHistoryPatched?: boolean
53
+ }
54
+
55
+ function getCurrentUrl() {
56
+ return typeof window !== 'undefined' ? window.location.href : ''
57
+ }
58
+
59
+ function patchHistoryEvents() {
60
+ const win = window as VaWindow
61
+ if (win.__vaHistoryPatched)
62
+ return
63
+ win.__vaHistoryPatched = true
64
+
65
+ const originalPushState = win.history.pushState.bind(win.history)
66
+ const originalReplaceState = win.history.replaceState.bind(win.history)
67
+
68
+ win.history.pushState = function (...args: Parameters<History['pushState']>) {
69
+ originalPushState(...args)
70
+ win.dispatchEvent(new Event(HISTORY_CHANGE_EVENT))
71
+ }
72
+
73
+ win.history.replaceState = function (...args: Parameters<History['replaceState']>) {
74
+ originalReplaceState(...args)
75
+ win.dispatchEvent(new Event(HISTORY_CHANGE_EVENT))
76
+ }
77
+ }
78
+
79
+ // Refs
80
+ const rootEl = ref<HTMLElement | null>(null)
81
+ const overlayEl = ref<HTMLElement | null>(null)
82
+ const toolbarRef = ref<any>(null)
83
+ const currentUrl = ref(props.pageUrl || getCurrentUrl())
84
+
85
+ // Core composables
86
+ const { settings } = useSettings()
87
+ const { mode, transition } = useInteractionMode()
88
+ const { annotations, addAnnotation, removeAnnotation, updateAnnotation, clearAnnotations, setScopeUrl } = useAnnotations(currentUrl.value)
89
+ const { hoveredRect, hoveredName, hoveredComponentChain, onMouseMove, clearHighlight, getElementUnderOverlay, cleanup: cleanupDetection } = useElementDetection(overlayEl, () => settings.showComponentTree)
90
+ const textSelection = useTextSelection(mode)
91
+ const multiSelect = useMultiSelect(mode, transition)
92
+ const areaSelect = useAreaSelect(mode, transition)
93
+ const animPause = useAnimationPause()
94
+ const { recalculatePositions: _recalculatePositions } = useMarkerPositions(annotations)
95
+ const { formatAnnotations } = useOutputFormatter()
96
+
97
+ // Local state
98
+ const pendingPosition = ref<{ x: number, y: number } | null>(null)
99
+ const pendingElementName = ref('')
100
+ const pendingTarget = ref<Element | null>(null)
101
+ const pendingComponentChain = ref<string | undefined>()
102
+ const pendingComputedStyles = ref<Record<string, string> | undefined>()
103
+ const pendingTextSelection = ref<{ text: string, element: Element, rect: { x: number, y: number, width: number, height: number } } | null>(null)
104
+ const editingAnnotation = ref<Annotation | null>(null)
105
+ const settingsOpen = ref(false)
106
+ const settingsAnchorEl = ref<HTMLElement | null>(null)
107
+ const copyFeedback = ref(false)
108
+ const toolbarDragging = ref(false)
109
+ const DRAG_END_SUPPRESSION_MS = 500
110
+ const SETTINGS_CLOSE_SUPPRESSION_MS = 220
111
+ let suppressInteractionsUntil = 0
112
+ const effectiveBlockPageInteractions = computed(() => props.blockPageInteractions ?? settings.blockPageInteractions)
113
+ const rootStyle = computed(() => {
114
+ const hex = settings.markerColor
115
+ if (!hex)
116
+ return undefined
117
+ const r = Number.parseInt(hex.slice(1, 3), 16)
118
+ const g = Number.parseInt(hex.slice(3, 5), 16)
119
+ const b = Number.parseInt(hex.slice(5, 7), 16)
120
+ return {
121
+ '--va-accent': hex,
122
+ '--va-accent-rgb': `${r}, ${g}, ${b}`,
123
+ } as Record<string, string>
124
+ })
125
+ const resolvedUrl = computed(() => currentUrl.value)
126
+ const pendingMarkerX = computed(() => {
127
+ if (!pendingPosition.value)
128
+ return 0
129
+ return (pendingPosition.value.x / window.innerWidth) * 100
130
+ })
131
+ const pendingMarkerY = computed(() => {
132
+ if (!pendingPosition.value)
133
+ return 0
134
+ return pendingPosition.value.y + (window.scrollY || document.documentElement.scrollTop)
135
+ })
136
+ const pendingIsSelection = computed(() => (
137
+ mode.value === 'input-open'
138
+ && !editingAnnotation.value
139
+ && (multiSelect.selectedElements.value.length > 0 || !!areaSelect.areaRect.value)
140
+ ))
141
+
142
+ // Portal setup (Vue 2.7 compat)
143
+ let portalContainer: HTMLElement | null = null
144
+ const isVue2 = _isVue2
145
+
146
+ const PassThrough = defineComponent({
147
+ render() {
148
+ // eslint-disable-next-line vue/require-slots-as-functions -- Vue 2: $slots.default is VNode[], not a function
149
+ const slot = this.$slots.default
150
+ return (typeof slot === 'function' ? slot() : slot?.[0]) || null
151
+ },
152
+ })
153
+
154
+ const portalWrapper = isVue2 ? PassThrough : 'Teleport'
155
+ const portalProps = isVue2 ? {} : { to: 'body' }
156
+
157
+ onMounted(() => {
158
+ if (isVue2 && rootEl.value) {
159
+ portalContainer = createPortalContainer()
160
+ portalContainer.appendChild(rootEl.value)
161
+ }
162
+ })
163
+
164
+ onBeforeUnmount(() => {
165
+ animPause.cleanup()
166
+ cleanupDetection()
167
+ if (portalContainer) {
168
+ destroyPortalContainer(portalContainer)
169
+ }
170
+ })
171
+
172
+ // Apply prop overrides to settings
173
+ watch(() => props.outputDetail, (v) => {
174
+ if (v)
175
+ settings.outputDetail = v
176
+ }, { immediate: true })
177
+ watch(() => props.markerColor, (v) => {
178
+ if (v)
179
+ settings.markerColor = v
180
+ }, { immediate: true })
181
+ watch(() => props.theme, (v) => {
182
+ if (v)
183
+ settings.theme = v
184
+ }, { immediate: true })
185
+ watch(() => props.blockPageInteractions, (v) => {
186
+ if (v)
187
+ settings.blockPageInteractions = v
188
+ }, { immediate: true })
189
+ watch(() => props.autoHideToolbar, (v) => {
190
+ if (v)
191
+ settings.autoHideToolbar = v
192
+ }, { immediate: true })
193
+ watch(() => props.pageUrl, (url) => {
194
+ syncUrlScope(url || getCurrentUrl())
195
+ }, { immediate: true })
196
+ watch(() => props.activationKey, (v) => {
197
+ if (v !== undefined)
198
+ settings.activationKey = v
199
+ }, { immediate: true })
200
+
201
+ // Event handlers
202
+ function onActivate() {
203
+ transition('inspect')
204
+ }
205
+
206
+ function onDeactivate() {
207
+ transition('idle')
208
+ clearHighlight()
209
+ closeSettings(false)
210
+ }
211
+
212
+ function onOverlayMouseMove(e: MouseEvent) {
213
+ if (isInteractionLocked())
214
+ return
215
+ if (mode.value === 'inspect') {
216
+ onMouseMove(e)
217
+ }
218
+ else if (mode.value === 'multi-selecting') {
219
+ multiSelect.onMouseMove(e)
220
+ }
221
+ else if (mode.value === 'area-selecting') {
222
+ areaSelect.onMouseMove(e)
223
+ }
224
+ }
225
+
226
+ function onOverlayMouseDown(e: MouseEvent) {
227
+ if (isInteractionLocked())
228
+ return
229
+ if (multiSelect.onMouseDown(e))
230
+ return
231
+ areaSelect.onMouseDown(e)
232
+ }
233
+
234
+ function onOverlayMouseUp(e: MouseEvent) {
235
+ if (isInteractionLocked())
236
+ return
237
+ if (mode.value === 'multi-selecting') {
238
+ multiSelect.onMouseUp()
239
+ if (multiSelect.selectedElements.value.length > 0) {
240
+ const elements = multiSelect.selectedElements.value
241
+ const rect = multiSelect.selectionRect.value
242
+ pendingPosition.value = rect
243
+ ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
244
+ : { x: e.clientX, y: e.clientY }
245
+ pendingElementName.value = `${elements.length} elements selected`
246
+ pendingComponentChain.value = undefined
247
+ pendingComputedStyles.value = undefined
248
+ pendingTarget.value = null
249
+ transition('input-open')
250
+ }
251
+ else {
252
+ multiSelect.reset()
253
+ transition('inspect')
254
+ }
255
+ return
256
+ }
257
+
258
+ if (mode.value === 'area-selecting') {
259
+ areaSelect.onMouseUp()
260
+ const rect = areaSelect.areaRect.value
261
+ if (rect && rect.width > 10 && rect.height > 10) {
262
+ pendingPosition.value = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
263
+ pendingElementName.value = 'Area selection'
264
+ pendingComponentChain.value = undefined
265
+ pendingComputedStyles.value = undefined
266
+ pendingTarget.value = null
267
+ transition('input-open')
268
+ }
269
+ else {
270
+ areaSelect.reset()
271
+ transition('inspect')
272
+ }
273
+ return
274
+ }
275
+
276
+ if (mode.value !== 'inspect')
277
+ return
278
+
279
+ // Check for text selection first
280
+ const textResult = textSelection.checkTextSelection(e)
281
+ if (textResult) {
282
+ pendingPosition.value = {
283
+ x: textResult.rect.left + textResult.rect.width / 2,
284
+ y: textResult.rect.bottom,
285
+ }
286
+ pendingElementName.value = `"${textResult.selectedText.slice(0, 30)}"`
287
+ pendingComponentChain.value = getVueComponents(textResult.anchorElement)
288
+ pendingComputedStyles.value = getRelevantComputedStyles(textResult.anchorElement)
289
+ pendingTarget.value = textResult.anchorElement
290
+ pendingTextSelection.value = {
291
+ text: textResult.selectedText,
292
+ element: textResult.anchorElement,
293
+ rect: {
294
+ x: textResult.rect.x,
295
+ y: textResult.rect.y,
296
+ width: textResult.rect.width,
297
+ height: textResult.rect.height,
298
+ },
299
+ }
300
+ transition('input-open')
301
+ return
302
+ }
303
+
304
+ // Normal click annotation
305
+ const el = getElementUnderOverlay(e)
306
+ if (!el || el.closest(VA_DATA_ATTR_SELECTOR))
307
+ return
308
+
309
+ pendingPosition.value = { x: e.clientX, y: e.clientY }
310
+ pendingElementName.value = getElementName(el)
311
+ pendingComponentChain.value = settings.showComponentTree ? detectVueComponents(el) : undefined
312
+ pendingComputedStyles.value = getRelevantComputedStyles(el)
313
+ pendingTarget.value = el
314
+ pendingTextSelection.value = null
315
+ transition('input-open')
316
+ }
317
+
318
+ function onOverlayWheel(_e: WheelEvent) {
319
+ if (isInteractionLocked())
320
+ return
321
+ const overlay = overlayEl.value
322
+ if (!overlay)
323
+ return
324
+ const previousPointerEvents = overlay.style.pointerEvents
325
+ overlay.style.pointerEvents = 'none'
326
+ requestAnimationFrame(() => {
327
+ if (overlay)
328
+ overlay.style.pointerEvents = previousPointerEvents
329
+ })
330
+ }
331
+
332
+ function getElementAtPointThroughOverlay(x: number, y: number): Element | null {
333
+ const overlay = overlayEl.value
334
+ if (!overlay)
335
+ return document.elementFromPoint(x, y)
336
+
337
+ const previousPointerEvents = overlay.style.pointerEvents
338
+ overlay.style.pointerEvents = 'none'
339
+ const el = document.elementFromPoint(x, y)
340
+ overlay.style.pointerEvents = previousPointerEvents
341
+ return el
342
+ }
343
+
344
+ function shouldUseDocumentFallbackEvents() {
345
+ return mode.value === 'inspect' && !effectiveBlockPageInteractions.value && !isInteractionLocked()
346
+ }
347
+
348
+ function lockInteractionsTemporarily(durationMs: number) {
349
+ suppressInteractionsUntil = Math.max(suppressInteractionsUntil, Date.now() + durationMs)
350
+ }
351
+
352
+ function isInteractionLocked() {
353
+ return settingsOpen.value || toolbarDragging.value || Date.now() < suppressInteractionsUntil
354
+ }
355
+
356
+ function closeSettings(lockInteractions = true) {
357
+ if (!settingsOpen.value)
358
+ return
359
+ settingsOpen.value = false
360
+ if (lockInteractions)
361
+ lockInteractionsTemporarily(SETTINGS_CLOSE_SUPPRESSION_MS)
362
+ }
363
+
364
+ function syncUrlScope(nextUrl: string) {
365
+ if (!nextUrl || currentUrl.value === nextUrl)
366
+ return
367
+ currentUrl.value = nextUrl
368
+ setScopeUrl(nextUrl)
369
+ }
370
+
371
+ function syncUrlScopeFromWindow() {
372
+ if (props.pageUrl)
373
+ return
374
+ syncUrlScope(getCurrentUrl())
375
+ }
376
+
377
+ function onToolbarDragStart() {
378
+ toolbarDragging.value = true
379
+ }
380
+
381
+ function onToolbarDragEnd() {
382
+ toolbarDragging.value = false
383
+ // Ignore trailing mouseup/click compatibility events right after drag release.
384
+ lockInteractionsTemporarily(DRAG_END_SUPPRESSION_MS)
385
+ }
386
+
387
+ function onDocumentMouseMove(e: MouseEvent) {
388
+ if (!shouldUseDocumentFallbackEvents())
389
+ return
390
+ onOverlayMouseMove(e)
391
+ }
392
+
393
+ function onDocumentMouseDown(e: MouseEvent) {
394
+ if (!shouldUseDocumentFallbackEvents())
395
+ return
396
+ onOverlayMouseDown(e)
397
+ }
398
+
399
+ function onDocumentMouseUp(e: MouseEvent) {
400
+ if (!shouldUseDocumentFallbackEvents())
401
+ return
402
+ onOverlayMouseUp(e)
403
+ }
404
+
405
+ function onDocumentWheel(e: WheelEvent) {
406
+ if (!shouldUseDocumentFallbackEvents())
407
+ return
408
+ onOverlayWheel(e)
409
+ }
410
+
411
+ function onDocumentClick(e: MouseEvent) {
412
+ if (mode.value === 'idle')
413
+ return
414
+ const target = e.target as Element
415
+ if (!target || target.closest(VA_DATA_ATTR_SELECTOR))
416
+ return
417
+ e.preventDefault()
418
+ e.stopPropagation()
419
+ }
420
+
421
+ function getVueComponents(el: Element): string | undefined {
422
+ return settings.showComponentTree
423
+ ? detectVueComponents(el, settings.outputDetail === 'forensic')
424
+ : undefined
425
+ }
426
+
427
+ function resetPendingState() {
428
+ pendingPosition.value = null
429
+ pendingTarget.value = null
430
+ pendingComponentChain.value = undefined
431
+ pendingComputedStyles.value = undefined
432
+ pendingTextSelection.value = null
433
+ editingAnnotation.value = null
434
+ }
435
+
436
+ function onInputAdd(comment: string) {
437
+ if (editingAnnotation.value) {
438
+ onInputSave(comment)
439
+ return
440
+ }
441
+
442
+ const scrollTop = window.scrollY || document.documentElement.scrollTop
443
+ const detail = settings.outputDetail
444
+ const url = resolvedUrl.value
445
+
446
+ if (mode.value === 'input-open' && multiSelect.selectedElements.value.length > 0) {
447
+ // Multi-select annotation
448
+ const selected = multiSelect.selectedElements.value
449
+ const elements = selected.map(el => ({
450
+ element: el.tagName.toLowerCase(),
451
+ elementPath: getElementPath(el),
452
+ cssClasses: Array.from(el.classList).join(' '),
453
+ boundingBox: (() => {
454
+ const r = el.getBoundingClientRect()
455
+ return { x: r.x, y: r.y, width: r.width, height: r.height }
456
+ })(),
457
+ }))
458
+ const firstElement = selected[0]!
459
+ const selectionBox = elements.reduce(
460
+ (acc, el) => {
461
+ const box = el.boundingBox!
462
+ acc.left = Math.min(acc.left, box.x)
463
+ acc.top = Math.min(acc.top, box.y)
464
+ acc.right = Math.max(acc.right, box.x + box.width)
465
+ acc.bottom = Math.max(acc.bottom, box.y + box.height)
466
+ return acc
467
+ },
468
+ { left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity },
469
+ )
470
+ const boundingBox = {
471
+ x: selectionBox.left,
472
+ y: selectionBox.top,
473
+ width: selectionBox.right - selectionBox.left,
474
+ height: selectionBox.bottom - selectionBox.top,
475
+ }
476
+
477
+ const ann = addAnnotation({
478
+ x: pendingPosition.value!.x / window.innerWidth * 100,
479
+ y: pendingPosition.value!.y + scrollTop,
480
+ comment,
481
+ url,
482
+ element: 'multi',
483
+ elementPath: `region at (${Math.round(boundingBox.x)}, ${Math.round(boundingBox.y)})`,
484
+ isMultiSelect: true,
485
+ elements,
486
+ boundingBox,
487
+ vueComponents: getVueComponents(firstElement),
488
+ nearbyElements: getNearbyElements(firstElement),
489
+ nearbyText: getNearbyText(firstElement),
490
+ cssClasses: detail === 'forensic' ? Array.from(firstElement.classList).join(' ') : undefined,
491
+ fullPath: detail === 'forensic' ? getElementPath(firstElement) : undefined,
492
+ computedStyles: detail === 'forensic' ? getComputedStylesSummary(firstElement) : undefined,
493
+ accessibility: detail === 'forensic' ? getAccessibilityInfo(firstElement) : undefined,
494
+ })
495
+ emit('annotation-add', ann)
496
+ multiSelect.reset()
497
+ }
498
+ else if (mode.value === 'input-open' && areaSelect.areaRect.value) {
499
+ // Area annotation
500
+ const area = { ...areaSelect.areaRect.value! }
501
+ const centerX = area.x + area.width / 2
502
+ const centerY = area.y + area.height / 2
503
+ const centerElement = getElementAtPointThroughOverlay(centerX, centerY) || document.body
504
+ const ann = addAnnotation({
505
+ x: centerX / window.innerWidth * 100,
506
+ y: centerY + scrollTop,
507
+ comment,
508
+ url,
509
+ element: 'area',
510
+ elementPath: `region at (${Math.round(area.x)}, ${Math.round(area.y)})`,
511
+ isAreaSelect: true,
512
+ area,
513
+ boundingBox: area,
514
+ vueComponents: getVueComponents(centerElement),
515
+ nearbyElements: getNearbyElements(centerElement),
516
+ nearbyText: getNearbyText(centerElement),
517
+ cssClasses: detail === 'forensic' ? Array.from(centerElement.classList).join(' ') : undefined,
518
+ fullPath: detail === 'forensic' ? getElementPath(centerElement) : undefined,
519
+ computedStyles: detail === 'forensic' ? getComputedStylesSummary(centerElement) : undefined,
520
+ accessibility: detail === 'forensic' ? getAccessibilityInfo(centerElement) : undefined,
521
+ })
522
+ emit('annotation-add', ann)
523
+ areaSelect.reset()
524
+ }
525
+ else if (pendingTextSelection.value) {
526
+ // Text selection annotation
527
+ const { element: el, rect, text } = pendingTextSelection.value
528
+ const ann = addAnnotation({
529
+ x: pendingPosition.value!.x / window.innerWidth * 100,
530
+ y: pendingPosition.value!.y + scrollTop,
531
+ comment,
532
+ url,
533
+ element: el.tagName.toLowerCase(),
534
+ elementPath: getElementPath(el),
535
+ selectedText: text,
536
+ boundingBox: rect,
537
+ vueComponents: getVueComponents(el),
538
+ nearbyElements: getNearbyElements(el),
539
+ nearbyText: getNearbyText(el),
540
+ cssClasses: detail === 'forensic' ? Array.from(el.classList).join(' ') : undefined,
541
+ fullPath: detail === 'forensic' ? getElementPath(el) : undefined,
542
+ computedStyles: detail === 'forensic' ? getComputedStylesSummary(el) : undefined,
543
+ accessibility: detail === 'forensic' ? getAccessibilityInfo(el) : undefined,
544
+ _targetRef: new WeakRef(el),
545
+ })
546
+ emit('annotation-add', ann)
547
+ }
548
+ else if (pendingTarget.value) {
549
+ // Element click annotation
550
+ const el = pendingTarget.value
551
+ const rect = el.getBoundingClientRect()
552
+ const fixed = checkIsFixed(el)
553
+
554
+ const ann = addAnnotation({
555
+ x: pendingPosition.value!.x / window.innerWidth * 100,
556
+ y: fixed ? pendingPosition.value!.y : pendingPosition.value!.y + scrollTop,
557
+ comment,
558
+ url,
559
+ element: el.tagName.toLowerCase(),
560
+ elementPath: getElementPath(el),
561
+ isFixed: fixed,
562
+ _targetRef: new WeakRef(el),
563
+ vueComponents: getVueComponents(el),
564
+ nearbyElements: getNearbyElements(el),
565
+ nearbyText: getNearbyText(el),
566
+ boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
567
+ cssClasses: detail === 'forensic' ? Array.from(el.classList).join(' ') : undefined,
568
+ fullPath: detail === 'forensic' ? getElementPath(el) : undefined,
569
+ computedStyles: detail === 'forensic' ? getComputedStylesSummary(el) : undefined,
570
+ accessibility: detail === 'forensic' ? getAccessibilityInfo(el) : undefined,
571
+ })
572
+ emit('annotation-add', ann)
573
+ }
574
+
575
+ resetPendingState()
576
+ transition('inspect')
577
+ }
578
+
579
+ function onInputCancel() {
580
+ resetPendingState()
581
+ multiSelect.reset()
582
+ areaSelect.reset()
583
+ transition('inspect')
584
+ }
585
+
586
+ async function onCopy() {
587
+ const markdown = formatAnnotations(annotations.value, settings.outputDetail, resolvedUrl.value)
588
+
589
+ if (props.copyToClipboard !== false) {
590
+ const success = await copyToClipboard(markdown)
591
+ if (!success)
592
+ return
593
+ copyFeedback.value = true
594
+ setTimeout(() => {
595
+ copyFeedback.value = false
596
+ }, 2000)
597
+ }
598
+
599
+ emit('copy', markdown)
600
+ if (settings.clearAfterCopy) {
601
+ const cleared = clearAnnotations()
602
+ emit('annotations-clear', cleared)
603
+ }
604
+ }
605
+
606
+ function onClear() {
607
+ const cleared = clearAnnotations()
608
+ emit('annotations-clear', cleared)
609
+ }
610
+
611
+ function onMarkerClick(ann: Annotation) {
612
+ // Open the annotation popup for editing
613
+ const scrollTop = window.scrollY || document.documentElement.scrollTop
614
+ const markerX = (ann.x / 100) * window.innerWidth
615
+ const markerY = ann.isFixed ? ann.y : ann.y - scrollTop
616
+
617
+ editingAnnotation.value = ann
618
+ pendingPosition.value = { x: markerX, y: markerY }
619
+ pendingElementName.value = getElementName(ann._targetRef?.deref() || document.createElement(ann.element))
620
+ pendingComponentChain.value = ann.vueComponents
621
+ pendingComputedStyles.value = ann.computedStyles
622
+ ? Object.fromEntries(ann.computedStyles.split('\n').filter(Boolean).map((line) => {
623
+ const idx = line.indexOf(':')
624
+ return idx > -1 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, '']
625
+ }))
626
+ : ann._targetRef?.deref()
627
+ ? getRelevantComputedStyles(ann._targetRef.deref()!)
628
+ : undefined
629
+ pendingTextSelection.value = null
630
+ pendingTarget.value = ann._targetRef?.deref() || null
631
+ transition('input-open')
632
+ }
633
+
634
+ function onInputDelete() {
635
+ if (!editingAnnotation.value)
636
+ return
637
+ const removed = removeAnnotation(editingAnnotation.value.id)
638
+ if (removed)
639
+ emit('annotation-delete', removed)
640
+ resetPendingState()
641
+ transition('inspect')
642
+ }
643
+
644
+ function onInputSave(comment: string) {
645
+ if (!editingAnnotation.value)
646
+ return
647
+ const updated = updateAnnotation(editingAnnotation.value.id, { comment })
648
+ if (updated)
649
+ emit('annotation-update', updated)
650
+ resetPendingState()
651
+ transition('inspect')
652
+ }
653
+
654
+ function onToggleArea(value: boolean) {
655
+ areaSelect.isAreaMode.value = value
656
+ }
657
+
658
+ function onToolbarPlacementUpdate(value: Settings['toolbarPlacement']) {
659
+ settings.toolbarPlacement = value
660
+ }
661
+
662
+ function onSettingsUpdate(updates: Partial<Settings>) {
663
+ Object.assign(settings, updates)
664
+ }
665
+
666
+ function onOpenSettings(anchorEl: HTMLElement | null) {
667
+ if (settingsOpen.value && settingsAnchorEl.value === anchorEl) {
668
+ closeSettings()
669
+ return
670
+ }
671
+ settingsAnchorEl.value = anchorEl
672
+ settingsOpen.value = true
673
+ }
674
+
675
+ // Keyboard shortcut manager
676
+ const shortcutConfig = computed<KeyboardShortcutConfig>(() => ({
677
+ ...DEFAULT_SHORTCUT_CONFIG,
678
+ doubleTap: {
679
+ ...DEFAULT_SHORTCUT_CONFIG.doubleTap,
680
+ enabled: settings.activationKey !== 'none',
681
+ key: settings.activationKey !== 'none' ? settings.activationKey : DEFAULT_SHORTCUT_CONFIG.doubleTap.key,
682
+ },
683
+ }))
684
+
685
+ useKeyboardShortcuts({
686
+ mode,
687
+ settingsOpen,
688
+ toolbarDragging,
689
+ toolbarRef,
690
+ isInteractionLocked,
691
+ config: shortcutConfig,
692
+ actions: {
693
+ activate: onActivate,
694
+ deactivate: onDeactivate,
695
+ elementSelect: () => onToggleArea(false),
696
+ areaSelect: () => onToggleArea(true),
697
+ pauseAnimations: () => animPause.toggle(),
698
+ copy: () => onCopy(),
699
+ clear: () => onClear(),
700
+ openSettings: () => onOpenSettings(null),
701
+ inputCancel: () => onInputCancel(),
702
+ closeSettings: () => closeSettings(),
703
+ },
704
+ })
705
+
706
+ onMounted(() => {
707
+ patchHistoryEvents()
708
+ window.addEventListener(HISTORY_CHANGE_EVENT, syncUrlScopeFromWindow)
709
+ window.addEventListener('popstate', syncUrlScopeFromWindow)
710
+ window.addEventListener('hashchange', syncUrlScopeFromWindow)
711
+ document.addEventListener('mousemove', onDocumentMouseMove, true)
712
+ document.addEventListener('mousedown', onDocumentMouseDown, true)
713
+ document.addEventListener('mouseup', onDocumentMouseUp, true)
714
+ document.addEventListener('wheel', onDocumentWheel, { passive: true, capture: true })
715
+ document.addEventListener('click', onDocumentClick, true)
716
+ })
717
+
718
+ onBeforeUnmount(() => {
719
+ window.removeEventListener(HISTORY_CHANGE_EVENT, syncUrlScopeFromWindow)
720
+ window.removeEventListener('popstate', syncUrlScopeFromWindow)
721
+ window.removeEventListener('hashchange', syncUrlScopeFromWindow)
722
+ document.removeEventListener('mousemove', onDocumentMouseMove, true)
723
+ document.removeEventListener('mousedown', onDocumentMouseDown, true)
724
+ document.removeEventListener('mouseup', onDocumentMouseUp, true)
725
+ document.removeEventListener('wheel', onDocumentWheel, true)
726
+ document.removeEventListener('click', onDocumentClick, true)
727
+ })
728
+ </script>
729
+
730
+ <template>
731
+ <component :is="portalWrapper" v-bind="portalProps">
732
+ <div ref="rootEl" data-agentation-vue :data-va-theme="settings.theme !== 'auto' ? settings.theme : undefined" :style="rootStyle">
733
+ <!-- Intercept overlay -->
734
+ <div
735
+ v-if="mode !== 'idle'"
736
+ ref="overlayEl"
737
+ class="__va-intercept"
738
+ :class="{ '__va-intercept--input-open': mode === 'input-open' }"
739
+ :style="mode === 'inspect' && !effectiveBlockPageInteractions ? { pointerEvents: 'none' } : undefined"
740
+ @mousemove="onOverlayMouseMove"
741
+ @mousedown="onOverlayMouseDown"
742
+ @mouseup="onOverlayMouseUp"
743
+ @wheel.passive="onOverlayWheel"
744
+ />
745
+
746
+ <!-- Hover highlight -->
747
+ <ElementHighlight
748
+ :rect="hoveredRect"
749
+ :element-name="hoveredName"
750
+ :component-chain="hoveredComponentChain"
751
+ :visible="mode === 'inspect' && !!hoveredRect"
752
+ />
753
+
754
+ <!-- Selection rectangle (multi or area) -->
755
+ <div
756
+ v-if="multiSelect.selectionRect.value"
757
+ class="__va-selection-rect"
758
+ :style="boundingBoxToStyle(multiSelect.selectionRect.value)"
759
+ />
760
+ <div
761
+ v-if="areaSelect.areaRect.value"
762
+ class="__va-selection-rect"
763
+ :style="boundingBoxToStyle(areaSelect.areaRect.value)"
764
+ />
765
+
766
+ <!-- Annotation markers -->
767
+ <AnnotationMarker
768
+ v-for="(ann, i) in annotations"
769
+ :key="ann.id"
770
+ :number="i + 1"
771
+ :x="ann.x"
772
+ :y="ann.y"
773
+ :is-fixed="ann.isFixed"
774
+ :is-stale="!ann._targetRef?.deref() && !!ann._targetRef"
775
+ :is-selection="!!(ann.isAreaSelect || ann.isMultiSelect)"
776
+ @click="onMarkerClick(ann)"
777
+ />
778
+
779
+ <!-- Pending marker (unsaved annotation) -->
780
+ <AnnotationMarker
781
+ v-if="mode === 'input-open' && pendingPosition && !editingAnnotation"
782
+ :number="annotations.length + 1"
783
+ :x="pendingMarkerX"
784
+ :y="pendingMarkerY"
785
+ :is-pending="true"
786
+ :is-selection="pendingIsSelection"
787
+ />
788
+
789
+ <!-- Annotation input -->
790
+ <AnnotationInput
791
+ v-if="mode === 'input-open' && pendingPosition"
792
+ :position="pendingPosition"
793
+ :element-name="pendingElementName"
794
+ :component-chain="pendingComponentChain"
795
+ :computed-styles="pendingComputedStyles"
796
+ :initial-comment="editingAnnotation?.comment"
797
+ :is-editing="!!editingAnnotation"
798
+ @add="onInputAdd"
799
+ @cancel="onInputCancel"
800
+ @delete="onInputDelete"
801
+ />
802
+
803
+ <!-- Settings panel -->
804
+ <SettingsPopover
805
+ :open="settingsOpen"
806
+ :anchor-el="settingsAnchorEl"
807
+ :settings="settings"
808
+ @update="onSettingsUpdate"
809
+ @close="closeSettings()"
810
+ />
811
+
812
+ <!-- Copy feedback -->
813
+ <div v-if="copyFeedback" class="__va-copy-feedback">
814
+ Copied!
815
+ </div>
816
+
817
+ <!-- Toolbar -->
818
+ <AgentationToolbar
819
+ ref="toolbarRef"
820
+ :mode="mode"
821
+ :annotation-count="annotations.length"
822
+ :is-paused="animPause.isPaused.value"
823
+ :is-area-mode="areaSelect.isAreaMode.value"
824
+ :auto-hide-enabled="settings.autoHideToolbar"
825
+ :placement="settings.toolbarPlacement"
826
+ @activate="onActivate"
827
+ @deactivate="onDeactivate"
828
+ @copy="onCopy"
829
+ @clear="onClear"
830
+ @toggle-pause="animPause.toggle"
831
+ @toggle-area="onToggleArea"
832
+ @update:placement="onToolbarPlacementUpdate"
833
+ @open-settings="onOpenSettings"
834
+ @drag-start="onToolbarDragStart"
835
+ @drag-end="onToolbarDragEnd"
836
+ />
837
+ </div>
838
+ </component>
839
+ </template>