@web-auto/camo 0.1.26 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +586 -586
  3. package/bin/browser-service.mjs +11 -11
  4. package/bin/camo.mjs +22 -22
  5. package/package.json +48 -48
  6. package/scripts/build.mjs +19 -19
  7. package/scripts/bump-version.mjs +34 -34
  8. package/scripts/check-file-size.mjs +80 -80
  9. package/scripts/file-size-policy.json +12 -2
  10. package/scripts/install.mjs +76 -76
  11. package/scripts/release.sh +54 -54
  12. package/src/autoscript/action-providers/index.mjs +6 -6
  13. package/src/autoscript/impact-engine.mjs +78 -78
  14. package/src/autoscript/runtime.mjs +1017 -1017
  15. package/src/autoscript/schema.mjs +376 -376
  16. package/src/cli.mjs +405 -405
  17. package/src/commands/attach.mjs +141 -141
  18. package/src/commands/autoscript.mjs +1011 -1011
  19. package/src/commands/browser.mjs +1255 -1257
  20. package/src/commands/container.mjs +401 -401
  21. package/src/commands/cookies.mjs +69 -69
  22. package/src/commands/create.mjs +98 -98
  23. package/src/commands/devtools.mjs +349 -349
  24. package/src/commands/events.mjs +152 -152
  25. package/src/commands/highlight-mode.mjs +24 -24
  26. package/src/commands/init.mjs +68 -68
  27. package/src/commands/lifecycle.mjs +275 -275
  28. package/src/commands/mouse.mjs +45 -45
  29. package/src/commands/profile.mjs +46 -46
  30. package/src/commands/record.mjs +115 -115
  31. package/src/commands/system.mjs +14 -14
  32. package/src/commands/window.mjs +123 -123
  33. package/src/container/change-notifier.mjs +362 -362
  34. package/src/container/element-filter.mjs +143 -143
  35. package/src/container/index.mjs +3 -3
  36. package/src/container/runtime-core/checkpoint.mjs +209 -209
  37. package/src/container/runtime-core/index.mjs +21 -21
  38. package/src/container/runtime-core/operations/index.mjs +774 -774
  39. package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
  40. package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
  41. package/src/container/runtime-core/operations/viewport.mjs +189 -189
  42. package/src/container/runtime-core/search.mjs +190 -190
  43. package/src/container/runtime-core/subscription.mjs +224 -224
  44. package/src/container/runtime-core/utils.mjs +94 -94
  45. package/src/container/runtime-core/validation.mjs +127 -184
  46. package/src/container/runtime-core.mjs +1 -1
  47. package/src/container/subscription-registry.mjs +459 -459
  48. package/src/core/actions.mjs +561 -561
  49. package/src/core/browser.mjs +266 -266
  50. package/src/core/index.mjs +52 -52
  51. package/src/core/utils.mjs +91 -91
  52. package/src/events/daemon-entry.mjs +33 -33
  53. package/src/events/daemon.mjs +80 -80
  54. package/src/events/progress-log.mjs +109 -109
  55. package/src/events/ws-server.mjs +239 -239
  56. package/src/lib/client.mjs +200 -200
  57. package/src/lifecycle/cleanup.mjs +83 -83
  58. package/src/lifecycle/lock.mjs +126 -126
  59. package/src/lifecycle/session-registry.mjs +279 -279
  60. package/src/lifecycle/session-view.mjs +76 -76
  61. package/src/lifecycle/session-watchdog.mjs +281 -281
  62. package/src/services/browser-service/index.js +671 -674
  63. package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
  64. package/src/services/browser-service/internal/BrowserSession.js +325 -336
  65. package/src/services/browser-service/internal/ElementRegistry.js +60 -60
  66. package/src/services/browser-service/internal/ProfileLock.js +84 -84
  67. package/src/services/browser-service/internal/SessionManager.js +184 -184
  68. package/src/services/browser-service/internal/SessionManager.test.js +39 -39
  69. package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
  70. package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
  71. package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
  72. package/src/services/browser-service/internal/browser-session/logging.js +46 -46
  73. package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
  74. package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
  75. package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
  76. package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
  77. package/src/services/browser-service/internal/browser-session/recording.js +198 -198
  78. package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
  79. package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
  80. package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
  81. package/src/services/browser-service/internal/browser-session/types.js +14 -14
  82. package/src/services/browser-service/internal/browser-session/utils.js +95 -95
  83. package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
  84. package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
  85. package/src/services/browser-service/internal/container-matcher.js +851 -851
  86. package/src/services/browser-service/internal/container-registry.js +182 -182
  87. package/src/services/browser-service/internal/engine-manager.js +259 -259
  88. package/src/services/browser-service/internal/fingerprint.js +203 -203
  89. package/src/services/browser-service/internal/heartbeat.js +137 -137
  90. package/src/services/browser-service/internal/logging.js +46 -46
  91. package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
  92. package/src/services/browser-service/internal/pageRuntime.js +28 -28
  93. package/src/services/browser-service/internal/runtimeInjector.js +31 -31
  94. package/src/services/browser-service/internal/service-process-logger.js +140 -140
  95. package/src/services/browser-service/internal/state-bus.js +45 -45
  96. package/src/services/browser-service/internal/storage-paths.js +42 -42
  97. package/src/services/browser-service/internal/ws-server.js +1194 -1194
  98. package/src/services/browser-service/internal/ws-server.test.js +58 -58
  99. package/src/services/browser-service/server.mjs +6 -6
  100. package/src/services/controller/cli-bridge.js +93 -93
  101. package/src/services/controller/container-index.js +50 -50
  102. package/src/services/controller/container-storage.js +36 -36
  103. package/src/services/controller/controller-actions.js +207 -207
  104. package/src/services/controller/controller.js +1138 -1138
  105. package/src/services/controller/selectors.js +54 -54
  106. package/src/services/controller/transport.js +125 -125
  107. package/src/utils/args.mjs +26 -26
  108. package/src/utils/browser-service.mjs +544 -544
  109. package/src/utils/command-log.mjs +64 -64
  110. package/src/utils/config.mjs +214 -214
  111. package/src/utils/fingerprint.mjs +181 -181
  112. package/src/utils/help.mjs +216 -216
  113. package/src/utils/js-policy.mjs +13 -13
  114. package/src/utils/ws-client.mjs +30 -30
  115. package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
  116. package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
  117. package/src/services/browser-service/index.js.bak +0 -671
