@reticlehq/browser 2.14.0 → 3.1.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 (130) hide show
  1. package/dist/actions/actions-dom.d.ts +8 -0
  2. package/dist/actions/actions-dom.js +70 -3
  3. package/dist/actions/actions-press.d.ts +4 -0
  4. package/dist/actions/actions-press.js +46 -0
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +41 -8
  7. package/dist/actions/appeared-text.js +6 -0
  8. package/dist/actions/scroll.d.ts +8 -1
  9. package/dist/actions/scroll.js +28 -10
  10. package/dist/commands/commands.js +10 -3
  11. package/dist/dom/a11y.js +2 -2
  12. package/dist/dom/desktop-capture.js +1 -1
  13. package/dist/dom/dom-ignore.js +1 -1
  14. package/dist/dom/occlusion.js +31 -5
  15. package/dist/dom/paint-context.d.ts +6 -0
  16. package/dist/dom/paint-context.js +38 -0
  17. package/dist/dom/query.js +2 -2
  18. package/dist/dom/snapshot.js +1 -1
  19. package/dist/edit-epoch.d.ts +1 -1
  20. package/dist/edit-epoch.js +1 -1
  21. package/dist/index.d.ts +4 -5
  22. package/dist/index.js +3 -5
  23. package/dist/observers/animation.js +1 -1
  24. package/dist/observers/blind-spots.js +1 -1
  25. package/dist/observers/dialogs.js +1 -1
  26. package/dist/observers/dom.js +28 -8
  27. package/dist/observers/download.js +1 -1
  28. package/dist/observers/health.js +1 -1
  29. package/dist/observers/{network-body.js → net-detail/network-body.js} +2 -2
  30. package/dist/observers/network-stream.js +2 -2
  31. package/dist/observers/network.d.ts +1 -1
  32. package/dist/observers/network.js +2 -2
  33. package/dist/observers/scroll.js +2 -2
  34. package/dist/presenter/chrome/layers.d.ts +2 -0
  35. package/dist/presenter/chrome/layers.js +2 -0
  36. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  37. package/dist/presenter/chrome/log-kinds.js +7 -0
  38. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -1
  39. package/dist/presenter/{presenter-log.d.ts → chrome/presenter-log.d.ts} +3 -12
  40. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +4 -9
  41. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  42. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  43. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -1
  44. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -0
  45. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -0
  46. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -0
  47. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -0
  48. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  49. package/dist/presenter/presenter-account-styles.js +99 -0
  50. package/dist/presenter/presenter-account.d.ts +40 -0
  51. package/dist/presenter/presenter-account.js +178 -0
  52. package/dist/presenter/presenter-config.d.ts +9 -2
  53. package/dist/presenter/presenter-config.js +3 -1
  54. package/dist/presenter/presenter-controls.d.ts +1 -6
  55. package/dist/presenter/presenter-controls.js +7 -2
  56. package/dist/presenter/presenter-dock-layout.js +1 -1
  57. package/dist/presenter/presenter-drag.d.ts +1 -19
  58. package/dist/presenter/presenter-drag.js +3 -50
  59. package/dist/presenter/presenter-glow.js +1 -1
  60. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  61. package/dist/presenter/presenter-hud-position.js +50 -0
  62. package/dist/presenter/presenter-report-styles.js +16 -1
  63. package/dist/presenter/presenter-report.d.ts +2 -1
  64. package/dist/presenter/presenter-report.js +29 -22
  65. package/dist/presenter/presenter-run-state.d.ts +1 -1
  66. package/dist/presenter/presenter-run-state.js +1 -1
  67. package/dist/presenter/presenter-settings-styles.js +2 -1
  68. package/dist/presenter/presenter-settings.d.ts +4 -0
  69. package/dist/presenter/presenter-settings.js +20 -2
  70. package/dist/presenter/presenter-shell-styles.js +21 -15
  71. package/dist/presenter/presenter-shell.d.ts +4 -0
  72. package/dist/presenter/presenter-shell.js +25 -4
  73. package/dist/presenter/presenter-styles.d.ts +2 -1
  74. package/dist/presenter/presenter-styles.js +6 -3
  75. package/dist/presenter/presenter-workspace.d.ts +4 -0
  76. package/dist/presenter/presenter-workspace.js +29 -2
  77. package/dist/presenter/presenter.d.ts +3 -3
  78. package/dist/presenter/presenter.js +25 -14
  79. package/dist/presenter/tour/tour-view.d.ts +33 -0
  80. package/dist/presenter/tour/tour-view.js +188 -0
  81. package/dist/presenter/tour/tour.d.ts +25 -0
  82. package/dist/presenter/tour/tour.js +245 -0
  83. package/dist/recorder/recorder.js +6 -0
  84. package/dist/registry/declared-channels.d.ts +2 -0
  85. package/dist/registry/declared-channels.js +14 -0
  86. package/dist/registry/declared-platform.d.ts +4 -0
  87. package/dist/registry/declared-platform.js +34 -0
  88. package/dist/registry/emitter.js +1 -1
  89. package/dist/registry/store-adapters.js +1 -1
  90. package/dist/registry/stores.js +1 -1
  91. package/dist/reticle-presenter-helpers.js +10 -6
  92. package/dist/reticle-singleton.d.ts +2 -0
  93. package/dist/reticle-singleton.js +3 -0
  94. package/dist/reticle.js +95 -35
  95. package/dist/review/annotator-styles.js +1 -1
  96. package/dist/review/annotator.js +2 -2
  97. package/dist/review/mark-anchor.js +3 -3
  98. package/dist/transport/transport.js +2 -2
  99. package/package.json +5 -4
  100. package/dist/presenter/presenter-test-helpers.d.ts +0 -14
  101. package/dist/presenter/presenter-test-helpers.js +0 -39
  102. /package/dist/actions/{synthetic-input.d.ts → synthetic/synthetic-input.d.ts} +0 -0
  103. /package/dist/actions/{synthetic-input.js → synthetic/synthetic-input.js} +0 -0
  104. /package/dist/dom/{auto-anchor.d.ts → addressing/auto-anchor.d.ts} +0 -0
  105. /package/dist/dom/{auto-anchor.js → addressing/auto-anchor.js} +0 -0
  106. /package/dist/dom/{refs.d.ts → addressing/refs.d.ts} +0 -0
  107. /package/dist/dom/{refs.js → addressing/refs.js} +0 -0
  108. /package/dist/dom/{source.d.ts → addressing/source.d.ts} +0 -0
  109. /package/dist/dom/{source.js → addressing/source.js} +0 -0
  110. /package/dist/observers/{network-body.d.ts → net-detail/network-body.d.ts} +0 -0
  111. /package/dist/observers/{network-redact.d.ts → net-detail/network-redact.d.ts} +0 -0
  112. /package/dist/observers/{network-redact.js → net-detail/network-redact.js} +0 -0
  113. /package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -0
  114. /package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -0
  115. /package/dist/presenter/{presenter-brand.d.ts → chrome/presenter-brand.d.ts} +0 -0
  116. /package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -0
  117. /package/dist/presenter/{presenter-effects.d.ts → chrome/presenter-effects.d.ts} +0 -0
  118. /package/dist/presenter/{presenter-hud-chrome.d.ts → chrome/presenter-hud-chrome.d.ts} +0 -0
  119. /package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -0
  120. /package/dist/presenter/{presenter-report-copy.d.ts → chrome/presenter-report-copy.d.ts} +0 -0
  121. /package/dist/presenter/{presenter-report-copy.js → chrome/presenter-report-copy.js} +0 -0
  122. /package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -0
  123. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  124. /package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -0
  125. /package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -0
  126. /package/dist/registry/{adapters.js → stores/adapters.js} +0 -0
  127. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
  128. /package/dist/timers/{native-console.js → console/native-console.js} +0 -0
  129. /package/dist/timers/{native-timers.d.ts → native/native-timers.d.ts} +0 -0
  130. /package/dist/timers/{native-timers.js → native/native-timers.js} +0 -0