@@ -1,1317 +1,1317 @@
1
- /* eslint-disable no-var */
2
- // __DOM_PICKER_INLINE_START
3
- /* DOM picker runtime: window.__domPicker
4
- * Responsible for pointer -> DOM -> highlight -> selection.
5
- */
6
- (() => {
7
- if (typeof window === 'undefined') return;
8
-
9
- const DEFAULT_TIMEOUT = 25000;
10
- const HOVER_STYLE = '2px dashed #fbbc05';
11
- const HIGHLIGHT_CHANNEL = '__camo_dom_picker';
12
-
13
- const domPickerState = {
14
- active: false,
15
- phase: 'idle', // 'idle' | 'hovering' | 'selected' | 'cancelled' | 'timeout'
16
- lastHover: null,
17
- selection: null,
18
- error: null,
19
- updatedAt: Date.now(),
20
- };
21
-
22
- const overlayApi = () => {
23
- const runtime = window.__camoRuntime;
24
- if (!runtime || !runtime.highlight || !runtime.highlight.highlightElements) {
25
- return null;
26
- }
27
- return runtime.highlight;
28
- };
29
-
30
- const setState = (patch) => {
31
- Object.assign(domPickerState, patch, { updatedAt: Date.now() });
32
- };
33
-
34
- const pickerShield = createPickerShield();
35
-
36
- function createPickerShield() {
37
- if (typeof window === 'undefined') return null;
38
- const SHIELD_CLASS = '__camo_picker_shield__';
39
- const instances = new Map();
40
- const observers = new Map();
41
- const frameLoadListeners = new Map();
42
- let callbacksRef = null;
43
-
44
- class ShieldInstance {
45
- constructor(frameWindow, frameElement, callbacks) {
46
- this.frameWindow = frameWindow;
47
- this.frameElement = frameElement || null;
48
- this.callbacks = callbacks || {};
49
- this.layer = null;
50
- this.bindings = null;
51
- }
52
-
53
- mount() {
54
- const doc = this.getDocument();
55
- if (!doc) return;
56
- if (this.layer && doc.contains(this.layer)) {
57
- return;
58
- }
59
- this.destroy();
60
- if (doc.readyState === 'loading') {
61
- doc.addEventListener('DOMContentLoaded', () => this.createLayer(doc), { once: true });
62
- } else {
63
- this.createLayer(doc);
64
- }
65
- }
66
-
67
- destroy() {
68
- this.unbindEvents();
69
- if (this.layer && this.layer.parentNode) {
70
- this.layer.parentNode.removeChild(this.layer);
71
- }
72
- this.layer = null;
73
- }
74
-
75
- getDocument() {
76
- try {
77
- return this.frameWindow.document;
78
- } catch {
79
- return null;
80
- }
81
- }
82
-
83
- createLayer(doc) {
84
- const host = doc.body || doc.documentElement;
85
- if (!host) return;
86
- const layer = doc.createElement('div');
87
- layer.className = SHIELD_CLASS;
88
- const style = layer.style;
89
- style.setProperty('position', 'fixed', 'important');
90
- style.setProperty('inset', '0', 'important');
91
- style.setProperty('width', '100vw', 'important');
92
- style.setProperty('height', '100vh', 'important');
93
- style.setProperty('z-index', '2147483646', 'important');
94
- style.setProperty('pointer-events', 'auto', 'important');
95
- style.setProperty('background', 'transparent', 'important');
96
- style.setProperty('cursor', 'crosshair', 'important');
97
- style.setProperty('touch-action', 'none', 'important');
98
- style.setProperty('user-select', 'none', 'important');
99
- style.setProperty('contain', 'strict', 'important');
100
- host.appendChild(layer);
101
- this.layer = layer;
102
- this.bindEvents();
103
- }
104
-
105
- bindEvents() {
106
- if (!this.layer) return;
107
- const win = this.frameWindow;
108
- const pointerMove = (e) => {
109
- this.consumeEvent(e);
110
- const target = this.resolveTarget(e);
111
- this.callbacks.onHover?.({
112
- target,
113
- event: e,
114
- frameWindow: this.frameWindow,
115
- frameElement: this.frameElement,
116
- });
117
- };
118
- const pointerDown = (e) => {
119
- this.consumeEvent(e);
120
- const target = this.resolveTarget(e);
121
- this.callbacks.onPointerDown?.({
122
- target,
123
- event: e,
124
- frameWindow: this.frameWindow,
125
- frameElement: this.frameElement,
126
- });
127
- };
128
- const pointerUp = (e) => {
129
- this.consumeEvent(e);
130
- const target = this.resolveTarget(e);
131
- this.callbacks.onPointerUp?.({
132
- target,
133
- event: e,
134
- frameWindow: this.frameWindow,
135
- frameElement: this.frameElement,
136
- });
137
- };
138
- const click = (e) => {
139
- this.consumeEvent(e);
140
- const target = this.resolveTarget(e);
141
- this.callbacks.onClick?.({
142
- target,
143
- event: e,
144
- frameWindow: this.frameWindow,
145
- frameElement: this.frameElement,
146
- });
147
- };
148
- const contextMenu = (e) => {
149
- this.consumeEvent(e);
150
- };
151
- win.addEventListener('pointermove', pointerMove, true);
152
- win.addEventListener('pointerdown', pointerDown, true);
153
- win.addEventListener('pointerup', pointerUp, true);
154
- win.addEventListener('pointercancel', pointerUp, true);
155
- win.addEventListener('click', click, true);
156
- win.addEventListener('contextmenu', contextMenu, true);
157
- this.bindings = { pointerMove, pointerDown, pointerUp, click, contextMenu };
158
- }
159
-
160
- unbindEvents() {
161
- if (!this.bindings) return;
162
- const { pointerMove, pointerDown, pointerUp, click, contextMenu } = this.bindings;
163
- const win = this.frameWindow;
164
- win.removeEventListener('pointermove', pointerMove, true);
165
- win.removeEventListener('pointerdown', pointerDown, true);
166
- win.removeEventListener('pointerup', pointerUp, true);
167
- win.removeEventListener('pointercancel', pointerUp, true);
168
- win.removeEventListener('click', click, true);
169
- win.removeEventListener('contextmenu', contextMenu, true);
170
- this.bindings = null;
171
- }
172
-
173
- consumeEvent(e) {
174
- if (!e) return;
175
- try {
176
- e.preventDefault();
177
- e.stopPropagation();
178
- if (typeof e.stopImmediatePropagation === 'function') {
179
- e.stopImmediatePropagation();
180
- }
181
- } catch {
182
- /* ignore */
183
- }
184
- }
185
-
186
- resolveTarget(e) {
187
- if (!e) return null;
188
- const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
189
- let fallbackCandidate = null;
190
- for (const node of path) {
191
- if (node instanceof Element && !this.isShieldElement(node)) {
192
- if (!fallbackCandidate) {
193
- fallbackCandidate = node;
194
- }
195
- const tag = node.tagName ? node.tagName.toLowerCase() : '';
196
- if (tag && tag !== 'html' && tag !== 'body') {
197
- return node;
198
- }
199
- }
200
- }
201
- const maybeTarget = e.target instanceof Element ? e.target : null;
202
- if (maybeTarget && !this.isShieldElement(maybeTarget)) {
203
- const tag = maybeTarget.tagName ? maybeTarget.tagName.toLowerCase() : '';
204
- if (tag && tag !== 'html' && tag !== 'body') {
205
- return maybeTarget;
206
- }
207
- if (!fallbackCandidate) {
208
- fallbackCandidate = maybeTarget;
209
- }
210
- }
211
- const alt = this.elementFromPointSafely(e);
212
- if (alt && !this.isShieldElement(alt)) {
213
- return alt;
214
- }
215
- return fallbackCandidate;
216
- }
217
-
218
- isShieldElement(el) {
219
- if (!(el instanceof Element)) return false;
220
- return el.classList.contains(SHIELD_CLASS);
221
- }
222
-
223
- elementFromPointSafely(e) {
224
- const doc = this.getDocument();
225
- if (!doc || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return null;
226
- const originalDisplay = this.layer ? this.layer.style.display : '';
227
- if (this.layer) {
228
- this.layer.style.display = 'none';
229
- }
230
- let result = null;
231
- try {
232
- const candidate = doc.elementFromPoint(e.clientX, e.clientY);
233
- if (candidate instanceof Element && !this.isShieldElement(candidate)) {
234
- result = candidate;
235
- }
236
- } catch {
237
- result = null;
238
- } finally {
239
- if (this.layer) {
240
- this.layer.style.display = originalDisplay || '';
241
- }
242
- }
243
- return result;
244
- }
245
- }
246
-
247
- const attachToWindow = (targetWindow, frameElement) => {
248
- if (!callbacksRef || instances.has(targetWindow)) return;
249
- const instance = new ShieldInstance(targetWindow, frameElement, callbacksRef);
250
- instances.set(targetWindow, instance);
251
- instance.mount();
252
- observeFrames(targetWindow);
253
- };
254
-
255
- const observeFrames = (targetWindow) => {
256
- const doc = getDocumentSafe(targetWindow);
257
- if (!doc) return;
258
- const observer = new MutationObserver(() => scanFrames(targetWindow));
259
- observer.observe(doc.documentElement || doc, { childList: true, subtree: true });
260
- observers.set(targetWindow, observer);
261
- scanFrames(targetWindow);
262
- };
263
-
264
- const scanFrames = (targetWindow) => {
265
- const doc = getDocumentSafe(targetWindow);
266
- if (!doc) return;
267
- const frames = Array.from(doc.querySelectorAll('iframe'));
268
- frames.forEach((frame) => tryAttachFrame(frame));
269
- };
270
-
271
- const tryAttachFrame = (frame) => {
272
- if (!frame || !frame.contentWindow) return;
273
- const childWindow = frame.contentWindow;
274
- if (instances.has(childWindow)) {
275
- instances.get(childWindow)?.mount();
276
- return;
277
- }
278
- try {
279
- // eslint-disable-next-line no-unused-expressions
280
- childWindow.document;
281
- } catch {
282
- markFrameUnavailable(frame);
283
- return;
284
- }
285
- attachToWindow(childWindow, frame);
286
- frame.dataset.__camoPickerAttached = 'true';
287
- const reloadHandler = () => tryAttachFrame(frame);
288
- frame.addEventListener('load', reloadHandler);
289
- frameLoadListeners.set(frame, reloadHandler);
290
- };
291
-
292
- const markFrameUnavailable = (frame) => {
293
- frame.dataset.__camoPickerBlocked = 'true';
294
- if (callbacksRef?.onFrameBlocked) {
295
- try {
296
- callbacksRef.onFrameBlocked(frame);
297
- } catch {
298
- /* ignore */
299
- }
300
- }
301
- };
302
-
303
- const detachAll = () => {
304
- instances.forEach((instance) => instance.destroy());
305
- instances.clear();
306
- observers.forEach((observer) => observer.disconnect());
307
- observers.clear();
308
- frameLoadListeners.forEach((listener, frame) => {
309
- frame.removeEventListener('load', listener);
310
- });
311
- frameLoadListeners.clear();
312
- };
313
-
314
- const getDocumentSafe = (targetWindow) => {
315
- try {
316
- return targetWindow.document;
317
- } catch {
318
- return null;
319
- }
320
- };
321
-
322
- return {
323
- attach(cb) {
324
- detachAll();
325
- callbacksRef = cb || null;
326
- if (!callbacksRef) return;
327
- attachToWindow(window, null);
328
- },
329
- detach() {
330
- detachAll();
331
- callbacksRef = null;
332
- },
333
- };
334
- }
335
-
336
- const core = {
337
- _session: null,
338
-
339
- _ensureStopped() {
340
- if (!this._session) return;
341
- try {
342
- this._session.stop();
343
- } catch {}
344
- this._session = null;
345
- },
346
-
347
- startSession(options) {
348
- const mode = (options && options.mode) || 'hover-select';
349
- const timeoutMs = Math.min(Math.max(Number(options && options.timeoutMs) || DEFAULT_TIMEOUT, 1000), 60000);
350
- const rootSelector = (options && (options.rootSelector || options.root_selector)) || null;
351
-
352
- this._ensureStopped();
353
-
354
- const session = createSession({ mode, timeoutMs, rootSelector });
355
- this._session = session;
356
- session.start();
357
- return this.getLastState();
358
- },
359
-
360
- cancel() {
361
- if (this._session) {
362
- this._session.cancel('cancelled-by-host');
363
- }
364
- return this.getLastState();
365
- },
366
-
367
- getLastState() {
368
- // return shallow-cloned, read-only-ish snapshot
369
- return JSON.parse(JSON.stringify(domPickerState));
370
- },
371
- };
372
-
373
- function createSession(config) {
374
- const mode = config.mode || 'hover-select';
375
- const timeoutMs = config.timeoutMs || DEFAULT_TIMEOUT;
376
- const rootSelector = config.rootSelector || null;
377
-
378
- let active = false;
379
- let timeoutToken = null;
380
-
381
- let lastHoverEl = null;
382
- let shieldActive = false;
383
-
384
- const listeners = [];
385
-
386
- const detachShield = () => {
387
- if (pickerShield && shieldActive) {
388
- try {
389
- pickerShield.detach();
390
- } catch {
391
- /* ignore */
392
- }
393
- shieldActive = false;
394
- }
395
- };
396
-
397
- const addListener = (target, type, handler, options) => {
398
- target.addEventListener(type, handler, options || true);
399
- listeners.push(() => {
400
- try {
401
- target.removeEventListener(type, handler, options || true);
402
- } catch {}
403
- });
404
- };
405
-
406
- const clearHighlight = () => {
407
- const api = overlayApi();
408
- if (!api || !api.clear) return;
409
- try {
410
- api.clear(HIGHLIGHT_CHANNEL);
411
- } catch {}
412
- };
413
-
414
- const highlightEl = (el) => {
415
- const api = overlayApi();
416
- if (!api || !api.highlightElements) return;
417
- if (!el) {
418
- clearHighlight();
419
- return;
420
- }
421
- try {
422
- api.highlightElements([el], {
423
- channel: HIGHLIGHT_CHANNEL,
424
- style: HOVER_STYLE,
425
- duration: 0,
426
- sticky: true,
427
- maxMatches: 1,
428
- });
429
- } catch {}
430
- };
431
-
432
- const isRootLike = (el) => {
433
- if (!el || !el.tagName) return false;
434
- const tag = el.tagName.toLowerCase();
435
- if (tag === 'html' || tag === 'body') return true;
436
- if (el.id && el.id.toLowerCase() === 'app') return true;
437
- return false;
438
- };
439
-
440
- const extractSelector = (el) => {
441
- if (!el) return null;
442
- if (el.id) return `#${el.id}`;
443
- if (el.classList && el.classList.length) {
444
- return `${el.tagName.toLowerCase()}.${Array.from(el.classList).join('.')}`;
445
- }
446
- return el.tagName ? el.tagName.toLowerCase() : null;
447
- };
448
-
449
- const extractPath = (el) => {
450
- if (!el) return null;
451
- try {
452
- const runtime = window.__camoRuntime;
453
- if (!runtime || !runtime.dom || !runtime.dom.buildPathForElement) {
454
- console.warn('[dom-picker] buildPathForElement missing');
455
- return null;
456
- }
457
- console.log('[dom-picker] extractPath rootSelector:', rootSelector);
458
- const path = runtime.dom.buildPathForElement(el, rootSelector);
459
- console.log('[dom-picker] extractPath result:', path);
460
- return path;
461
- } catch (err) {
462
- console.warn('[dom-picker] extractPath error', err);
463
- return null;
464
- }
465
- };
466
-
467
- const extractRect = (el) => {
468
- if (!el || !el.getBoundingClientRect) return null;
469
- const rect = el.getBoundingClientRect();
470
- return {
471
- x: Math.round(rect.left),
472
- y: Math.round(rect.top),
473
- width: Math.round(rect.width),
474
- height: Math.round(rect.height),
475
- };
476
- };
477
-
478
- const extractText = (el) => {
479
- if (!el) return '';
480
- return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
481
- };
482
-
483
- const pickElementFromPoint = (x, y, event) => {
484
- const elements = (typeof document.elementsFromPoint === 'function'
485
- ? document.elementsFromPoint(x, y)
486
- : []);
487
- const stack = Array.isArray(elements) ? elements : [];
488
-
489
- const rejectSet = new Set();
490
-
491
- // Exclude overlays created by our highlight layer
492
- const overlayLayer = document.getElementById('__camo_highlight_layer');
493
- if (overlayLayer && overlayLayer.contains) {
494
- stack.forEach((el) => {
495
- if (overlayLayer.contains(el)) rejectSet.add(el);
496
- });
497
- }
498
-
499
- for (let i = 0; i < stack.length; i += 1) {
500
- const el = stack[i];
501
- if (!(el instanceof Element)) continue;
502
- if (rejectSet.has(el)) continue;
503
- if (isRootLike(el)) continue;
504
- return el;
505
- }
506
-
507
- // Fallback using elementFromPoint
508
- const fallback = document.elementFromPoint(x, y);
509
- if (fallback instanceof Element && !isRootLike(fallback)) {
510
- if (!rejectSet.has(fallback)) return fallback;
511
- }
512
-
513
- // Last resort: event target
514
- if (event && event.target && event.target instanceof Element && !isRootLike(event.target)) {
515
- return event.target;
516
- }
517
-
518
- return null;
519
- };
520
-
521
- const updateHover = (el) => {
522
- lastHoverEl = el;
523
- if (!el) {
524
- setState({ phase: active ? 'hovering' : domPickerState.phase, lastHover: null });
525
- clearHighlight();
526
- return;
527
- }
528
- const path = extractPath(el);
529
- const selector = extractSelector(el);
530
- setState({
531
- phase: 'hovering',
532
- lastHover: {
533
- path: path || null,
534
- selector: selector || null,
535
- },
536
- });
537
- highlightEl(el);
538
- };
539
-
540
- const finalize = (result) => {
541
- console.log('[dom-picker] finalize called', result);
542
- if (!active) return;
543
- active = false;
544
- if (timeoutToken) {
545
- clearTimeout(timeoutToken);
546
- timeoutToken = null;
547
- }
548
- detachShield();
549
- listeners.forEach((fn) => {
550
- try {
551
- fn();
552
- } catch {}
553
- });
554
- clearHighlight();
555
-
556
- if (result && result.type === 'timeout') {
557
- setState({ phase: 'timeout', error: null, active: false });
558
- return;
559
- }
560
- if (result && result.type === 'cancel') {
561
- setState({ phase: 'cancelled', error: null, active: false });
562
- return;
563
- }
564
- if (result && result.type === 'error') {
565
- setState({ phase: 'cancelled', error: result.error || 'unknown-error', active: false });
566
- return;
567
- }
568
- if (result && result.type === 'select' && result.element) {
569
- const el = result.element;
570
- console.log('[dom-picker] finalize select element', el);
571
- const path = extractPath(el);
572
- console.log('[dom-picker] finalize path', path);
573
- const selector = extractSelector(el);
574
- const rect = extractRect(el);
575
- const text = extractText(el);
576
- const tag = el.tagName ? el.tagName.toLowerCase() : '';
577
- const id = el.id || null;
578
- const classes = Array.from(el.classList || []);
579
-
580
- setState({
581
- phase: 'selected',
582
- active: false,
583
- selection: {
584
- path: path || '',
585
- selector: selector || '',
586
- rect: rect || { x: 0, y: 0, width: 0, height: 0 },
587
- text,
588
- tag,
589
- id,
590
- classes,
591
- },
592
- });
593
- return;
594
- }
595
-
596
- // default: cancelled
597
- setState({ phase: 'cancelled', active: false });
598
- };
599
-
600
- const onPointerMove = (event, forcedTarget) => {
601
- if (!active) return;
602
- if (forcedTarget) {
603
- updateHover(forcedTarget);
604
- return;
605
- }
606
- const x = event?.clientX;
607
- const y = event?.clientY;
608
- if (typeof x === 'number' && typeof y === 'number') {
609
- const el = pickElementFromPoint(x, y, event);
610
- updateHover(el);
611
- } else {
612
- updateHover(null);
613
- }
614
- };
615
-
616
- const commitElement = (el, event) => {
617
- const target = el || lastHoverEl;
618
- if (!target && event) {
619
- const fallback = pickElementFromPoint(event.clientX, event.clientY, event);
620
- if (fallback) {
621
- finalize({ type: 'select', element: fallback });
622
- return;
623
- }
624
- }
625
- if (!target) {
626
- finalize({ type: 'error', error: 'no-element' });
627
- return;
628
- }
629
- finalize({ type: 'select', element: target });
630
- };
631
-
632
- const commitFromEvent = (event, forcedTarget) => {
633
- if (!event && !forcedTarget) {
634
- commitElement(null, null);
635
- return;
636
- }
637
- if (forcedTarget) {
638
- commitElement(forcedTarget, event || null);
639
- return;
640
- }
641
- const x = event?.clientX;
642
- const y = event?.clientY;
643
- const el = typeof x === 'number' && typeof y === 'number' ? pickElementFromPoint(x, y, event) : null;
644
- commitElement(el, event || null);
645
- };
646
-
647
- const onPointerDown = (event, forcedTarget) => {
648
- if (!active) return;
649
- if (event.button !== undefined && event.button !== 0) return;
650
- event.preventDefault();
651
- event.stopPropagation();
652
- commitFromEvent(event, forcedTarget || null);
653
- };
654
-
655
- const onMouseDown = (event, forcedTarget) => {
656
- if (!active) return;
657
- if (event.button !== 0) return; // left button only
658
- event.preventDefault();
659
- event.stopPropagation();
660
- commitFromEvent(event, forcedTarget || null);
661
- };
662
-
663
- const onKeyDown = (event) => {
664
- if (!active) return;
665
- if (event.key === 'Escape') {
666
- event.preventDefault();
667
- event.stopPropagation();
668
- finalize({ type: 'cancel' });
669
- }
670
- };
671
-
672
- const onWindowBlur = () => {
673
- if (!active) return;
674
- finalize({ type: 'cancel' });
675
- };
676
-
677
- const onWindowMouseOut = (event) => {
678
- if (!active) return;
679
- const next = event.relatedTarget || event.toElement;
680
- if (!next || next === window || next === document) {
681
- updateHover(null);
682
- }
683
- };
684
-
685
- const onScroll = () => {
686
- if (!active) return;
687
- if (lastHoverEl) highlightEl(lastHoverEl);
688
- };
689
-
690
- return {
691
- start() {
692
- active = true;
693
- setState({
694
- active: true,
695
- phase: 'hovering',
696
- lastHover: null,
697
- selection: null,
698
- error: null,
699
- });
700
-
701
- if (pickerShield) {
702
- let attached = false;
703
- try {
704
- pickerShield.attach({
705
- onHover: ({ target, event }) => {
706
- onPointerMove(event || null, target || null);
707
- },
708
- onPointerDown: ({ target, event }) => {
709
- onPointerDown(event || { button: 0 }, target || null);
710
- },
711
- onPointerUp: () => {
712
- /* no-op */
713
- },
714
- onClick: ({ target, event }) => {
715
- if (event) {
716
- try {
717
- event.preventDefault();
718
- event.stopPropagation();
719
- } catch {
720
- /* ignore */
721
- }
722
- }
723
- commitFromEvent(event || null, target || null);
724
- },
725
- onFrameBlocked: (frame) => {
726
- try {
727
- // eslint-disable-next-line no-console
728
- console.warn('[dom-picker] frame blocked for shield', frame?.src || frame?.id || 'unknown');
729
- } catch {
730
- /* ignore */
731
- }
732
- },
733
- });
734
- attached = true;
735
- } catch (err) {
736
- // eslint-disable-next-line no-console
737
- console.warn('[dom-picker] pickerShield attach failed', err);
738
- }
739
- shieldActive = attached;
740
- if (!attached) {
741
- addListener(document, 'pointermove', onPointerMove, true);
742
- addListener(document, 'pointerdown', onPointerDown, true);
743
- addListener(document, 'mousedown', onMouseDown, true);
744
- }
745
- } else {
746
- addListener(document, 'pointermove', onPointerMove, true);
747
- addListener(document, 'pointerdown', onPointerDown, true);
748
- addListener(document, 'mousedown', onMouseDown, true);
749
- }
750
- addListener(document, 'keydown', onKeyDown, true);
751
- addListener(window, 'blur', onWindowBlur, true);
752
- addListener(window, 'scroll', onScroll, true);
753
- addListener(window, 'mouseout', onWindowMouseOut, true);
754
-
755
- timeoutToken = window.setTimeout(() => {
756
- finalize({ type: 'timeout' });
757
- }, timeoutMs);
758
- },
759
- cancel(reason) {
760
- if (!active) return;
761
- finalize({ type: 'cancel', reason: reason || 'cancelled' });
762
- },
763
- stop() {
764
- if (!active) return;
765
- finalize({ type: 'cancel', reason: 'stopped' });
766
- },
767
- };
768
- }
769
-
770
- const api = {
771
- startSession: (options) => core.startSession(options || {}),
772
- cancel: () => core.cancel(),
773
- getLastState: () => core.getLastState(),
774
- };
775
-
776
- Object.defineProperty(window, '__domPicker', {
777
- value: api,
778
- configurable: true,
779
- enumerable: false,
780
- writable: false,
781
- });
782
- })();
783
-
784
- // __DOM_PICKER_INLINE_END
785
- (function attachDomPickerLoopback() {
786
- if (typeof window === 'undefined') return;
787
-
788
- function delay(ms) {
789
- return new Promise((resolve) => setTimeout(resolve, ms));
790
- }
791
-
792
- function findElementCenter(selector) {
793
- const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
794
- if (!el) return null;
795
- const rect = el.getBoundingClientRect();
796
- if (!rect || !Number.isFinite(rect.left)) return null;
797
- const vw = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
798
- const vh = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
799
- const inset = 6;
800
- const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
801
- const safeLeft = Math.max(rect.left + inset, inset);
802
- const safeRight = Math.min(rect.right - inset, vw - inset);
803
- const safeTop = Math.max(rect.top + inset, inset);
804
- const safeBottom = Math.min(rect.bottom - inset, vh - inset);
805
- const cx = safeRight >= safeLeft ? (safeLeft + safeRight) / 2 : rect.left + rect.width / 2;
806
- const cy = safeBottom >= safeTop ? (safeTop + safeBottom) / 2 : rect.top + rect.height / 2;
807
- return {
808
- x: Math.round(clamp(cx, inset, vw - inset)),
809
- y: Math.round(clamp(cy, inset, vh - inset)),
810
- rect,
811
- element: el,
812
- };
813
- }
814
-
815
- async function hoverLoopCheck(selector, options = {}) {
816
- const picker = window.__domPicker;
817
- if (!picker || typeof picker.startSession !== 'function') {
818
- return { error: 'domPicker unavailable' };
819
- }
820
- const center = findElementCenter(selector);
821
- if (!center) return { error: 'element_not_found', selector };
822
- const runtime = window.__camoRuntime;
823
- const buildPath = runtime?.dom?.buildPathForElement;
824
- const targetPath = buildPath && center.element instanceof Element ? buildPath(center.element, null) : null;
825
- const fromPoint = document.elementFromPoint(center.x, center.y);
826
- const fromPointPath = buildPath && fromPoint instanceof Element ? buildPath(fromPoint, null) : null;
827
- // NOTE: Real mouse move should be triggered by Playwright (page.mouse.move).
828
- // Here we only start the session and wait for it to pick up the hover.
829
- const before = picker.getLastState?.();
830
- if (!before?.phase || before.phase === 'idle') {
831
- picker.startSession?.({ timeoutMs: options.timeoutMs || 8000 });
832
- await delay(16);
833
- }
834
- await delay(options.settleMs || 32);
835
- const after = picker.getLastState?.();
836
- const hoveredPath = after?.selection?.path || after?.hovered?.path || after?.selected?.path || after?.path || null;
837
- const overlayRect = after?.selection?.rect || after?.hovered?.rect || after?.selected?.rect || after?.rect || null;
838
- const matches = Boolean(targetPath && hoveredPath && hoveredPath === targetPath && overlayRect);
839
- return {
840
- selector,
841
- point: { x: center.x, y: center.y },
842
- targetRect: center.rect,
843
- hoveredPath,
844
- targetPath,
845
- fromPointPath,
846
- overlayRect,
847
- stateBefore: before,
848
- stateAfter: after,
849
- matches,
850
- };
851
- }
852
-
853
- // Expose loopback helper as part of domPicker runtime for system use.
854
- function ensureDomPickerRuntime() {
855
- if (!window.__domPicker) return;
856
- const picker = window.__domPicker;
857
- picker.hoverLoopCheck = hoverLoopCheck;
858
- picker.findElementCenter = findElementCenter;
859
- }
860
-
861
- if (document.readyState === 'loading') {
862
- document.addEventListener('DOMContentLoaded', ensureDomPickerRuntime, { once: true });
863
- } else {
864
- ensureDomPickerRuntime();
865
- }
866
- })();
867
-
868
- (() => {
869
- if (typeof window === 'undefined') {
870
- return;
871
- }
872
- if (window.__camoRuntime && window.__camoRuntime.ready) {
873
- return;
874
- }
875
-
876
- window.__camoRuntimeBootCount = (window.__camoRuntimeBootCount || 0) + 1;
877
-
878
- const VERSION = '0.1.0';
879
- const DEFAULT_STYLE = null;
880
- const registry = new Map();
881
-
882
- function dispatchBridgeEvent(type, data = {}) {
883
- try {
884
- if (typeof window.camo_dispatch === 'function') {
885
- window.camo_dispatch({ ts: Date.now(), type, data });
886
- }
887
- window.dispatchEvent(new CustomEvent(`camo:${type}`, { detail: data }));
888
- } catch {
889
- /* ignore bridge errors */
890
- }
891
- }
892
-
893
- let handshakeNotified = false;
894
- function notifyHandshakeStatus(status) {
895
- if (handshakeNotified) return;
896
- handshakeNotified = true;
897
- dispatchBridgeEvent('handshake.status', {
898
- status,
899
- href: window.location.href,
900
- hostname: window.location.hostname,
901
- runtimeVersion: VERSION,
902
- bootCount: window.__camoRuntimeBootCount || 1,
903
- });
904
- }
905
-
906
- const domUtils = {
907
- resolveRoot(selector) {
908
- if (selector) {
909
- try {
910
- const explicit = document.querySelector(selector);
911
- if (explicit) return explicit;
912
- } catch {
913
- /* ignore invalid selector */
914
- }
915
- }
916
- return document.body || document.documentElement;
917
- },
918
- resolveByPath(path, selector) {
919
- if (!path || typeof path !== 'string') return null;
920
- const parts = path.split('/').filter(Boolean);
921
- if (!parts.length || parts[0] !== 'root') return null;
922
- let current = domUtils.resolveRoot(selector);
923
- for (let i = 1; i < parts.length; i += 1) {
924
- if (!current) break;
925
- const idx = Number(parts[i]);
926
- if (Number.isNaN(idx)) return null;
927
- const children = current.children || [];
928
- current = children[idx] || null;
929
- }
930
- return current;
931
- },
932
- buildPathForElement(el, selector) {
933
- if (!el || !(el instanceof Element)) return null;
934
- const root = domUtils.resolveRoot(selector);
935
- const indices = [];
936
- let cursor = el;
937
- let guard = 0;
938
- while (cursor && guard < 200) {
939
- if (cursor === root) {
940
- indices.push('root');
941
- break;
942
- }
943
- const parent = cursor.parentElement;
944
- if (!parent) break;
945
- const idx = Array.prototype.indexOf.call(parent.children || [], cursor);
946
- indices.push(String(idx));
947
- cursor = parent;
948
- guard += 1;
949
- }
950
- if (!indices.length) return null;
951
- if (indices[indices.length - 1] !== 'root') {
952
- indices.push('root');
953
- }
954
- return indices.reverse().join('/');
955
- },
956
- snapshotNode(el, options = {}) {
957
- if (!el || !(el instanceof Element)) return null;
958
- const path = domUtils.buildPathForElement(el, options.rootSelector);
959
- const childLimit = Number(options.maxChildren || 20);
960
- const depthLimit = Number(options.maxDepth || 3);
961
- return domUtils.collectNode(el, {
962
- path: path || 'root',
963
- depth: 0,
964
- depthLimit,
965
- childLimit,
966
- forcePaths: options.forcePaths || []
967
- });
968
- },
969
- collectNode(el, ctx) {
970
- if (!el || !(el instanceof Element)) return null;
971
- const node = {
972
- path: ctx.path,
973
- tag: el.tagName ? el.tagName.toLowerCase() : 'node',
974
- id: el.id || null,
975
- classes: Array.from(el.classList || []),
976
- textSnippet: domUtils.extractText(el),
977
- text: domUtils.extractText(el),
978
- childCount: el.children ? el.children.length : 0,
979
- children: [],
980
- };
981
-
982
- // Check if current path needs to be expanded (depthLimit override)
983
- let shouldExpand = ctx.depth < ctx.depthLimit;
984
- if (!shouldExpand && ctx.forcePaths && ctx.forcePaths.length > 0) {
985
- for (const fp of ctx.forcePaths) {
986
- if (fp.startsWith(ctx.path + '/')) {
987
- shouldExpand = true;
988
- break;
989
- }
990
- }
991
- }
992
-
993
- if (shouldExpand && el.children && el.children.length) {
994
- const maxChildren = Math.max(1, ctx.childLimit);
995
- const totalChildren = el.children.length;
996
- const defaultCount = Math.min(totalChildren, maxChildren);
997
- const indices = new Set();
998
-
999
- for (let i = 0; i < defaultCount; i += 1) {
1000
- indices.add(i);
1001
- }
1002
-
1003
- if (ctx.forcePaths && ctx.forcePaths.length > 0) {
1004
- const prefix = `${ctx.path}/`;
1005
- for (const fp of ctx.forcePaths) {
1006
- if (!fp.startsWith(prefix)) continue;
1007
- const rest = fp.slice(prefix.length);
1008
- const next = rest.split('/')[0];
1009
- const idx = Number(next);
1010
- if (!Number.isNaN(idx) && idx >= 0 && idx < totalChildren) {
1011
- indices.add(idx);
1012
- }
1013
- }
1014
- }
1015
-
1016
- const ordered = Array.from(indices).sort((a, b) => a - b);
1017
- for (const i of ordered) {
1018
- const child = el.children[i];
1019
- const childPath = `${ctx.path}/${i}`;
1020
- const result = domUtils.collectNode(child, {
1021
- path: childPath,
1022
- depth: ctx.depth + 1,
1023
- depthLimit: ctx.depthLimit,
1024
- childLimit: ctx.childLimit,
1025
- forcePaths: ctx.forcePaths
1026
- });
1027
- if (result) {
1028
- node.children.push(result);
1029
- }
1030
- }
1031
- }
1032
- return node;
1033
- },
1034
- extractText(el) {
1035
- if (!el) return '';
1036
- return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
1037
- },
1038
- };
1039
-
1040
- function ensureOverlayLayer() {
1041
- let layer = document.getElementById('__camo_highlight_layer');
1042
- if (layer && layer.parentElement === document.body) return layer;
1043
- if (!layer) {
1044
- layer = document.createElement('div');
1045
- layer.id = '__camo_highlight_layer';
1046
- }
1047
- Object.assign(layer.style, {
1048
- position: 'fixed',
1049
- left: '0',
1050
- top: '0',
1051
- width: '100%',
1052
- height: '100%',
1053
- pointerEvents: 'none',
1054
- zIndex: '2147483646',
1055
- });
1056
- if (!layer.parentElement) {
1057
- document.body.appendChild(layer);
1058
- }
1059
- return layer;
1060
- }
1061
-
1062
- function createOverlay(rect, style) {
1063
- const layer = ensureOverlayLayer();
1064
- const el = document.createElement('div');
1065
- el.className = '__camo_highlight_box';
1066
- Object.assign(el.style, {
1067
- position: 'absolute',
1068
- boxSizing: 'border-box',
1069
- left: `${Math.round(rect.x)}px`,
1070
- top: `${Math.round(rect.y)}px`,
1071
- width: `${Math.max(0, Math.round(rect.width))}px`,
1072
- height: `${Math.max(0, Math.round(rect.height))}px`,
1073
- pointerEvents: 'none',
1074
- ...style,
1075
- });
1076
- layer.appendChild(el);
1077
- return el;
1078
- }
1079
-
1080
- function removeOverlay(overlay) {
1081
- if (overlay && overlay.parentElement) {
1082
- overlay.parentElement.removeChild(overlay);
1083
- }
1084
- }
1085
-
1086
- function clearChannel(channel) {
1087
- const key = channel || 'default';
1088
- const entry = registry.get(key);
1089
- if (entry) {
1090
- if (Array.isArray(entry.items)) {
1091
- entry.items.forEach((item) => {
1092
- try { removeOverlay(item.overlay); } catch {}
1093
- });
1094
- } else if (Array.isArray(entry.overlays)) {
1095
- entry.overlays.forEach((ov) => {
1096
- try { removeOverlay(ov); } catch {}
1097
- });
1098
- }
1099
- }
1100
- registry.delete(key);
1101
- }
1102
-
1103
- let scrollListenerInitialized = false;
1104
-
1105
- function updateAllOverlays() {
1106
- registry.forEach((entry) => {
1107
- if (entry && Array.isArray(entry.items)) {
1108
- entry.items.forEach(({ overlay, element }) => {
1109
- if (!element || !element.isConnected || !overlay) return;
1110
- const rect = element.getBoundingClientRect();
1111
- if (rect.width > 0 && rect.height > 0) {
1112
- Object.assign(overlay.style, {
1113
- left: `${Math.round(rect.x)}px`,
1114
- top: `${Math.round(rect.y)}px`,
1115
- width: `${Math.round(rect.width)}px`,
1116
- height: `${Math.round(rect.height)}px`,
1117
- display: 'block'
1118
- });
1119
- } else {
1120
- overlay.style.display = 'none';
1121
- }
1122
- });
1123
- }
1124
- });
1125
- }
1126
-
1127
- function setupScrollListener() {
1128
- if (scrollListenerInitialized) return;
1129
- scrollListenerInitialized = true;
1130
-
1131
- let ticking = false;
1132
- const handler = () => {
1133
- if (!ticking) {
1134
- window.requestAnimationFrame(() => {
1135
- updateAllOverlays();
1136
- ticking = false;
1137
- });
1138
- ticking = true;
1139
- }
1140
- };
1141
-
1142
- window.addEventListener('scroll', handler, { capture: true, passive: true });
1143
- window.addEventListener('resize', handler, { passive: true });
1144
- }
1145
-
1146
-
1147
- function highlightNodes(nodes, options = {}) {
1148
- const channel = options.channel || 'default';
1149
- const style = options.style || '2px solid rgba(255, 193, 7, 0.9)';
1150
- const borderStyle = typeof style === 'string' ? { border: style, borderRadius: '4px' } : style;
1151
-
1152
- const prev = registry.get(channel);
1153
- if (prev) {
1154
- if (Array.isArray(prev.items)) {
1155
- prev.items.forEach((item) => {
1156
- try { removeOverlay(item.overlay); } catch {}
1157
- });
1158
- } else if (Array.isArray(prev.overlays)) {
1159
- prev.overlays.forEach((ov) => {
1160
- try { removeOverlay(ov); } catch {}
1161
- });
1162
- }
1163
- }
1164
-
1165
- const items = [];
1166
- const overlays = [];
1167
- const list = Array.isArray(nodes) ? nodes : [];
1168
- list.forEach((node) => {
1169
- if (!(node instanceof Element)) return;
1170
- const rect = node.getBoundingClientRect();
1171
- if (!rect || !rect.width || !rect.height) return;
1172
- const overlay = createOverlay(rect, borderStyle);
1173
- items.push({ overlay, element: node });
1174
- overlays.push(overlay);
1175
- });
1176
-
1177
- registry.set(channel, {
1178
- items,
1179
- overlays,
1180
- sticky: Boolean(options.sticky || options.hold),
1181
- cleanup: () => {
1182
- items.forEach((item) => {
1183
- try {
1184
- removeOverlay(item.overlay);
1185
- } catch {}
1186
- });
1187
- },
1188
- });
1189
-
1190
- setupScrollListener();
1191
-
1192
- if (!options.sticky && !options.hold) {
1193
- const duration = Number(options.duration || 0);
1194
- if (duration > 0) {
1195
- setTimeout(() => {
1196
- clearChannel(channel);
1197
- }, duration);
1198
- }
1199
- }
1200
-
1201
- return { selector: options.selector || null, count: items.length, channel };
1202
- }
1203
-
1204
- function highlightSelector(selector, options = {}) {
1205
- const rootSelector = options.rootSelector || null;
1206
- const root = domUtils.resolveRoot(rootSelector);
1207
- if (!root || !selector) {
1208
- clearChannel(options.channel);
1209
- return { selector: selector || null, count: 0, channel: options.channel || 'default' };
1210
- }
1211
- let nodes = [];
1212
- try {
1213
- const scope = rootSelector ? root : document;
1214
- if (scope === root && typeof root.matches === 'function' && root.matches(selector)) {
1215
- nodes.push(root);
1216
- }
1217
- nodes = nodes.concat(Array.from(scope.querySelectorAll(selector)));
1218
- } catch {
1219
- nodes = [];
1220
- }
1221
- return highlightNodes(nodes, { ...options, selector });
1222
- }
1223
-
1224
- function getDomBranch(path, options = {}) {
1225
- const root = domUtils.resolveRoot(options.rootSelector);
1226
- if (!root) {
1227
- return {
1228
- node: null,
1229
- error: 'root-not-found',
1230
- };
1231
- }
1232
- let target = root;
1233
- if (path && path !== 'root') {
1234
- target = domUtils.resolveByPath(path, options.rootSelector) || root;
1235
- }
1236
- // Extract forcePaths if provided
1237
- const forcePaths = Array.isArray(options.forcePaths) ? options.forcePaths : [];
1238
-
1239
- const node = domUtils.snapshotNode(target, { ...options, forcePaths });
1240
- return {
1241
- node,
1242
- path: node?.path || 'root',
1243
- capturedAt: Date.now(),
1244
- };
1245
- }
1246
-
1247
- function getNodeDetails(path, options = {}) {
1248
- const el = domUtils.resolveByPath(path, options.rootSelector);
1249
- if (!el) {
1250
- return { path, exists: false };
1251
- }
1252
- const rect = el.getBoundingClientRect();
1253
- return {
1254
- path,
1255
- exists: true,
1256
- tag: el.tagName ? el.tagName.toLowerCase() : 'node',
1257
- id: el.id || null,
1258
- classes: Array.from(el.classList || []),
1259
- text: domUtils.extractText(el),
1260
- boundingRect: {
1261
- x: rect.left,
1262
- y: rect.top,
1263
- width: rect.width,
1264
- height: rect.height,
1265
- },
1266
- };
1267
- }
1268
-
1269
- function bootRuntime() {
1270
- // dom-picker bootstrap: ensure __domPicker is loaded if bundled separately
1271
- try {
1272
- // noop; domPicker.runtime.js attaches itself to window when included by loader
1273
- } catch { /* ignore */ }
1274
-
1275
- const runtime = {
1276
- version: VERSION,
1277
- ready: true,
1278
- highlight: {
1279
- highlightSelector,
1280
- highlightElements: highlightNodes,
1281
- clear: clearChannel,
1282
- },
1283
- dom: {
1284
- getBranch: getDomBranch,
1285
- getNodeDetails,
1286
- buildPathForElement: domUtils.buildPathForElement,
1287
- resolveByPath: domUtils.resolveByPath,
1288
- },
1289
- getDomBranch,
1290
- ping() {
1291
- return { ts: Date.now(), href: window.location.href };
1292
- },
1293
- get domPicker() {
1294
- return window.__domPicker || null;
1295
- },
1296
- };
1297
- Object.defineProperty(window, '__camoRuntime', {
1298
- value: runtime,
1299
- configurable: true,
1300
- enumerable: false,
1301
- writable: false,
1302
- });
1303
- notifyHandshakeStatus('ready');
1304
- }
1305
-
1306
- if (document.readyState === 'loading') {
1307
- document.addEventListener(
1308
- 'DOMContentLoaded',
1309
- () => {
1310
- bootRuntime();
1311
- },
1312
- { once: true },
1313
- );
1314
- } else {
1315
- bootRuntime();
1316
- }
1317
- })();
1
+ /* eslint-disable no-var */
2
+ // __DOM_PICKER_INLINE_START
3
+ /* DOM picker runtime: window.__domPicker
4
+ * Responsible for pointer -> DOM -> highlight -> selection.
5
+ */
6
+ (() => {
7
+ if (typeof window === 'undefined') return;
8
+
9
+ const DEFAULT_TIMEOUT = 25000;
10
+ const HOVER_STYLE = '2px dashed #fbbc05';
11
+ const HIGHLIGHT_CHANNEL = '__camo_dom_picker';
12
+
13
+ const domPickerState = {
14
+ active: false,
15
+ phase: 'idle', // 'idle' | 'hovering' | 'selected' | 'cancelled' | 'timeout'
16
+ lastHover: null,
17
+ selection: null,
18
+ error: null,
19
+ updatedAt: Date.now(),
20
+ };
21
+
22
+ const overlayApi = () => {
23
+ const runtime = window.__camoRuntime;
24
+ if (!runtime || !runtime.highlight || !runtime.highlight.highlightElements) {
25
+ return null;
26
+ }
27
+ return runtime.highlight;
28
+ };
29
+
30
+ const setState = (patch) => {
31
+ Object.assign(domPickerState, patch, { updatedAt: Date.now() });
32
+ };
33
+
34
+ const pickerShield = createPickerShield();
35
+
36
+ function createPickerShield() {
37
+ if (typeof window === 'undefined') return null;
38
+ const SHIELD_CLASS = '__camo_picker_shield__';
39
+ const instances = new Map();
40
+ const observers = new Map();
41
+ const frameLoadListeners = new Map();
42
+ let callbacksRef = null;
43
+
44
+ class ShieldInstance {
45
+ constructor(frameWindow, frameElement, callbacks) {
46
+ this.frameWindow = frameWindow;
47
+ this.frameElement = frameElement || null;
48
+ this.callbacks = callbacks || {};
49
+ this.layer = null;
50
+ this.bindings = null;
51
+ }
52
+
53
+ mount() {
54
+ const doc = this.getDocument();
55
+ if (!doc) return;
56
+ if (this.layer && doc.contains(this.layer)) {
57
+ return;
58
+ }
59
+ this.destroy();
60
+ if (doc.readyState === 'loading') {
61
+ doc.addEventListener('DOMContentLoaded', () => this.createLayer(doc), { once: true });
62
+ } else {
63
+ this.createLayer(doc);
64
+ }
65
+ }
66
+
67
+ destroy() {
68
+ this.unbindEvents();
69
+ if (this.layer && this.layer.parentNode) {
70
+ this.layer.parentNode.removeChild(this.layer);
71
+ }
72
+ this.layer = null;
73
+ }
74
+
75
+ getDocument() {
76
+ try {
77
+ return this.frameWindow.document;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ createLayer(doc) {
84
+ const host = doc.body || doc.documentElement;
85
+ if (!host) return;
86
+ const layer = doc.createElement('div');
87
+ layer.className = SHIELD_CLASS;
88
+ const style = layer.style;
89
+ style.setProperty('position', 'fixed', 'important');
90
+ style.setProperty('inset', '0', 'important');
91
+ style.setProperty('width', '100vw', 'important');
92
+ style.setProperty('height', '100vh', 'important');
93
+ style.setProperty('z-index', '2147483646', 'important');
94
+ style.setProperty('pointer-events', 'auto', 'important');
95
+ style.setProperty('background', 'transparent', 'important');
96
+ style.setProperty('cursor', 'crosshair', 'important');
97
+ style.setProperty('touch-action', 'none', 'important');
98
+ style.setProperty('user-select', 'none', 'important');
99
+ style.setProperty('contain', 'strict', 'important');
100
+ host.appendChild(layer);
101
+ this.layer = layer;
102
+ this.bindEvents();
103
+ }
104
+
105
+ bindEvents() {
106
+ if (!this.layer) return;
107
+ const win = this.frameWindow;
108
+ const pointerMove = (e) => {
109
+ this.consumeEvent(e);
110
+ const target = this.resolveTarget(e);
111
+ this.callbacks.onHover?.({
112
+ target,
113
+ event: e,
114
+ frameWindow: this.frameWindow,
115
+ frameElement: this.frameElement,
116
+ });
117
+ };
118
+ const pointerDown = (e) => {
119
+ this.consumeEvent(e);
120
+ const target = this.resolveTarget(e);
121
+ this.callbacks.onPointerDown?.({
122
+ target,
123
+ event: e,
124
+ frameWindow: this.frameWindow,
125
+ frameElement: this.frameElement,
126
+ });
127
+ };
128
+ const pointerUp = (e) => {
129
+ this.consumeEvent(e);
130
+ const target = this.resolveTarget(e);
131
+ this.callbacks.onPointerUp?.({
132
+ target,
133
+ event: e,
134
+ frameWindow: this.frameWindow,
135
+ frameElement: this.frameElement,
136
+ });
137
+ };
138
+ const click = (e) => {
139
+ this.consumeEvent(e);
140
+ const target = this.resolveTarget(e);
141
+ this.callbacks.onClick?.({
142
+ target,
143
+ event: e,
144
+ frameWindow: this.frameWindow,
145
+ frameElement: this.frameElement,
146
+ });
147
+ };
148
+ const contextMenu = (e) => {
149
+ this.consumeEvent(e);
150
+ };
151
+ win.addEventListener('pointermove', pointerMove, true);
152
+ win.addEventListener('pointerdown', pointerDown, true);
153
+ win.addEventListener('pointerup', pointerUp, true);
154
+ win.addEventListener('pointercancel', pointerUp, true);
155
+ win.addEventListener('click', click, true);
156
+ win.addEventListener('contextmenu', contextMenu, true);
157
+ this.bindings = { pointerMove, pointerDown, pointerUp, click, contextMenu };
158
+ }
159
+
160
+ unbindEvents() {
161
+ if (!this.bindings) return;
162
+ const { pointerMove, pointerDown, pointerUp, click, contextMenu } = this.bindings;
163
+ const win = this.frameWindow;
164
+ win.removeEventListener('pointermove', pointerMove, true);
165
+ win.removeEventListener('pointerdown', pointerDown, true);
166
+ win.removeEventListener('pointerup', pointerUp, true);
167
+ win.removeEventListener('pointercancel', pointerUp, true);
168
+ win.removeEventListener('click', click, true);
169
+ win.removeEventListener('contextmenu', contextMenu, true);
170
+ this.bindings = null;
171
+ }
172
+
173
+ consumeEvent(e) {
174
+ if (!e) return;
175
+ try {
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ if (typeof e.stopImmediatePropagation === 'function') {
179
+ e.stopImmediatePropagation();
180
+ }
181
+ } catch {
182
+ /* ignore */
183
+ }
184
+ }
185
+
186
+ resolveTarget(e) {
187
+ if (!e) return null;
188
+ const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
189
+ let fallbackCandidate = null;
190
+ for (const node of path) {
191
+ if (node instanceof Element && !this.isShieldElement(node)) {
192
+ if (!fallbackCandidate) {
193
+ fallbackCandidate = node;
194
+ }
195
+ const tag = node.tagName ? node.tagName.toLowerCase() : '';
196
+ if (tag && tag !== 'html' && tag !== 'body') {
197
+ return node;
198
+ }
199
+ }
200
+ }
201
+ const maybeTarget = e.target instanceof Element ? e.target : null;
202
+ if (maybeTarget && !this.isShieldElement(maybeTarget)) {
203
+ const tag = maybeTarget.tagName ? maybeTarget.tagName.toLowerCase() : '';
204
+ if (tag && tag !== 'html' && tag !== 'body') {
205
+ return maybeTarget;
206
+ }
207
+ if (!fallbackCandidate) {
208
+ fallbackCandidate = maybeTarget;
209
+ }
210
+ }
211
+ const alt = this.elementFromPointSafely(e);
212
+ if (alt && !this.isShieldElement(alt)) {
213
+ return alt;
214
+ }
215
+ return fallbackCandidate;
216
+ }
217
+
218
+ isShieldElement(el) {
219
+ if (!(el instanceof Element)) return false;
220
+ return el.classList.contains(SHIELD_CLASS);
221
+ }
222
+
223
+ elementFromPointSafely(e) {
224
+ const doc = this.getDocument();
225
+ if (!doc || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return null;
226
+ const originalDisplay = this.layer ? this.layer.style.display : '';
227
+ if (this.layer) {
228
+ this.layer.style.display = 'none';
229
+ }
230
+ let result = null;
231
+ try {
232
+ const candidate = doc.elementFromPoint(e.clientX, e.clientY);
233
+ if (candidate instanceof Element && !this.isShieldElement(candidate)) {
234
+ result = candidate;
235
+ }
236
+ } catch {
237
+ result = null;
238
+ } finally {
239
+ if (this.layer) {
240
+ this.layer.style.display = originalDisplay || '';
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+ }
246
+
247
+ const attachToWindow = (targetWindow, frameElement) => {
248
+ if (!callbacksRef || instances.has(targetWindow)) return;
249
+ const instance = new ShieldInstance(targetWindow, frameElement, callbacksRef);
250
+ instances.set(targetWindow, instance);
251
+ instance.mount();
252
+ observeFrames(targetWindow);
253
+ };
254
+
255
+ const observeFrames = (targetWindow) => {
256
+ const doc = getDocumentSafe(targetWindow);
257
+ if (!doc) return;
258
+ const observer = new MutationObserver(() => scanFrames(targetWindow));
259
+ observer.observe(doc.documentElement || doc, { childList: true, subtree: true });
260
+ observers.set(targetWindow, observer);
261
+ scanFrames(targetWindow);
262
+ };
263
+
264
+ const scanFrames = (targetWindow) => {
265
+ const doc = getDocumentSafe(targetWindow);
266
+ if (!doc) return;
267
+ const frames = Array.from(doc.querySelectorAll('iframe'));
268
+ frames.forEach((frame) => tryAttachFrame(frame));
269
+ };
270
+
271
+ const tryAttachFrame = (frame) => {
272
+ if (!frame || !frame.contentWindow) return;
273
+ const childWindow = frame.contentWindow;
274
+ if (instances.has(childWindow)) {
275
+ instances.get(childWindow)?.mount();
276
+ return;
277
+ }
278
+ try {
279
+ // eslint-disable-next-line no-unused-expressions
280
+ childWindow.document;
281
+ } catch {
282
+ markFrameUnavailable(frame);
283
+ return;
284
+ }
285
+ attachToWindow(childWindow, frame);
286
+ frame.dataset.__camoPickerAttached = 'true';
287
+ const reloadHandler = () => tryAttachFrame(frame);
288
+ frame.addEventListener('load', reloadHandler);
289
+ frameLoadListeners.set(frame, reloadHandler);
290
+ };
291
+
292
+ const markFrameUnavailable = (frame) => {
293
+ frame.dataset.__camoPickerBlocked = 'true';
294
+ if (callbacksRef?.onFrameBlocked) {
295
+ try {
296
+ callbacksRef.onFrameBlocked(frame);
297
+ } catch {
298
+ /* ignore */
299
+ }
300
+ }
301
+ };
302
+
303
+ const detachAll = () => {
304
+ instances.forEach((instance) => instance.destroy());
305
+ instances.clear();
306
+ observers.forEach((observer) => observer.disconnect());
307
+ observers.clear();
308
+ frameLoadListeners.forEach((listener, frame) => {
309
+ frame.removeEventListener('load', listener);
310
+ });
311
+ frameLoadListeners.clear();
312
+ };
313
+
314
+ const getDocumentSafe = (targetWindow) => {
315
+ try {
316
+ return targetWindow.document;
317
+ } catch {
318
+ return null;
319
+ }
320
+ };
321
+
322
+ return {
323
+ attach(cb) {
324
+ detachAll();
325
+ callbacksRef = cb || null;
326
+ if (!callbacksRef) return;
327
+ attachToWindow(window, null);
328
+ },
329
+ detach() {
330
+ detachAll();
331
+ callbacksRef = null;
332
+ },
333
+ };
334
+ }
335
+
336
+ const core = {
337
+ _session: null,
338
+
339
+ _ensureStopped() {
340
+ if (!this._session) return;
341
+ try {
342
+ this._session.stop();
343
+ } catch {}
344
+ this._session = null;
345
+ },
346
+
347
+ startSession(options) {
348
+ const mode = (options && options.mode) || 'hover-select';
349
+ const timeoutMs = Math.min(Math.max(Number(options && options.timeoutMs) || DEFAULT_TIMEOUT, 1000), 60000);
350
+ const rootSelector = (options && (options.rootSelector || options.root_selector)) || null;
351
+
352
+ this._ensureStopped();
353
+
354
+ const session = createSession({ mode, timeoutMs, rootSelector });
355
+ this._session = session;
356
+ session.start();
357
+ return this.getLastState();
358
+ },
359
+
360
+ cancel() {
361
+ if (this._session) {
362
+ this._session.cancel('cancelled-by-host');
363
+ }
364
+ return this.getLastState();
365
+ },
366
+
367
+ getLastState() {
368
+ // return shallow-cloned, read-only-ish snapshot
369
+ return JSON.parse(JSON.stringify(domPickerState));
370
+ },
371
+ };
372
+
373
+ function createSession(config) {
374
+ const mode = config.mode || 'hover-select';
375
+ const timeoutMs = config.timeoutMs || DEFAULT_TIMEOUT;
376
+ const rootSelector = config.rootSelector || null;
377
+
378
+ let active = false;
379
+ let timeoutToken = null;
380
+
381
+ let lastHoverEl = null;
382
+ let shieldActive = false;
383
+
384
+ const listeners = [];
385
+
386
+ const detachShield = () => {
387
+ if (pickerShield && shieldActive) {
388
+ try {
389
+ pickerShield.detach();
390
+ } catch {
391
+ /* ignore */
392
+ }
393
+ shieldActive = false;
394
+ }
395
+ };
396
+
397
+ const addListener = (target, type, handler, options) => {
398
+ target.addEventListener(type, handler, options || true);
399
+ listeners.push(() => {
400
+ try {
401
+ target.removeEventListener(type, handler, options || true);
402
+ } catch {}
403
+ });
404
+ };
405
+
406
+ const clearHighlight = () => {
407
+ const api = overlayApi();
408
+ if (!api || !api.clear) return;
409
+ try {
410
+ api.clear(HIGHLIGHT_CHANNEL);
411
+ } catch {}
412
+ };
413
+
414
+ const highlightEl = (el) => {
415
+ const api = overlayApi();
416
+ if (!api || !api.highlightElements) return;
417
+ if (!el) {
418
+ clearHighlight();
419
+ return;
420
+ }
421
+ try {
422
+ api.highlightElements([el], {
423
+ channel: HIGHLIGHT_CHANNEL,
424
+ style: HOVER_STYLE,
425
+ duration: 0,
426
+ sticky: true,
427
+ maxMatches: 1,
428
+ });
429
+ } catch {}
430
+ };
431
+
432
+ const isRootLike = (el) => {
433
+ if (!el || !el.tagName) return false;
434
+ const tag = el.tagName.toLowerCase();
435
+ if (tag === 'html' || tag === 'body') return true;
436
+ if (el.id && el.id.toLowerCase() === 'app') return true;
437
+ return false;
438
+ };
439
+
440
+ const extractSelector = (el) => {
441
+ if (!el) return null;
442
+ if (el.id) return `#${el.id}`;
443
+ if (el.classList && el.classList.length) {
444
+ return `${el.tagName.toLowerCase()}.${Array.from(el.classList).join('.')}`;
445
+ }
446
+ return el.tagName ? el.tagName.toLowerCase() : null;
447
+ };
448
+
449
+ const extractPath = (el) => {
450
+ if (!el) return null;
451
+ try {
452
+ const runtime = window.__camoRuntime;
453
+ if (!runtime || !runtime.dom || !runtime.dom.buildPathForElement) {
454
+ console.warn('[dom-picker] buildPathForElement missing');
455
+ return null;
456
+ }
457
+ console.log('[dom-picker] extractPath rootSelector:', rootSelector);
458
+ const path = runtime.dom.buildPathForElement(el, rootSelector);
459
+ console.log('[dom-picker] extractPath result:', path);
460
+ return path;
461
+ } catch (err) {
462
+ console.warn('[dom-picker] extractPath error', err);
463
+ return null;
464
+ }
465
+ };
466
+
467
+ const extractRect = (el) => {
468
+ if (!el || !el.getBoundingClientRect) return null;
469
+ const rect = el.getBoundingClientRect();
470
+ return {
471
+ x: Math.round(rect.left),
472
+ y: Math.round(rect.top),
473
+ width: Math.round(rect.width),
474
+ height: Math.round(rect.height),
475
+ };
476
+ };
477
+
478
+ const extractText = (el) => {
479
+ if (!el) return '';
480
+ return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
481
+ };
482
+
483
+ const pickElementFromPoint = (x, y, event) => {
484
+ const elements = (typeof document.elementsFromPoint === 'function'
485
+ ? document.elementsFromPoint(x, y)
486
+ : []);
487
+ const stack = Array.isArray(elements) ? elements : [];
488
+
489
+ const rejectSet = new Set();
490
+
491
+ // Exclude overlays created by our highlight layer
492
+ const overlayLayer = document.getElementById('__camo_highlight_layer');
493
+ if (overlayLayer && overlayLayer.contains) {
494
+ stack.forEach((el) => {
495
+ if (overlayLayer.contains(el)) rejectSet.add(el);
496
+ });
497
+ }
498
+
499
+ for (let i = 0; i < stack.length; i += 1) {
500
+ const el = stack[i];
501
+ if (!(el instanceof Element)) continue;
502
+ if (rejectSet.has(el)) continue;
503
+ if (isRootLike(el)) continue;
504
+ return el;
505
+ }
506
+
507
+ // Fallback using elementFromPoint
508
+ const fallback = document.elementFromPoint(x, y);
509
+ if (fallback instanceof Element && !isRootLike(fallback)) {
510
+ if (!rejectSet.has(fallback)) return fallback;
511
+ }
512
+
513
+ // Last resort: event target
514
+ if (event && event.target && event.target instanceof Element && !isRootLike(event.target)) {
515
+ return event.target;
516
+ }
517
+
518
+ return null;
519
+ };
520
+
521
+ const updateHover = (el) => {
522
+ lastHoverEl = el;
523
+ if (!el) {
524
+ setState({ phase: active ? 'hovering' : domPickerState.phase, lastHover: null });
525
+ clearHighlight();
526
+ return;
527
+ }
528
+ const path = extractPath(el);
529
+ const selector = extractSelector(el);
530
+ setState({
531
+ phase: 'hovering',
532
+ lastHover: {
533
+ path: path || null,
534
+ selector: selector || null,
535
+ },
536
+ });
537
+ highlightEl(el);
538
+ };
539
+
540
+ const finalize = (result) => {
541
+ console.log('[dom-picker] finalize called', result);
542
+ if (!active) return;
543
+ active = false;
544
+ if (timeoutToken) {
545
+ clearTimeout(timeoutToken);
546
+ timeoutToken = null;
547
+ }
548
+ detachShield();
549
+ listeners.forEach((fn) => {
550
+ try {
551
+ fn();
552
+ } catch {}
553
+ });
554
+ clearHighlight();
555
+
556
+ if (result && result.type === 'timeout') {
557
+ setState({ phase: 'timeout', error: null, active: false });
558
+ return;
559
+ }
560
+ if (result && result.type === 'cancel') {
561
+ setState({ phase: 'cancelled', error: null, active: false });
562
+ return;
563
+ }
564
+ if (result && result.type === 'error') {
565
+ setState({ phase: 'cancelled', error: result.error || 'unknown-error', active: false });
566
+ return;
567
+ }
568
+ if (result && result.type === 'select' && result.element) {
569
+ const el = result.element;
570
+ console.log('[dom-picker] finalize select element', el);
571
+ const path = extractPath(el);
572
+ console.log('[dom-picker] finalize path', path);
573
+ const selector = extractSelector(el);
574
+ const rect = extractRect(el);
575
+ const text = extractText(el);
576
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
577
+ const id = el.id || null;
578
+ const classes = Array.from(el.classList || []);
579
+
580
+ setState({
581
+ phase: 'selected',
582
+ active: false,
583
+ selection: {
584
+ path: path || '',
585
+ selector: selector || '',
586
+ rect: rect || { x: 0, y: 0, width: 0, height: 0 },
587
+ text,
588
+ tag,
589
+ id,
590
+ classes,
591
+ },
592
+ });
593
+ return;
594
+ }
595
+
596
+ // default: cancelled
597
+ setState({ phase: 'cancelled', active: false });
598
+ };
599
+
600
+ const onPointerMove = (event, forcedTarget) => {
601
+ if (!active) return;
602
+ if (forcedTarget) {
603
+ updateHover(forcedTarget);
604
+ return;
605
+ }
606
+ const x = event?.clientX;
607
+ const y = event?.clientY;
608
+ if (typeof x === 'number' && typeof y === 'number') {
609
+ const el = pickElementFromPoint(x, y, event);
610
+ updateHover(el);
611
+ } else {
612
+ updateHover(null);
613
+ }
614
+ };
615
+
616
+ const commitElement = (el, event) => {
617
+ const target = el || lastHoverEl;
618
+ if (!target && event) {
619
+ const fallback = pickElementFromPoint(event.clientX, event.clientY, event);
620
+ if (fallback) {
621
+ finalize({ type: 'select', element: fallback });
622
+ return;
623
+ }
624
+ }
625
+ if (!target) {
626
+ finalize({ type: 'error', error: 'no-element' });
627
+ return;
628
+ }
629
+ finalize({ type: 'select', element: target });
630
+ };
631
+
632
+ const commitFromEvent = (event, forcedTarget) => {
633
+ if (!event && !forcedTarget) {
634
+ commitElement(null, null);
635
+ return;
636
+ }
637
+ if (forcedTarget) {
638
+ commitElement(forcedTarget, event || null);
639
+ return;
640
+ }
641
+ const x = event?.clientX;
642
+ const y = event?.clientY;
643
+ const el = typeof x === 'number' && typeof y === 'number' ? pickElementFromPoint(x, y, event) : null;
644
+ commitElement(el, event || null);
645
+ };
646
+
647
+ const onPointerDown = (event, forcedTarget) => {
648
+ if (!active) return;
649
+ if (event.button !== undefined && event.button !== 0) return;
650
+ event.preventDefault();
651
+ event.stopPropagation();
652
+ commitFromEvent(event, forcedTarget || null);
653
+ };
654
+
655
+ const onMouseDown = (event, forcedTarget) => {
656
+ if (!active) return;
657
+ if (event.button !== 0) return; // left button only
658
+ event.preventDefault();
659
+ event.stopPropagation();
660
+ commitFromEvent(event, forcedTarget || null);
661
+ };
662
+
663
+ const onKeyDown = (event) => {
664
+ if (!active) return;
665
+ if (event.key === 'Escape') {
666
+ event.preventDefault();
667
+ event.stopPropagation();
668
+ finalize({ type: 'cancel' });
669
+ }
670
+ };
671
+
672
+ const onWindowBlur = () => {
673
+ if (!active) return;
674
+ finalize({ type: 'cancel' });
675
+ };
676
+
677
+ const onWindowMouseOut = (event) => {
678
+ if (!active) return;
679
+ const next = event.relatedTarget || event.toElement;
680
+ if (!next || next === window || next === document) {
681
+ updateHover(null);
682
+ }
683
+ };
684
+
685
+ const onScroll = () => {
686
+ if (!active) return;
687
+ if (lastHoverEl) highlightEl(lastHoverEl);
688
+ };
689
+
690
+ return {
691
+ start() {
692
+ active = true;
693
+ setState({
694
+ active: true,
695
+ phase: 'hovering',
696
+ lastHover: null,
697
+ selection: null,
698
+ error: null,
699
+ });
700
+
701
+ if (pickerShield) {
702
+ let attached = false;
703
+ try {
704
+ pickerShield.attach({
705
+ onHover: ({ target, event }) => {
706
+ onPointerMove(event || null, target || null);
707
+ },
708
+ onPointerDown: ({ target, event }) => {
709
+ onPointerDown(event || { button: 0 }, target || null);
710
+ },
711
+ onPointerUp: () => {
712
+ /* no-op */
713
+ },
714
+ onClick: ({ target, event }) => {
715
+ if (event) {
716
+ try {
717
+ event.preventDefault();
718
+ event.stopPropagation();
719
+ } catch {
720
+ /* ignore */
721
+ }
722
+ }
723
+ commitFromEvent(event || null, target || null);
724
+ },
725
+ onFrameBlocked: (frame) => {
726
+ try {
727
+ // eslint-disable-next-line no-console
728
+ console.warn('[dom-picker] frame blocked for shield', frame?.src || frame?.id || 'unknown');
729
+ } catch {
730
+ /* ignore */
731
+ }
732
+ },
733
+ });
734
+ attached = true;
735
+ } catch (err) {
736
+ // eslint-disable-next-line no-console
737
+ console.warn('[dom-picker] pickerShield attach failed', err);
738
+ }
739
+ shieldActive = attached;
740
+ if (!attached) {
741
+ addListener(document, 'pointermove', onPointerMove, true);
742
+ addListener(document, 'pointerdown', onPointerDown, true);
743
+ addListener(document, 'mousedown', onMouseDown, true);
744
+ }
745
+ } else {
746
+ addListener(document, 'pointermove', onPointerMove, true);
747
+ addListener(document, 'pointerdown', onPointerDown, true);
748
+ addListener(document, 'mousedown', onMouseDown, true);
749
+ }
750
+ addListener(document, 'keydown', onKeyDown, true);
751
+ addListener(window, 'blur', onWindowBlur, true);
752
+ addListener(window, 'scroll', onScroll, true);
753
+ addListener(window, 'mouseout', onWindowMouseOut, true);
754
+
755
+ timeoutToken = window.setTimeout(() => {
756
+ finalize({ type: 'timeout' });
757
+ }, timeoutMs);
758
+ },
759
+ cancel(reason) {
760
+ if (!active) return;
761
+ finalize({ type: 'cancel', reason: reason || 'cancelled' });
762
+ },
763
+ stop() {
764
+ if (!active) return;
765
+ finalize({ type: 'cancel', reason: 'stopped' });
766
+ },
767
+ };
768
+ }
769
+
770
+ const api = {
771
+ startSession: (options) => core.startSession(options || {}),
772
+ cancel: () => core.cancel(),
773
+ getLastState: () => core.getLastState(),
774
+ };
775
+
776
+ Object.defineProperty(window, '__domPicker', {
777
+ value: api,
778
+ configurable: true,
779
+ enumerable: false,
780
+ writable: false,
781
+ });
782
+ })();
783
+
784
+ // __DOM_PICKER_INLINE_END
785
+ (function attachDomPickerLoopback() {
786
+ if (typeof window === 'undefined') return;
787
+
788
+ function delay(ms) {
789
+ return new Promise((resolve) => setTimeout(resolve, ms));
790
+ }
791
+
792
+ function findElementCenter(selector) {
793
+ const el = typeof selector === 'string' ? document.querySelector(selector) : selector;
794
+ if (!el) return null;
795
+ const rect = el.getBoundingClientRect();
796
+ if (!rect || !Number.isFinite(rect.left)) return null;
797
+ const vw = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
798
+ const vh = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
799
+ const inset = 6;
800
+ const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
801
+ const safeLeft = Math.max(rect.left + inset, inset);
802
+ const safeRight = Math.min(rect.right - inset, vw - inset);
803
+ const safeTop = Math.max(rect.top + inset, inset);
804
+ const safeBottom = Math.min(rect.bottom - inset, vh - inset);
805
+ const cx = safeRight >= safeLeft ? (safeLeft + safeRight) / 2 : rect.left + rect.width / 2;
806
+ const cy = safeBottom >= safeTop ? (safeTop + safeBottom) / 2 : rect.top + rect.height / 2;
807
+ return {
808
+ x: Math.round(clamp(cx, inset, vw - inset)),
809
+ y: Math.round(clamp(cy, inset, vh - inset)),
810
+ rect,
811
+ element: el,
812
+ };
813
+ }
814
+
815
+ async function hoverLoopCheck(selector, options = {}) {
816
+ const picker = window.__domPicker;
817
+ if (!picker || typeof picker.startSession !== 'function') {
818
+ return { error: 'domPicker unavailable' };
819
+ }
820
+ const center = findElementCenter(selector);
821
+ if (!center) return { error: 'element_not_found', selector };
822
+ const runtime = window.__camoRuntime;
823
+ const buildPath = runtime?.dom?.buildPathForElement;
824
+ const targetPath = buildPath && center.element instanceof Element ? buildPath(center.element, null) : null;
825
+ const fromPoint = document.elementFromPoint(center.x, center.y);
826
+ const fromPointPath = buildPath && fromPoint instanceof Element ? buildPath(fromPoint, null) : null;
827
+ // NOTE: Real mouse move should be triggered by Playwright (page.mouse.move).
828
+ // Here we only start the session and wait for it to pick up the hover.
829
+ const before = picker.getLastState?.();
830
+ if (!before?.phase || before.phase === 'idle') {
831
+ picker.startSession?.({ timeoutMs: options.timeoutMs || 8000 });
832
+ await delay(16);
833
+ }
834
+ await delay(options.settleMs || 32);
835
+ const after = picker.getLastState?.();
836
+ const hoveredPath = after?.selection?.path || after?.hovered?.path || after?.selected?.path || after?.path || null;
837
+ const overlayRect = after?.selection?.rect || after?.hovered?.rect || after?.selected?.rect || after?.rect || null;
838
+ const matches = Boolean(targetPath && hoveredPath && hoveredPath === targetPath && overlayRect);
839
+ return {
840
+ selector,
841
+ point: { x: center.x, y: center.y },
842
+ targetRect: center.rect,
843
+ hoveredPath,
844
+ targetPath,
845
+ fromPointPath,
846
+ overlayRect,
847
+ stateBefore: before,
848
+ stateAfter: after,
849
+ matches,
850
+ };
851
+ }
852
+
853
+ // Expose loopback helper as part of domPicker runtime for system use.
854
+ function ensureDomPickerRuntime() {
855
+ if (!window.__domPicker) return;
856
+ const picker = window.__domPicker;
857
+ picker.hoverLoopCheck = hoverLoopCheck;
858
+ picker.findElementCenter = findElementCenter;
859
+ }
860
+
861
+ if (document.readyState === 'loading') {
862
+ document.addEventListener('DOMContentLoaded', ensureDomPickerRuntime, { once: true });
863
+ } else {
864
+ ensureDomPickerRuntime();
865
+ }
866
+ })();
867
+
868
+ (() => {
869
+ if (typeof window === 'undefined') {
870
+ return;
871
+ }
872
+ if (window.__camoRuntime && window.__camoRuntime.ready) {
873
+ return;
874
+ }
875
+
876
+ window.__camoRuntimeBootCount = (window.__camoRuntimeBootCount || 0) + 1;
877
+
878
+ const VERSION = '0.1.0';
879
+ const DEFAULT_STYLE = null;
880
+ const registry = new Map();
881
+
882
+ function dispatchBridgeEvent(type, data = {}) {
883
+ try {
884
+ if (typeof window.camo_dispatch === 'function') {
885
+ window.camo_dispatch({ ts: Date.now(), type, data });
886
+ }
887
+ window.dispatchEvent(new CustomEvent(`camo:${type}`, { detail: data }));
888
+ } catch {
889
+ /* ignore bridge errors */
890
+ }
891
+ }
892
+
893
+ let handshakeNotified = false;
894
+ function notifyHandshakeStatus(status) {
895
+ if (handshakeNotified) return;
896
+ handshakeNotified = true;
897
+ dispatchBridgeEvent('handshake.status', {
898
+ status,
899
+ href: window.location.href,
900
+ hostname: window.location.hostname,
901
+ runtimeVersion: VERSION,
902
+ bootCount: window.__camoRuntimeBootCount || 1,
903
+ });
904
+ }
905
+
906
+ const domUtils = {
907
+ resolveRoot(selector) {
908
+ if (selector) {
909
+ try {
910
+ const explicit = document.querySelector(selector);
911
+ if (explicit) return explicit;
912
+ } catch {
913
+ /* ignore invalid selector */
914
+ }
915
+ }
916
+ return document.body || document.documentElement;
917
+ },
918
+ resolveByPath(path, selector) {
919
+ if (!path || typeof path !== 'string') return null;
920
+ const parts = path.split('/').filter(Boolean);
921
+ if (!parts.length || parts[0] !== 'root') return null;
922
+ let current = domUtils.resolveRoot(selector);
923
+ for (let i = 1; i < parts.length; i += 1) {
924
+ if (!current) break;
925
+ const idx = Number(parts[i]);
926
+ if (Number.isNaN(idx)) return null;
927
+ const children = current.children || [];
928
+ current = children[idx] || null;
929
+ }
930
+ return current;
931
+ },
932
+ buildPathForElement(el, selector) {
933
+ if (!el || !(el instanceof Element)) return null;
934
+ const root = domUtils.resolveRoot(selector);
935
+ const indices = [];
936
+ let cursor = el;
937
+ let guard = 0;
938
+ while (cursor && guard < 200) {
939
+ if (cursor === root) {
940
+ indices.push('root');
941
+ break;
942
+ }
943
+ const parent = cursor.parentElement;
944
+ if (!parent) break;
945
+ const idx = Array.prototype.indexOf.call(parent.children || [], cursor);
946
+ indices.push(String(idx));
947
+ cursor = parent;
948
+ guard += 1;
949
+ }
950
+ if (!indices.length) return null;
951
+ if (indices[indices.length - 1] !== 'root') {
952
+ indices.push('root');
953
+ }
954
+ return indices.reverse().join('/');
955
+ },
956
+ snapshotNode(el, options = {}) {
957
+ if (!el || !(el instanceof Element)) return null;
958
+ const path = domUtils.buildPathForElement(el, options.rootSelector);
959
+ const childLimit = Number(options.maxChildren || 20);
960
+ const depthLimit = Number(options.maxDepth || 3);
961
+ return domUtils.collectNode(el, {
962
+ path: path || 'root',
963
+ depth: 0,
964
+ depthLimit,
965
+ childLimit,
966
+ forcePaths: options.forcePaths || []
967
+ });
968
+ },
969
+ collectNode(el, ctx) {
970
+ if (!el || !(el instanceof Element)) return null;
971
+ const node = {
972
+ path: ctx.path,
973
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
974
+ id: el.id || null,
975
+ classes: Array.from(el.classList || []),
976
+ textSnippet: domUtils.extractText(el),
977
+ text: domUtils.extractText(el),
978
+ childCount: el.children ? el.children.length : 0,
979
+ children: [],
980
+ };
981
+
982
+ // Check if current path needs to be expanded (depthLimit override)
983
+ let shouldExpand = ctx.depth < ctx.depthLimit;
984
+ if (!shouldExpand && ctx.forcePaths && ctx.forcePaths.length > 0) {
985
+ for (const fp of ctx.forcePaths) {
986
+ if (fp.startsWith(ctx.path + '/')) {
987
+ shouldExpand = true;
988
+ break;
989
+ }
990
+ }
991
+ }
992
+
993
+ if (shouldExpand && el.children && el.children.length) {
994
+ const maxChildren = Math.max(1, ctx.childLimit);
995
+ const totalChildren = el.children.length;
996
+ const defaultCount = Math.min(totalChildren, maxChildren);
997
+ const indices = new Set();
998
+
999
+ for (let i = 0; i < defaultCount; i += 1) {
1000
+ indices.add(i);
1001
+ }
1002
+
1003
+ if (ctx.forcePaths && ctx.forcePaths.length > 0) {
1004
+ const prefix = `${ctx.path}/`;
1005
+ for (const fp of ctx.forcePaths) {
1006
+ if (!fp.startsWith(prefix)) continue;
1007
+ const rest = fp.slice(prefix.length);
1008
+ const next = rest.split('/')[0];
1009
+ const idx = Number(next);
1010
+ if (!Number.isNaN(idx) && idx >= 0 && idx < totalChildren) {
1011
+ indices.add(idx);
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ const ordered = Array.from(indices).sort((a, b) => a - b);
1017
+ for (const i of ordered) {
1018
+ const child = el.children[i];
1019
+ const childPath = `${ctx.path}/${i}`;
1020
+ const result = domUtils.collectNode(child, {
1021
+ path: childPath,
1022
+ depth: ctx.depth + 1,
1023
+ depthLimit: ctx.depthLimit,
1024
+ childLimit: ctx.childLimit,
1025
+ forcePaths: ctx.forcePaths
1026
+ });
1027
+ if (result) {
1028
+ node.children.push(result);
1029
+ }
1030
+ }
1031
+ }
1032
+ return node;
1033
+ },
1034
+ extractText(el) {
1035
+ if (!el) return '';
1036
+ return (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 160);
1037
+ },
1038
+ };
1039
+
1040
+ function ensureOverlayLayer() {
1041
+ let layer = document.getElementById('__camo_highlight_layer');
1042
+ if (layer && layer.parentElement === document.body) return layer;
1043
+ if (!layer) {
1044
+ layer = document.createElement('div');
1045
+ layer.id = '__camo_highlight_layer';
1046
+ }
1047
+ Object.assign(layer.style, {
1048
+ position: 'fixed',
1049
+ left: '0',
1050
+ top: '0',
1051
+ width: '100%',
1052
+ height: '100%',
1053
+ pointerEvents: 'none',
1054
+ zIndex: '2147483646',
1055
+ });
1056
+ if (!layer.parentElement) {
1057
+ document.body.appendChild(layer);
1058
+ }
1059
+ return layer;
1060
+ }
1061
+
1062
+ function createOverlay(rect, style) {
1063
+ const layer = ensureOverlayLayer();
1064
+ const el = document.createElement('div');
1065
+ el.className = '__camo_highlight_box';
1066
+ Object.assign(el.style, {
1067
+ position: 'absolute',
1068
+ boxSizing: 'border-box',
1069
+ left: `${Math.round(rect.x)}px`,
1070
+ top: `${Math.round(rect.y)}px`,
1071
+ width: `${Math.max(0, Math.round(rect.width))}px`,
1072
+ height: `${Math.max(0, Math.round(rect.height))}px`,
1073
+ pointerEvents: 'none',
1074
+ ...style,
1075
+ });
1076
+ layer.appendChild(el);
1077
+ return el;
1078
+ }
1079
+
1080
+ function removeOverlay(overlay) {
1081
+ if (overlay && overlay.parentElement) {
1082
+ overlay.parentElement.removeChild(overlay);
1083
+ }
1084
+ }
1085
+
1086
+ function clearChannel(channel) {
1087
+ const key = channel || 'default';
1088
+ const entry = registry.get(key);
1089
+ if (entry) {
1090
+ if (Array.isArray(entry.items)) {
1091
+ entry.items.forEach((item) => {
1092
+ try { removeOverlay(item.overlay); } catch {}
1093
+ });
1094
+ } else if (Array.isArray(entry.overlays)) {
1095
+ entry.overlays.forEach((ov) => {
1096
+ try { removeOverlay(ov); } catch {}
1097
+ });
1098
+ }
1099
+ }
1100
+ registry.delete(key);
1101
+ }
1102
+
1103
+ let scrollListenerInitialized = false;
1104
+
1105
+ function updateAllOverlays() {
1106
+ registry.forEach((entry) => {
1107
+ if (entry && Array.isArray(entry.items)) {
1108
+ entry.items.forEach(({ overlay, element }) => {
1109
+ if (!element || !element.isConnected || !overlay) return;
1110
+ const rect = element.getBoundingClientRect();
1111
+ if (rect.width > 0 && rect.height > 0) {
1112
+ Object.assign(overlay.style, {
1113
+ left: `${Math.round(rect.x)}px`,
1114
+ top: `${Math.round(rect.y)}px`,
1115
+ width: `${Math.round(rect.width)}px`,
1116
+ height: `${Math.round(rect.height)}px`,
1117
+ display: 'block'
1118
+ });
1119
+ } else {
1120
+ overlay.style.display = 'none';
1121
+ }
1122
+ });
1123
+ }
1124
+ });
1125
+ }
1126
+
1127
+ function setupScrollListener() {
1128
+ if (scrollListenerInitialized) return;
1129
+ scrollListenerInitialized = true;
1130
+
1131
+ let ticking = false;
1132
+ const handler = () => {
1133
+ if (!ticking) {
1134
+ window.requestAnimationFrame(() => {
1135
+ updateAllOverlays();
1136
+ ticking = false;
1137
+ });
1138
+ ticking = true;
1139
+ }
1140
+ };
1141
+
1142
+ window.addEventListener('scroll', handler, { capture: true, passive: true });
1143
+ window.addEventListener('resize', handler, { passive: true });
1144
+ }
1145
+
1146
+
1147
+ function highlightNodes(nodes, options = {}) {
1148
+ const channel = options.channel || 'default';
1149
+ const style = options.style || '2px solid rgba(255, 193, 7, 0.9)';
1150
+ const borderStyle = typeof style === 'string' ? { border: style, borderRadius: '4px' } : style;
1151
+
1152
+ const prev = registry.get(channel);
1153
+ if (prev) {
1154
+ if (Array.isArray(prev.items)) {
1155
+ prev.items.forEach((item) => {
1156
+ try { removeOverlay(item.overlay); } catch {}
1157
+ });
1158
+ } else if (Array.isArray(prev.overlays)) {
1159
+ prev.overlays.forEach((ov) => {
1160
+ try { removeOverlay(ov); } catch {}
1161
+ });
1162
+ }
1163
+ }
1164
+
1165
+ const items = [];
1166
+ const overlays = [];
1167
+ const list = Array.isArray(nodes) ? nodes : [];
1168
+ list.forEach((node) => {
1169
+ if (!(node instanceof Element)) return;
1170
+ const rect = node.getBoundingClientRect();
1171
+ if (!rect || !rect.width || !rect.height) return;
1172
+ const overlay = createOverlay(rect, borderStyle);
1173
+ items.push({ overlay, element: node });
1174
+ overlays.push(overlay);
1175
+ });
1176
+
1177
+ registry.set(channel, {
1178
+ items,
1179
+ overlays,
1180
+ sticky: Boolean(options.sticky || options.hold),
1181
+ cleanup: () => {
1182
+ items.forEach((item) => {
1183
+ try {
1184
+ removeOverlay(item.overlay);
1185
+ } catch {}
1186
+ });
1187
+ },
1188
+ });
1189
+
1190
+ setupScrollListener();
1191
+
1192
+ if (!options.sticky && !options.hold) {
1193
+ const duration = Number(options.duration || 0);
1194
+ if (duration > 0) {
1195
+ setTimeout(() => {
1196
+ clearChannel(channel);
1197
+ }, duration);
1198
+ }
1199
+ }
1200
+
1201
+ return { selector: options.selector || null, count: items.length, channel };
1202
+ }
1203
+
1204
+ function highlightSelector(selector, options = {}) {
1205
+ const rootSelector = options.rootSelector || null;
1206
+ const root = domUtils.resolveRoot(rootSelector);
1207
+ if (!root || !selector) {
1208
+ clearChannel(options.channel);
1209
+ return { selector: selector || null, count: 0, channel: options.channel || 'default' };
1210
+ }
1211
+ let nodes = [];
1212
+ try {
1213
+ const scope = rootSelector ? root : document;
1214
+ if (scope === root && typeof root.matches === 'function' && root.matches(selector)) {
1215
+ nodes.push(root);
1216
+ }
1217
+ nodes = nodes.concat(Array.from(scope.querySelectorAll(selector)));
1218
+ } catch {
1219
+ nodes = [];
1220
+ }
1221
+ return highlightNodes(nodes, { ...options, selector });
1222
+ }
1223
+
1224
+ function getDomBranch(path, options = {}) {
1225
+ const root = domUtils.resolveRoot(options.rootSelector);
1226
+ if (!root) {
1227
+ return {
1228
+ node: null,
1229
+ error: 'root-not-found',
1230
+ };
1231
+ }
1232
+ let target = root;
1233
+ if (path && path !== 'root') {
1234
+ target = domUtils.resolveByPath(path, options.rootSelector) || root;
1235
+ }
1236
+ // Extract forcePaths if provided
1237
+ const forcePaths = Array.isArray(options.forcePaths) ? options.forcePaths : [];
1238
+
1239
+ const node = domUtils.snapshotNode(target, { ...options, forcePaths });
1240
+ return {
1241
+ node,
1242
+ path: node?.path || 'root',
1243
+ capturedAt: Date.now(),
1244
+ };
1245
+ }
1246
+
1247
+ function getNodeDetails(path, options = {}) {
1248
+ const el = domUtils.resolveByPath(path, options.rootSelector);
1249
+ if (!el) {
1250
+ return { path, exists: false };
1251
+ }
1252
+ const rect = el.getBoundingClientRect();
1253
+ return {
1254
+ path,
1255
+ exists: true,
1256
+ tag: el.tagName ? el.tagName.toLowerCase() : 'node',
1257
+ id: el.id || null,
1258
+ classes: Array.from(el.classList || []),
1259
+ text: domUtils.extractText(el),
1260
+ boundingRect: {
1261
+ x: rect.left,
1262
+ y: rect.top,
1263
+ width: rect.width,
1264
+ height: rect.height,
1265
+ },
1266
+ };
1267
+ }
1268
+
1269
+ function bootRuntime() {
1270
+ // dom-picker bootstrap: ensure __domPicker is loaded if bundled separately
1271
+ try {
1272
+ // noop; domPicker.runtime.js attaches itself to window when included by loader
1273
+ } catch { /* ignore */ }
1274
+
1275
+ const runtime = {
1276
+ version: VERSION,
1277
+ ready: true,
1278
+ highlight: {
1279
+ highlightSelector,
1280
+ highlightElements: highlightNodes,
1281
+ clear: clearChannel,
1282
+ },
1283
+ dom: {
1284
+ getBranch: getDomBranch,
1285
+ getNodeDetails,
1286
+ buildPathForElement: domUtils.buildPathForElement,
1287
+ resolveByPath: domUtils.resolveByPath,
1288
+ },
1289
+ getDomBranch,
1290
+ ping() {
1291
+ return { ts: Date.now(), href: window.location.href };
1292
+ },
1293
+ get domPicker() {
1294
+ return window.__domPicker || null;
1295
+ },
1296
+ };
1297
+ Object.defineProperty(window, '__camoRuntime', {
1298
+ value: runtime,
1299
+ configurable: true,
1300
+ enumerable: false,
1301
+ writable: false,
1302
+ });
1303
+ notifyHandshakeStatus('ready');
1304
+ }
1305
+
1306
+ if (document.readyState === 'loading') {
1307
+ document.addEventListener(
1308
+ 'DOMContentLoaded',
1309
+ () => {
1310
+ bootRuntime();
1311
+ },
1312
+ { once: true },
1313
+ );
1314
+ } else {
1315
+ bootRuntime();
1316
+ }
1317
+ })();