@@ -16,4 +16,12 @@ export declare function clickGeometry(el: HTMLElement): ClickGeometry;
16
16
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
17
17
  export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
18
18
  export declare function dragElement(source: HTMLElement, target: HTMLElement | null, data: unknown): Promise<boolean>;
19
+ export declare function fireTapSequence(el: HTMLElement, hold: {
20
+ ms: number;
21
+ sleep: (ms: number) => Promise<void>;
22
+ now: () => number;
23
+ } | undefined): Promise<{
24
+ prevented: boolean;
25
+ heldMs: number;
26
+ }>;
19
27
  export {};
@@ -1,7 +1,7 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  import { hitTestOccluder } from '../dom/occlusion.js';
3
- import { nativeFrame } from '../timers/native-timers.js';
4
- import { asSyntheticInput } from './synthetic-input.js';
3
+ import { nativeFrame } from '../timers/native/native-timers.js';
4
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
5
5
  export const NO_GEOMETRY = {
6
6
  occluded: false,
7
7
  occludedBy: null,
@@ -176,3 +176,70 @@ export async function dragElement(source, target, data) {
176
176
  }
177
177
  return dropPrevented;
178
178
  }
179
+ export async function fireTapSequence(el, hold) {
180
+ const touches = touchListFor(el);
181
+ firePointerTouch(el, 'pointerdown');
182
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
183
+ if (el.tabIndex >= 0 && 'function' === typeof el.focus)
184
+ el.focus();
185
+ let heldMs = 0;
186
+ if (hold !== undefined && hold.ms > 0) {
187
+ const started = hold.now();
188
+ await hold.sleep(hold.ms);
189
+ heldMs = hold.now() - started;
190
+ }
191
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
192
+ firePointerTouch(el, 'pointerup');
193
+ const clicked = asSyntheticInput(() => el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
194
+ return { prevented: !clicked, heldMs };
195
+ }
196
+ function firePointerTouch(el, type) {
197
+ asSyntheticInput(() => {
198
+ if ('function' === typeof PointerEvent) {
199
+ el.dispatchEvent(new PointerEvent(type, {
200
+ bubbles: true,
201
+ cancelable: true,
202
+ pointerType: 'touch',
203
+ isPrimary: true,
204
+ width: 23,
205
+ height: 23,
206
+ }));
207
+ return;
208
+ }
209
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
210
+ });
211
+ }
212
+ function touchListFor(el) {
213
+ if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
214
+ return [];
215
+ const rect = el.getBoundingClientRect();
216
+ try {
217
+ return [
218
+ new Touch({
219
+ identifier: 0,
220
+ target: el,
221
+ clientX: rect.left + rect.width / 2,
222
+ clientY: rect.top + rect.height / 2,
223
+ }),
224
+ ];
225
+ }
226
+ catch {
227
+ return [];
228
+ }
229
+ }
230
+ function makeTouchEvent(type, touches) {
231
+ if ('function' === typeof TouchEvent) {
232
+ try {
233
+ return new TouchEvent(type, {
234
+ bubbles: true,
235
+ cancelable: true,
236
+ touches,
237
+ targetTouches: touches,
238
+ changedTouches: touches,
239
+ });
240
+ }
241
+ catch {
242
+ }
243
+ }
244
+ return new Event(type, { bubbles: true, cancelable: true });
245
+ }
@@ -8,3 +8,7 @@ export declare function pressModifiers(args: Record<string, unknown>): {
8
8
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
9
9
  export declare function focusedOrDocument(): HTMLElement;
10
10
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
11
+ export declare function pressKeys(args: Record<string, unknown>): string[];
12
+ export declare function holdKey(el: HTMLElement, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
13
+ export declare function pressCombo(el: HTMLElement, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
14
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
@@ -1,4 +1,7 @@
1
1
  import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
2
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
+ import { nativeSetTimeout } from '../timers/native/native-timers.js';
4
+ const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
2
5
  import { isHtmlElement } from '../dom/realm.js';
3
6
  function asString(value, fallback = '') {
4
7
  return 'string' === typeof value ? value : fallback;
@@ -75,3 +78,46 @@ export function focusedOrDocument() {
75
78
  export function isReflessDocumentPress(ref, action, args) {
76
79
  return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
77
80
  }
81
+ export function pressKeys(args) {
82
+ const raw = args['keys'];
83
+ if (!Array.isArray(raw))
84
+ return [];
85
+ return raw.map((k) => asString(k)).filter((k) => k.length > 0);
86
+ }
87
+ const KEY_REPEAT_MS = 33;
88
+ const KEY_REPEAT_DELAY_MS = 500;
89
+ export async function holdKey(el, key, code, mods, ms) {
90
+ const started = Date.now();
91
+ if (ms > KEY_REPEAT_DELAY_MS)
92
+ await sleep(KEY_REPEAT_DELAY_MS);
93
+ while (Date.now() - started < ms) {
94
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
95
+ key,
96
+ code,
97
+ bubbles: true,
98
+ cancelable: true,
99
+ repeat: true,
100
+ ...mods,
101
+ })));
102
+ await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
103
+ }
104
+ }
105
+ export async function pressCombo(el, keys, mods, holdMs) {
106
+ let prevented = false;
107
+ for (const key of keys) {
108
+ const code = pressCode({}, key);
109
+ const ok = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
110
+ if (!ok)
111
+ prevented = true;
112
+ }
113
+ if (holdMs > 0)
114
+ await sleep(holdMs);
115
+ for (const key of [...keys].reverse()) {
116
+ const code = pressCode({}, key);
117
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
118
+ }
119
+ return prevented;
120
+ }
121
+ export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
122
+ 'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
123
+ 'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
@@ -35,6 +35,7 @@ interface ActionResult {
35
35
  warning?: string;
36
36
  }
37
37
  export declare const MAX_HOLD_MS = 30000;
38
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
38
39
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
39
40
  export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
40
41
  export interface ActionStep {
@@ -1,14 +1,14 @@
1
1
  import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
- import { asSyntheticInput } from './synthetic-input.js';
3
- import { echoRef, refs } from '../dom/refs.js';
2
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
+ import { echoRef, refs } from '../dom/addressing/refs.js';
4
4
  import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
5
5
  import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
6
6
  import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
7
- import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
7
+ import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
8
8
  import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
9
- import { nativeSetTimeout, settle } from '../timers/native-timers.js';
9
+ import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
10
10
  import { AppearedText } from './appeared-text.js';
11
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, } from './actions-press.js';
11
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
12
12
  function asString(value, fallback = '') {
13
13
  return 'string' === typeof value ? value : fallback;
14
14
  }
@@ -124,7 +124,8 @@ function assertActionAllowed(el, action, args) {
124
124
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
125
125
  }
126
126
  }
127
- import { NO_GEOMETRY, fireClickSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, } from './actions-dom.js';
127
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, } from './actions-dom.js';
128
+ import { scrollFrom } from './scroll.js';
128
129
  function enabledOf(el) {
129
130
  return !getStates(el).includes(ElementState.DISABLED);
130
131
  }
@@ -151,8 +152,18 @@ async function dispatchFor(el, action, args) {
151
152
  const hold = clampHold(args['holdMs']);
152
153
  return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
153
154
  }
155
+ if (ActionType.TAP === action) {
156
+ const hold = clampHold(args['holdMs']);
157
+ return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
158
+ }
154
159
  return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
155
160
  }
161
+ export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
162
+ 'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
163
+ 'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
164
+ function asFiniteNumber(raw) {
165
+ return 'number' === typeof raw && Number.isFinite(raw) ? raw : undefined;
166
+ }
156
167
  async function dispatchOther(el, action, args) {
157
168
  switch (action) {
158
169
  case ActionType.DBLCLICK:
@@ -253,16 +264,31 @@ async function dispatchOther(el, action, args) {
253
264
  return false;
254
265
  }
255
266
  case ActionType.PRESS: {
267
+ const mods = pressModifiers(args);
268
+ const combo = pressKeys(args);
269
+ if (combo.length > 0)
270
+ return await pressCombo(el, combo, mods, clampHold(args['holdMs']));
256
271
  const key = pressKey(args);
257
272
  const code = pressCode(args, key);
258
- const mods = pressModifiers(args);
259
273
  const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
274
+ const hold = clampHold(args['holdMs']);
275
+ if (hold > 0)
276
+ await holdKey(el, key, code, mods, hold);
260
277
  asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
261
278
  return !down;
262
279
  }
263
280
  case ActionType.SCROLL_INTO_VIEW:
264
281
  el.scrollIntoView();
265
282
  return false;
283
+ case ActionType.ZOOM:
284
+ throw new Error(ZOOM_NEEDS_REAL_BROWSER_MSG);
285
+ case ActionType.SCROLL: {
286
+ const dy = asFiniteNumber(args['dy']);
287
+ const dx = asFiniteNumber(args['dx']);
288
+ const fraction = asFiniteNumber(args['fraction']);
289
+ scrollFrom(el, dy, fraction, dx);
290
+ return false;
291
+ }
266
292
  case ActionType.UPLOAD: {
267
293
  if (!isInput(el) || el.type !== 'file') {
268
294
  throw new Error('upload target must be a <input type="file">');
@@ -302,6 +328,12 @@ async function dispatchOther(el, action, args) {
302
328
  throw new Error(`unknown action '${action}'`);
303
329
  }
304
330
  }
331
+ function asSettleBudget(raw) {
332
+ if ('number' !== typeof raw || !Number.isFinite(raw))
333
+ return undefined;
334
+ return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
335
+ }
336
+ const FRAME_BUDGET_MS_CEILING = 200;
305
337
  export async function executeAction(ref, action, args = {}) {
306
338
  const named = 0 < ref.length;
307
339
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
@@ -336,7 +368,8 @@ export async function executeAction(ref, action, args = {}) {
336
368
  heldMs = outcome.heldMs;
337
369
  }
338
370
  finally {
339
- const outcome = await settle();
371
+ const budget = asSettleBudget(args['settleMs']);
372
+ const outcome = await settle(budget);
340
373
  settled = outcome.settled;
341
374
  settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
342
375
  obs.disconnect();
@@ -6,6 +6,10 @@ const HAS_LETTER = /\p{L}/u;
6
6
  function saysSomething(text) {
7
7
  return HAS_LETTER.test(text);
8
8
  }
9
+ const MAX_MESSAGE_ELEMENTS = 3;
10
+ function isRenderedView(node) {
11
+ return node instanceof Element && node.querySelectorAll('*').length > MAX_MESSAGE_ELEMENTS;
12
+ }
9
13
  export class AppearedText {
10
14
  #seen = new Set();
11
15
  #length = 0;
@@ -20,6 +24,8 @@ export class AppearedText {
20
24
  for (const node of record.addedNodes) {
21
25
  if (this.#full())
22
26
  return;
27
+ if (isRenderedView(node))
28
+ continue;
23
29
  const owner = TEXT_NODE === node.nodeType ? node.parentElement : elementOf(node);
24
30
  this.#add(node.textContent, owner);
25
31
  }
@@ -4,6 +4,13 @@ interface ScrollResult {
4
4
  scrollHeight: number;
5
5
  clientHeight: number;
6
6
  atEnd: boolean;
7
+ atStart: boolean;
8
+ scrollLeft: number;
9
+ scrollWidth: number;
10
+ clientWidth: number;
11
+ atLeftEnd: boolean;
12
+ atRightEnd: boolean;
7
13
  }
8
- export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
14
+ export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
15
+ export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
9
16
  export {};
@@ -1,36 +1,54 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
- function nearestScrollable(el) {
4
+ function nearestScrollable(el, axis) {
5
5
  let cur = el;
6
6
  while (cur !== null && cur !== document.body) {
7
- const oy = getComputedStyle(cur).overflowY;
8
- if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
7
+ const style = getComputedStyle(cur);
8
+ const overflow = 'x' === axis ? style.overflowX : style.overflowY;
9
+ const scrollable = 'x' === axis ? cur.scrollWidth > cur.clientWidth : cur.scrollHeight > cur.clientHeight;
10
+ if (('auto' === overflow || 'scroll' === overflow) && scrollable)
9
11
  return cur;
10
12
  cur = cur.parentElement;
11
13
  }
12
14
  return document.scrollingElement ?? document.documentElement;
13
15
  }
14
- export function scrollContainer(ref, dy, fraction) {
16
+ export function scrollContainer(ref, dy, fraction, dx) {
15
17
  const base = ref !== undefined ? refs.resolve(ref) : null;
16
- const target = base instanceof Element
17
- ? nearestScrollable(base)
18
+ return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
19
+ }
20
+ export function scrollFrom(base, dy, fraction, dx) {
21
+ const axis = dx !== undefined && dx !== 0 && (dy === undefined || 0 === dy) ? 'x' : 'y';
22
+ const target = base !== undefined
23
+ ? nearestScrollable(base, axis)
18
24
  : (document.scrollingElement ?? document.documentElement);
19
25
  const before = target.scrollTop;
26
+ const beforeLeft = target.scrollLeft;
20
27
  if (fraction !== undefined && fraction >= 0 && fraction <= 1) {
21
28
  target.scrollTop = Math.round(target.scrollHeight * fraction);
22
29
  }
23
30
  else {
24
- const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
25
- target.scrollTop = before + step;
31
+ if (dx !== undefined && dx !== 0)
32
+ target.scrollLeft = beforeLeft + dx;
33
+ if (dx === undefined || (dy !== undefined && 0 !== dy)) {
34
+ const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
35
+ target.scrollTop = before + step;
36
+ }
26
37
  }
27
38
  target.dispatchEvent(new Event('scroll', { bubbles: false }));
28
39
  const after = target.scrollTop;
40
+ const afterLeft = target.scrollLeft;
29
41
  return {
30
- scrolled: after !== before,
42
+ scrolled: after !== before || afterLeft !== beforeLeft,
31
43
  scrollTop: after,
32
44
  scrollHeight: target.scrollHeight,
33
45
  clientHeight: target.clientHeight,
34
46
  atEnd: after + target.clientHeight >= target.scrollHeight - 1,
47
+ atStart: after <= 1,
48
+ scrollLeft: afterLeft,
49
+ scrollWidth: target.scrollWidth,
50
+ clientWidth: target.clientWidth,
51
+ atLeftEnd: afterLeft <= 1,
52
+ atRightEnd: afterLeft + target.clientWidth >= target.scrollWidth - 1,
35
53
  };
36
54
  }
@@ -1,17 +1,18 @@
1
1
  import { ActionType, ComponentStateReason, DANGEROUS_ACTION_CONFIRM_ARG, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
+ import { paintContextOf } from '../dom/paint-context.js';
3
4
  import { matchQuery, runQuery } from '../dom/query.js';
4
5
  import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
5
6
  import { describe } from '../dom/a11y.js';
6
- import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/source.js';
7
+ import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
7
8
  import { themeReport } from '../dom/theme.js';
8
- import { refs } from '../dom/refs.js';
9
+ import { refs } from '../dom/addressing/refs.js';
9
10
  import { editEpoch } from '../edit-epoch.js';
10
11
  import { isButton, isInput } from '../dom/realm.js';
11
12
  import { hitTestOccluder } from '../dom/occlusion.js';
12
13
  import { readStorage } from '../observers/storage.js';
13
14
  import { captureDesktopWindow } from '../dom/desktop-capture.js';
14
- import { identifyComponent, readComponentState } from '../registry/adapters.js';
15
+ import { identifyComponent, readComponentState } from '../registry/stores/adapters.js';
15
16
  import { readStoresWithTruncation, readStoresRaw, storeNames } from '../registry/stores.js';
16
17
  import { sanitizeWithReport } from '../security/serialization.js';
17
18
  import { getCapabilities } from '../registry/capabilities.js';
@@ -84,10 +85,16 @@ function inspect(ref) {
84
85
  scrollHeight: el.scrollHeight,
85
86
  clientHeight: el.clientHeight,
86
87
  overflowY: cs?.overflowY ?? 'visible',
88
+ scrollLeft: el.scrollLeft,
89
+ scrollWidth: el.scrollWidth,
90
+ clientWidth: el.clientWidth,
91
+ overflowX: cs?.overflowX ?? 'visible',
87
92
  };
93
+ const paintContext = paintContextOf(el);
88
94
  return {
89
95
  ...describe(el),
90
96
  ...(source !== undefined ? { source } : {}),
97
+ ...(paintContext.length > 0 ? { paintContext } : {}),
91
98
  ...(sourceUnavailable !== undefined ? { sourceUnavailable } : {}),
92
99
  tag: el.tagName.toLowerCase(),
93
100
  href: el.getAttribute('href') ?? undefined,
package/dist/dom/a11y.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
2
2
  import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
3
- import { refs } from './refs.js';
3
+ import { refs } from './addressing/refs.js';
4
4
  import { inspectChart } from './chart.js';
5
5
  import { isSensitiveKey } from '../security/serialization.js';
6
- import { formatSource, sourceFromDom } from './source.js';
6
+ import { formatSource, sourceFromDom } from './addressing/source.js';
7
7
  const NAME_FROM_CONTENT = new Set([
8
8
  'button',
9
9
  'link',
@@ -1,5 +1,5 @@
1
1
  import { RETICLE_IPC_GLOBAL, RETICLE_TAURI_CAPTURE_COMMAND, VisualReason } from '@reticlehq/core';
2
- import { nativeFrame } from '../timers/native-timers.js';
2
+ import { nativeFrame } from '../timers/native/native-timers.js';
3
3
  import { RETICLE_OVERLAY } from './dom-ignore.js';
4
4
  const FULL_PAGE_UNSUPPORTED_MARKER = VisualReason.FULL_PAGE_UNSUPPORTED;
5
5
  const RECOGNISED_MARKERS = [
@@ -1,4 +1,4 @@
1
- export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker]';
1
+ export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker],[data-reticle-tour]';
2
2
  const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
3
3
  let extraIgnore = '';
4
4
  let presenterVisible = false;
@@ -1,13 +1,39 @@
1
1
  import { isReticleUi } from './dom-ignore.js';
2
+ const SAMPLE_POINTS = [
3
+ [0.5, 0.5],
4
+ [0.25, 0.25],
5
+ [0.75, 0.25],
6
+ [0.25, 0.75],
7
+ [0.75, 0.75],
8
+ ];
9
+ const BLOCKED_MAJORITY = 3;
2
10
  export function hitTestOccluder(el, rect) {
3
11
  if (0 === rect.width || 0 === rect.height)
4
12
  return null;
5
13
  const doc = el.ownerDocument;
6
14
  if (typeof doc.elementFromPoint !== 'function')
7
15
  return null;
8
- const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
9
- if (null === top || isReticleUi(top))
10
- return null;
11
- const lands = top === el || el.contains(top) || top.contains(el);
12
- return lands ? null : top;
16
+ const withStack = doc;
17
+ const topAt = (x, y) => {
18
+ const found = withStack.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
19
+ return found.find((c) => null !== c && !isReticleUi(c)) ?? null;
20
+ };
21
+ const blockers = new Map();
22
+ for (const [fx, fy] of SAMPLE_POINTS) {
23
+ const top = topAt(rect.left + rect.width * fx, rect.top + rect.height * fy);
24
+ if (null === top)
25
+ continue;
26
+ if (top === el || el.contains(top) || top.contains(el))
27
+ continue;
28
+ blockers.set(top, (blockers.get(top) ?? 0) + 1);
29
+ }
30
+ let worst = null;
31
+ let worstCount = 0;
32
+ for (const [element, count] of blockers) {
33
+ if (count > worstCount) {
34
+ worst = element;
35
+ worstCount = count;
36
+ }
37
+ }
38
+ return worstCount >= BLOCKED_MAJORITY ? worst : null;
13
39
  }
@@ -0,0 +1,6 @@
1
+ export interface PaintContextEntry {
2
+ readonly on: string;
3
+ readonly property: string;
4
+ readonly value: string;
5
+ }
6
+ export declare function paintContextOf(el: Element): PaintContextEntry[];
@@ -0,0 +1,38 @@
1
+ const PAINT_PROPERTIES = [
2
+ 'filter',
3
+ 'opacity',
4
+ 'transform',
5
+ 'mix-blend-mode',
6
+ 'backdrop-filter',
7
+ ];
8
+ const INERT = {
9
+ filter: 'none',
10
+ opacity: '1',
11
+ transform: 'none',
12
+ 'mix-blend-mode': 'normal',
13
+ 'backdrop-filter': 'none',
14
+ };
15
+ function describeElement(el) {
16
+ if (el.id.length > 0)
17
+ return `#${el.id}`;
18
+ const cls = el.getAttribute('class');
19
+ if (cls !== null && cls.trim().length > 0)
20
+ return `.${cls.trim().split(/\s+/).join('.')}`;
21
+ return el.tagName.toLowerCase();
22
+ }
23
+ export function paintContextOf(el) {
24
+ const view = el.ownerDocument.defaultView;
25
+ if (null === view)
26
+ return [];
27
+ const found = [];
28
+ for (let node = el; node !== null && node !== el.ownerDocument.documentElement; node = node.parentElement) {
29
+ const style = view.getComputedStyle(node);
30
+ for (const property of PAINT_PROPERTIES) {
31
+ const value = style.getPropertyValue(property);
32
+ if (value.length > 0 && value !== INERT[property]) {
33
+ found.push({ on: describeElement(node), property, value });
34
+ }
35
+ }
36
+ }
37
+ return found;
38
+ }
package/dist/dom/query.js CHANGED
@@ -5,8 +5,8 @@ import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisibl
5
5
  import { isIgnored, isReticleOverlay } from './dom-ignore.js';
6
6
  import { isSensitiveKey } from '../security/serialization.js';
7
7
  import { declaredTestids } from '../registry/capabilities.js';
8
- import { identifyComponent } from '../registry/adapters.js';
9
- import { refs } from './refs.js';
8
+ import { identifyComponent } from '../registry/stores/adapters.js';
9
+ import { refs } from './addressing/refs.js';
10
10
  const TESTID_ATTR = 'data-testid';
11
11
  const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
12
12
  const MAX_PRESENT_TESTIDS = 12;
@@ -2,7 +2,7 @@ import { ElementState, SnapshotMode } from '@reticlehq/core';
2
2
  import { capturedRootOf } from './shadow-registry.js';
3
3
  import { isFrame } from './realm.js';
4
4
  import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
5
- import { refs } from './refs.js';
5
+ import { refs } from './addressing/refs.js';
6
6
  import { isIgnored, isReticleOverlay } from './dom-ignore.js';
7
7
  const INTERACTIVE = new Set([
8
8
  'button',
@@ -1,4 +1,4 @@
1
- import { type RefRegistry } from './dom/refs.js';
1
+ import { type RefRegistry } from './dom/addressing/refs.js';
2
2
  export declare class EditEpoch {
3
3
  #private;
4
4
  constructor(registry: RefRegistry);
@@ -1,5 +1,5 @@
1
1
  import { NO_EDITS_OBSERVED } from '@reticlehq/core';
2
- import { echoRef, refs } from './dom/refs.js';
2
+ import { echoRef, refs } from './dom/addressing/refs.js';
3
3
  const HOT_UPDATE_APPLIED = 'vite:afterUpdate';
4
4
  const MAX_NAMED_FILES = 3;
5
5
  const STALE_REF_REFUSAL = 'no longer resolves to an element';
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { Reticle } from './reticle.js';
2
- export declare const reticle: Reticle;
1
+ export { reticle } from './reticle-singleton.js';
3
2
  export { Reticle } from './reticle.js';
4
3
  export type { ReticleConnectOptions } from './reticle.js';
5
4
  export { SESSION_AUTO } from '@reticlehq/core';
6
5
  export { setIgnoreSelectors } from './dom/dom-ignore.js';
7
- export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/adapters.js';
8
- export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/adapters.js';
6
+ export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/stores/adapters.js';
7
+ export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/stores/adapters.js';
9
8
  export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, sourceOwner, markAdapterSource, } from './registry/stores.js';
10
9
  export type { StoreGetter, StoreLike, StoreSubscribe } from './registry/stores.js';
11
10
  export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
@@ -23,6 +22,6 @@ export type { ChartReport, ChartFinding, CanvasChartData } from './dom/chart.js'
23
22
  export { matchQuery, runQuery } from './dom/query.js';
24
23
  export { executeAction, executeSequence } from './actions/actions.js';
25
24
  export { describe, getRole, getAccessibleName, getStates, isVisible } from './dom/a11y.js';
26
- export { refs, RefRegistry } from './dom/refs.js';
25
+ export { refs, RefRegistry } from './dom/addressing/refs.js';
27
26
  export { Annotator, installAnnotator, type AnnotatorDeps } from './review/annotator.js';
28
27
  export { resolveMarkAnchor, type MarkAnchor } from './review/mark-anchor.js';
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
- import { Reticle } from './reticle.js';
2
- const globalStore = globalThis;
3
- export const reticle = (globalStore.__reticleInstance ??= new Reticle());
1
+ export { reticle } from './reticle-singleton.js';
4
2
  export { Reticle } from './reticle.js';
5
3
  export { SESSION_AUTO } from '@reticlehq/core';
6
4
  export { setIgnoreSelectors } from './dom/dom-ignore.js';
7
- export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/adapters.js';
5
+ export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/stores/adapters.js';
8
6
  export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, sourceOwner, markAdapterSource, } from './registry/stores.js';
9
7
  export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
10
8
  export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
@@ -16,6 +14,6 @@ export { inspectChart, canvasChartData } from './dom/chart.js';
16
14
  export { matchQuery, runQuery } from './dom/query.js';
17
15
  export { executeAction, executeSequence } from './actions/actions.js';
18
16
  export { describe, getRole, getAccessibleName, getStates, isVisible } from './dom/a11y.js';
19
- export { refs, RefRegistry } from './dom/refs.js';
17
+ export { refs, RefRegistry } from './dom/addressing/refs.js';
20
18
  export { Annotator, installAnnotator } from './review/annotator.js';
21
19
  export { resolveMarkAnchor } from './review/mark-anchor.js';
@@ -1,5 +1,5 @@
1
1
  import { EventType } from '@reticlehq/core';
2
- import { refs } from '../dom/refs.js';
2
+ import { refs } from '../dom/addressing/refs.js';
3
3
  import { isReticleOverlay } from '../dom/dom-ignore.js';
4
4
  export function installAnimation(emit) {
5
5
  const ac = new AbortController();
@@ -2,7 +2,7 @@ import { EventType, BlindSpotKind } from '@reticlehq/core';
2
2
  import { countUnmountedRows } from '../dom/virtualized.js';
3
3
  import { isCaptured } from '../dom/shadow-registry.js';
4
4
  import { sameOriginFrameBodies } from './frames.js';
5
- import { nativeClearTimeout, nativeSetTimeout } from '../timers/native-timers.js';
5
+ import { nativeClearTimeout, nativeSetTimeout } from '../timers/native/native-timers.js';
6
6
  export function countClosedShadowRoots() {
7
7
  let count = 0;
8
8
  for (const el of document.querySelectorAll('*')) {