@reticlehq/browser 2.4.0 → 2.5.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 (71) hide show
  1. package/dist/actions/actions-dom.js +10 -10
  2. package/dist/actions/actions.d.ts +13 -0
  3. package/dist/actions/actions.js +45 -15
  4. package/dist/actions/scroll.js +1 -1
  5. package/dist/commands/commands.js +27 -22
  6. package/dist/connect-options.d.ts +6 -0
  7. package/dist/dom/a11y.js +20 -20
  8. package/dist/dom/auto-anchor.js +1 -1
  9. package/dist/dom/blind-spots.js +1 -1
  10. package/dist/dom/chart.js +5 -5
  11. package/dist/dom/desktop-capture.d.ts +2 -1
  12. package/dist/dom/desktop-capture.js +4 -4
  13. package/dist/dom/occlusion.js +2 -2
  14. package/dist/dom/query.js +28 -18
  15. package/dist/dom/realm.js +13 -13
  16. package/dist/dom/refs.d.ts +14 -0
  17. package/dist/dom/refs.js +20 -1
  18. package/dist/dom/shadow-registry.js +2 -2
  19. package/dist/dom/snapshot.js +7 -7
  20. package/dist/dom/source.js +3 -3
  21. package/dist/dom/theme.js +6 -6
  22. package/dist/dom/virtualized.js +1 -1
  23. package/dist/observers/blind-spots.js +2 -2
  24. package/dist/observers/console.js +3 -3
  25. package/dist/observers/dom.js +5 -5
  26. package/dist/observers/download.d.ts +2 -1
  27. package/dist/observers/download.js +2 -2
  28. package/dist/observers/focus.js +1 -1
  29. package/dist/observers/frames.js +1 -1
  30. package/dist/observers/health.js +70 -2
  31. package/dist/observers/install-all.d.ts +2 -1
  32. package/dist/observers/ipc.d.ts +2 -7
  33. package/dist/observers/ipc.js +7 -7
  34. package/dist/observers/network-body.js +2 -2
  35. package/dist/observers/network-redact.js +5 -5
  36. package/dist/observers/network-stream.js +2 -2
  37. package/dist/observers/network.d.ts +3 -3
  38. package/dist/observers/network.js +9 -9
  39. package/dist/observers/perf.js +1 -1
  40. package/dist/observers/scroll.js +1 -1
  41. package/dist/observers/state.js +1 -1
  42. package/dist/observers/storage.js +7 -7
  43. package/dist/presenter/presenter-brand.d.ts +19 -0
  44. package/dist/presenter/presenter-brand.js +21 -0
  45. package/dist/presenter/presenter-config.d.ts +17 -0
  46. package/dist/presenter/presenter-config.js +21 -0
  47. package/dist/presenter/presenter-controls.d.ts +1 -11
  48. package/dist/presenter/presenter-controls.js +6 -7
  49. package/dist/presenter/presenter-log.js +2 -2
  50. package/dist/presenter/presenter-run-state.js +1 -1
  51. package/dist/presenter/presenter-styles.d.ts +1 -1
  52. package/dist/presenter/presenter-styles.js +20 -9
  53. package/dist/presenter/presenter-tally.js +3 -3
  54. package/dist/presenter/presenter-test-helpers.d.ts +2 -1
  55. package/dist/presenter/presenter-test-helpers.js +2 -2
  56. package/dist/presenter/presenter-verbs.js +16 -11
  57. package/dist/presenter/presenter.js +13 -10
  58. package/dist/recorder/recorder.js +10 -9
  59. package/dist/registry/store-adapters.js +1 -1
  60. package/dist/registry/stores.js +3 -3
  61. package/dist/reticle-presenter-helpers.js +3 -3
  62. package/dist/reticle.js +31 -13
  63. package/dist/review/annotator.js +7 -7
  64. package/dist/security/serialization.js +9 -9
  65. package/dist/session-continuity.d.ts +17 -0
  66. package/dist/session-continuity.js +42 -0
  67. package/dist/timers/clock.js +4 -4
  68. package/dist/timers/native-console.js +1 -1
  69. package/dist/timers/native-timers.js +4 -4
  70. package/dist/transport/transport.js +5 -5
  71. package/package.json +6 -4
@@ -17,7 +17,7 @@ export function fireClickSequence(el) {
17
17
  const from = doc.activeElement ?? doc.body;
18
18
  firePointer(el, 'pointerdown', from);
19
19
  el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
20
- if (el.tabIndex >= 0 && typeof el.focus === 'function')
20
+ if (el.tabIndex >= 0 && 'function' === typeof el.focus)
21
21
  el.focus();
22
22
  firePointer(el, 'pointerup', from);
23
23
  el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
@@ -31,7 +31,7 @@ function isMeasurable(rect) {
31
31
  /** The center of `rect` falls outside the visible viewport. */
32
32
  function isOffViewport(el, rect) {
33
33
  const win = el.ownerDocument.defaultView;
34
- if (win === null)
34
+ if (null === win)
35
35
  return false;
36
36
  const cx = rect.left + rect.width / 2;
37
37
  const cy = rect.top + rect.height / 2;
@@ -40,7 +40,7 @@ function isOffViewport(el, rect) {
40
40
  /** Hit-test the center: occluded iff the top NON-Reticle element is a foreign subtree (not target/ancestor/descendant). */
41
41
  function hitTest(el, rect) {
42
42
  const top = hitTestOccluder(el, rect);
43
- return top === null
43
+ return null === top
44
44
  ? { occluded: false, occludedBy: null }
45
45
  : { occluded: true, occludedBy: refs.refFor(top) };
46
46
  }
@@ -56,7 +56,7 @@ export function clickGeometry(el) {
56
56
  if (!isMeasurable(rect))
57
57
  return NO_GEOMETRY;
58
58
  let scrolledIntoView = false;
59
- if (isOffViewport(el, rect) && typeof el.scrollIntoView === 'function') {
59
+ if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
60
60
  el.scrollIntoView({ block: 'center', inline: 'center' });
61
61
  scrolledIntoView = true;
62
62
  rect = el.getBoundingClientRect();
@@ -64,7 +64,7 @@ export function clickGeometry(el) {
64
64
  return { ...hitTest(el, rect), scrolledIntoView };
65
65
  }
66
66
  export function firePointer(el, type, relatedTarget = null) {
67
- if (typeof PointerEvent === 'function') {
67
+ if ('function' === typeof PointerEvent) {
68
68
  el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
69
69
  }
70
70
  else {
@@ -73,7 +73,7 @@ export function firePointer(el, type, relatedTarget = null) {
73
73
  }
74
74
  /** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
75
75
  export function firePointerNonBubbling(el, type, relatedTarget = null) {
76
- if (typeof PointerEvent === 'function') {
76
+ if ('function' === typeof PointerEvent) {
77
77
  el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
78
78
  }
79
79
  else {
@@ -87,9 +87,9 @@ function makeDataTransfer(data) {
87
87
  // data: { mime, value } or [{ mime, value }, …]
88
88
  const entries = Array.isArray(data) ? data : data !== undefined ? [data] : [];
89
89
  for (const entry of entries) {
90
- if (typeof entry === 'object' && entry !== null) {
90
+ if ('object' === typeof entry && entry !== null) {
91
91
  const e = entry;
92
- if (typeof e.mime === 'string' && typeof e.value === 'string')
92
+ if ('string' === typeof e.mime && 'string' === typeof e.value)
93
93
  dt.setData(e.mime, e.value);
94
94
  }
95
95
  }
@@ -102,7 +102,7 @@ function makeDataTransfer(data) {
102
102
  export async function dragElement(source, target, data) {
103
103
  const dest = target ?? source;
104
104
  const fire = (el, type) => {
105
- if (typeof PointerEvent === 'function' && type.startsWith('pointer')) {
105
+ if ('function' === typeof PointerEvent && type.startsWith('pointer')) {
106
106
  el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true }));
107
107
  }
108
108
  else {
@@ -118,7 +118,7 @@ export async function dragElement(source, target, data) {
118
118
  fire(dest, 'pointerup');
119
119
  fire(dest, 'mouseup');
120
120
  let dropPrevented = false;
121
- if (typeof DragEvent === 'function') {
121
+ if ('function' === typeof DragEvent) {
122
122
  const dataTransfer = makeDataTransfer(data);
123
123
  const init = { bubbles: true, cancelable: true };
124
124
  if (dataTransfer !== null)
@@ -81,7 +81,20 @@ interface SequenceStepResult {
81
81
  dispatched: boolean;
82
82
  settled: boolean;
83
83
  settleReason: SettleReason | null;
84
+ /**
85
+ * The SAME anchors a single act reports. Reporting only the testid meant a testid-less app
86
+ * compiled every sub-step to a volatile ref, so the saved flow carried the degraded sentinel and
87
+ * every replay drifted.
88
+ */
84
89
  testid?: string;
90
+ component?: string;
91
+ role?: string;
92
+ name?: string;
93
+ source?: {
94
+ file: string;
95
+ line: number;
96
+ column?: number;
97
+ };
85
98
  /** best-effort caveat for this step (e.g. synthetic hover may not fire enter/leave). */
86
99
  warning?: string;
87
100
  }
@@ -1,19 +1,19 @@
1
1
  import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, SettleReason, } from '@reticlehq/core';
2
- import { refs } from '../dom/refs.js';
2
+ import { echoRef, refs } from '../dom/refs.js';
3
3
  import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
4
4
  import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
5
5
  import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
6
6
  import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
7
7
  import { nativeSetTimeout, settle } from '../timers/native-timers.js';
8
8
  function asString(value, fallback = '') {
9
- return typeof value === 'string' ? value : fallback;
9
+ return 'string' === typeof value ? value : fallback;
10
10
  }
11
11
  function requireElement(ref) {
12
12
  const el = refs.resolve(ref);
13
- if (el === null)
14
- throw new Error(`ref '${ref}' no longer resolves to an element`);
13
+ if (null === el)
14
+ throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
15
15
  if (!isHtmlElement(el))
16
- throw new Error(`ref '${ref}' is not an HTMLElement`);
16
+ throw new Error(`ref '${echoRef(ref)}' is not an HTMLElement`);
17
17
  return el;
18
18
  }
19
19
  /**
@@ -39,12 +39,20 @@ function anchorOf(el) {
39
39
  out.component = component;
40
40
  if (info?.source !== undefined)
41
41
  out.source = info.source;
42
+ // Reported INDEPENDENTLY. They used to be all-or-nothing, so an element with a role and no
43
+ // accessible name — an icon button, a clickable div, a control labelled by an SVG — came back with
44
+ // no identity at all once identifyComponent (React-specific) also found nothing. Two features read
45
+ // this and both silently degrade without it: the flow recorder anchors a step by it, and coverage
46
+ // recognises a control it already drove across a re-render by it.
47
+ //
48
+ // A role alone is not a unique anchor, and the flow compiler still requires role AND name before it
49
+ // will anchor a step that way. But it is real information, and reporting it beats reporting nothing.
42
50
  const role = getRole(el);
43
51
  const name = getAccessibleName(el);
44
- if (role.length > 0 && name.length > 0) {
52
+ if (role.length > 0)
45
53
  out.role = role;
54
+ if (name.length > 0)
46
55
  out.name = name;
47
- }
48
56
  return out;
49
57
  }
50
58
  const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
@@ -108,7 +116,7 @@ function assertActionAllowed(el, action, args) {
108
116
  action === ActionType.DBLCLICK ||
109
117
  action === ActionType.DRAG ||
110
118
  action === ActionType.SUBMIT ||
111
- (action === ActionType.PRESS && asString(args['key'], 'Enter') === 'Enter');
119
+ (action === ActionType.PRESS && 'Enter' === asString(args['key'], 'Enter'));
112
120
  const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args['toRef'])) : null;
113
121
  const context = isHtmlElement(dragTarget)
114
122
  ? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}`
@@ -135,7 +143,7 @@ function valueOf(el) {
135
143
  /** Ref of the currently-focused element, treating body/null as "no focus". */
136
144
  function activeRef(el) {
137
145
  const active = el.ownerDocument.activeElement;
138
- if (active === null || active === el.ownerDocument.body)
146
+ if (null === active || active === el.ownerDocument.body)
139
147
  return null;
140
148
  return refs.refFor(active);
141
149
  }
@@ -165,7 +173,7 @@ async function dispatchFor(el, action, args) {
165
173
  firePointer(el, 'pointermove', from);
166
174
  const moved = el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true }));
167
175
  // hover-dwell: keep "hovering" for holdMs so timer-gated reveals can mount.
168
- const holdMs = typeof args['holdMs'] === 'number' ? args['holdMs'] : 0;
176
+ const holdMs = 'number' === typeof args['holdMs'] ? args['holdMs'] : 0;
169
177
  if (holdMs > 0)
170
178
  await sleep(holdMs);
171
179
  return !moved;
@@ -224,13 +232,27 @@ async function dispatchFor(el, action, args) {
224
232
  if (isSelect(el)) {
225
233
  // A MISSING value assigned '' to the <select>. No option carries that, so the browser sets
226
234
  // selectedIndex to -1 — deselecting everything — from a call that was simply malformed.
227
- // (Selecting a value that no option carries is a different case and is deliberately NOT
228
- // refused: `actions.effect.test.ts` pins it as a detectable no-op, where the reported
229
- // valueChanged delta is what tells the agent the option never took.)
230
235
  if (typeof args['value'] !== 'string') {
231
236
  throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
232
237
  }
233
- el.value = args['value'];
238
+ const wanted = args['value'];
239
+ const options = Array.from(el.options);
240
+ // REFUSE an option that does not exist, before touching the element.
241
+ //
242
+ // This used to assign anyway and report the resulting `valueChanged` delta as proof the
243
+ // option never took. That only works if nobody is listening. An unmatched value drives
244
+ // selectedIndex to -1, so `el.value` becomes '' — and we then dispatched `change`. Reported
245
+ // from a real session: the app read that empty value in its change handler and persisted it,
246
+ // corrupting a stored language setting. The "detectable no-op" both mutated the app into a
247
+ // state no user could reach AND did it through the app's own handler.
248
+ //
249
+ // Same rule as the readonly/disabled refusal: if a real user could not do it, forcing it is
250
+ // not a test, it is damage.
251
+ if (!options.some((option) => option.value === wanted)) {
252
+ const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
253
+ throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
254
+ }
255
+ el.value = wanted;
234
256
  el.dispatchEvent(new Event('change', { bubbles: true }));
235
257
  return false; // change is not cancelable.
236
258
  }
@@ -245,7 +267,7 @@ async function dispatchFor(el, action, args) {
245
267
  throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
246
268
  case ActionType.SUBMIT: {
247
269
  const form = isForm(el) ? el : el.closest('form');
248
- if (form === null)
270
+ if (null === form)
249
271
  throw new Error('no form to submit');
250
272
  form.requestSubmit();
251
273
  return false; // requestSubmit returns void; the internal submit event is unobservable.
@@ -377,6 +399,14 @@ export async function executeSequence(steps) {
377
399
  };
378
400
  if (res.testid !== undefined)
379
401
  stepBase.testid = res.testid;
402
+ if (res.component !== undefined)
403
+ stepBase.component = res.component;
404
+ if (res.role !== undefined)
405
+ stepBase.role = res.role;
406
+ if (res.name !== undefined)
407
+ stepBase.name = res.name;
408
+ if (res.source !== undefined)
409
+ stepBase.source = res.source;
380
410
  if (res.warning !== undefined)
381
411
  stepBase.warning = res.warning;
382
412
  stepResults.push(stepBase);
@@ -6,7 +6,7 @@ function nearestScrollable(el) {
6
6
  let cur = el;
7
7
  while (cur !== null && cur !== document.body) {
8
8
  const oy = getComputedStyle(cur).overflowY;
9
- if ((oy === 'auto' || oy === 'scroll') && cur.scrollHeight > cur.clientHeight)
9
+ if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
10
10
  return cur;
11
11
  cur = cur.parentElement;
12
12
  }
@@ -5,7 +5,7 @@ import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/acti
5
5
  import { describe } from '../dom/a11y.js';
6
6
  import { sourceFor, formatSource } from '../dom/source.js';
7
7
  import { themeReport } from '../dom/theme.js';
8
- import { refs } from '../dom/refs.js';
8
+ import { echoRef, refs } from '../dom/refs.js';
9
9
  import { isButton, isInput } from '../dom/realm.js';
10
10
  import { hitTestOccluder } from '../dom/occlusion.js';
11
11
  import { readStorage } from '../observers/storage.js';
@@ -19,16 +19,16 @@ import { scrollContainer } from '../actions/scroll.js';
19
19
  /** Query param appended on a hard reload to bypass the browser cache. */
20
20
  export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
21
21
  function str(value) {
22
- return typeof value === 'string' ? value : undefined;
22
+ return 'string' === typeof value ? value : undefined;
23
23
  }
24
24
  function num(value) {
25
- return typeof value === 'number' ? value : undefined;
25
+ return 'number' === typeof value ? value : undefined;
26
26
  }
27
27
  function record(value) {
28
- return typeof value === 'object' && value !== null ? value : {};
28
+ return 'object' === typeof value && value !== null ? value : {};
29
29
  }
30
30
  function sourceLocation(value) {
31
- if (typeof value !== 'object' || value === null)
31
+ if (typeof value !== 'object' || null === value)
32
32
  return undefined;
33
33
  const obj = value;
34
34
  if (typeof obj['file'] !== 'string' || typeof obj['line'] !== 'number')
@@ -49,15 +49,20 @@ function queryFromArgs(args) {
49
49
  component: str(args['component']),
50
50
  scope: str(args['scope']),
51
51
  attrs: Array.isArray(args['attrs'])
52
- ? args['attrs'].filter((a) => typeof a === 'string')
52
+ ? args['attrs'].filter((a) => 'string' === typeof a)
53
53
  : undefined,
54
54
  source: sourceLocation(args['source']),
55
55
  };
56
56
  }
57
57
  function inspect(ref) {
58
58
  const el = refs.resolve(ref);
59
- if (el === null)
60
- return { error: `ref '${ref}' no longer resolves` };
59
+ // THROW, exactly as executeAction does. Returning an error payload made it a SUCCESSFUL command
60
+ // result, which the server then handed to reticle_inspect's outputSchema (ref/role/name/states/
61
+ // visible all required) — so the MCP layer answered -32602 Output validation error for the most
62
+ // ordinary thing that follows a click. The wording matches actions.ts because the server's
63
+ // recovery table keys off /no longer resolves to an element/ to attach the stale-ref recovery.
64
+ if (null === el)
65
+ throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
61
66
  const rect = el.getBoundingClientRect();
62
67
  const component = identifyComponent(el);
63
68
  const view = el.ownerDocument.defaultView;
@@ -104,7 +109,7 @@ function isOccluded(el, rect) {
104
109
  }
105
110
  /** Narrowing guard: an adapter returned a ComponentStateResult (has a boolean `ok`). */
106
111
  function isComponentStateResult(value) {
107
- return (typeof value === 'object' && value !== null && 'ok' in value && typeof value.ok === 'boolean');
112
+ return ('object' === typeof value && value !== null && 'ok' in value && 'boolean' === typeof value.ok);
108
113
  }
109
114
  const COMPONENT_UNAVAILABLE = {
110
115
  ok: false,
@@ -158,7 +163,7 @@ function readState(ref, store, path, depth) {
158
163
  result.truncation = truncation;
159
164
  if (ref !== undefined && ref.length > 0) {
160
165
  const el = refs.resolve(ref);
161
- if (el === null) {
166
+ if (null === el) {
162
167
  result.component = COMPONENT_UNAVAILABLE;
163
168
  }
164
169
  else {
@@ -184,11 +189,11 @@ function listAnimations() {
184
189
  return { animations };
185
190
  }
186
191
  export function resolveNavigationUrl(rawUrl, baseUrl) {
187
- if (rawUrl.length === 0 || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
192
+ if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
188
193
  return null;
189
194
  try {
190
195
  const url = new URL(rawUrl, baseUrl);
191
- return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
196
+ return 'http:' === url.protocol || 'https:' === url.protocol ? url.toString() : null;
192
197
  }
193
198
  catch {
194
199
  return null;
@@ -203,14 +208,14 @@ export function createCommandRegistry() {
203
208
  }));
204
209
  reg.set(ReticleCommand.QUERY, (args) => {
205
210
  const limit = args['limit'];
206
- return runQuery(queryFromArgs(args), typeof limit === 'number' ? limit : undefined);
211
+ return runQuery(queryFromArgs(args), 'number' === typeof limit ? limit : undefined);
207
212
  });
208
213
  reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state'])));
209
214
  reg.set(ReticleCommand.ACT, (args) => {
210
215
  const action = str(args['action']) ?? '';
211
216
  if (action === ActionType.WEBMCP) {
212
217
  const inner = record(args['args']);
213
- return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), inner[DANGEROUS_ACTION_CONFIRM_ARG] === true);
218
+ return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), true === inner[DANGEROUS_ACTION_CONFIRM_ARG]);
214
219
  }
215
220
  return executeAction(str(args['ref']) ?? '', action, record(args['args']));
216
221
  });
@@ -218,14 +223,14 @@ export function createCommandRegistry() {
218
223
  reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? ''));
219
224
  reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
220
225
  reg.set(ReticleCommand.CLOCK, (args) => {
221
- if (args['reset'] === true) {
226
+ if (true === args['reset']) {
222
227
  resetClock();
223
228
  }
224
229
  else {
225
- if (args['freeze'] === true)
230
+ if (true === args['freeze'])
226
231
  freezeClock();
227
232
  const adv = args['advanceMs'];
228
- if (typeof adv === 'number')
233
+ if ('number' === typeof adv)
229
234
  advanceClock(adv);
230
235
  }
231
236
  return { frozen: isClockFrozen() };
@@ -233,24 +238,24 @@ export function createCommandRegistry() {
233
238
  reg.set(ReticleCommand.STATE_READ, (args) => readState(str(args['ref']), str(args['store']), str(args['path']), num(args['depth'])));
234
239
  reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
235
240
  reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
236
- reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(args['fullPage'] === true));
241
+ reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(true === args['fullPage']));
237
242
  reg.set(ReticleCommand.SCROLL, (args) => {
238
243
  const dy = args['dy'];
239
244
  const fraction = args['fraction'];
240
- return scrollContainer(str(args['ref']), typeof dy === 'number' ? dy : undefined, typeof fraction === 'number' ? fraction : undefined);
245
+ return scrollContainer(str(args['ref']), 'number' === typeof dy ? dy : undefined, 'number' === typeof fraction ? fraction : undefined);
241
246
  });
242
247
  reg.set(ReticleCommand.NAVIGATE, (args) => {
243
248
  const rawUrl = str(args['url']);
244
- if (rawUrl === undefined || rawUrl.length === 0)
249
+ if (rawUrl === undefined || 0 === rawUrl.length)
245
250
  return { ok: false, reason: 'url required' };
246
251
  const url = resolveNavigationUrl(rawUrl, window.location.href);
247
- if (url === null)
252
+ if (null === url)
248
253
  return { ok: false, reason: 'only http(s) navigation is allowed' };
249
254
  window.location.assign(url);
250
255
  return { ok: true, url };
251
256
  });
252
257
  reg.set(ReticleCommand.REFRESH, (args) => {
253
- if (args['hard'] === true) {
258
+ if (true === args['hard']) {
254
259
  // Hard reload: navigate to self with a cache-busting param then replace history.
255
260
  const url = new URL(window.location.href);
256
261
  url.searchParams.set(RELOAD_CACHE_BUST_PARAM, String(Date.now()));
@@ -45,6 +45,12 @@ export interface ReticleConnectOptions {
45
45
  * and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
46
46
  */
47
47
  root?: string;
48
+ /**
49
+ * The SDK's own package version, so a version-skewed pair against the daemon can name itself
50
+ * rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
51
+ * only for a hand-wired connect that has no plugin.
52
+ */
53
+ sdkVersion?: string;
48
54
  /** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
49
55
  present?: boolean;
50
56
  /** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
package/dist/dom/a11y.js CHANGED
@@ -45,15 +45,15 @@ function inputRole(input) {
45
45
  const type = input.type.toLowerCase();
46
46
  if (INPUT_TEXT_TYPES.has(type))
47
47
  return 'textbox';
48
- if (type === 'checkbox')
48
+ if ('checkbox' === type)
49
49
  return 'checkbox';
50
- if (type === 'radio')
50
+ if ('radio' === type)
51
51
  return 'radio';
52
- if (type === 'range')
52
+ if ('range' === type)
53
53
  return 'slider';
54
- if (type === 'number')
54
+ if ('number' === type)
55
55
  return 'spinbutton';
56
- if (type === 'submit' || type === 'button' || type === 'reset')
56
+ if ('submit' === type || 'button' === type || 'reset' === type)
57
57
  return 'button';
58
58
  return 'textbox';
59
59
  }
@@ -115,7 +115,7 @@ function collapse(text) {
115
115
  }
116
116
  function labelledByText(el) {
117
117
  const ids = el.getAttribute('aria-labelledby');
118
- if (ids === null)
118
+ if (null === ids)
119
119
  return null;
120
120
  const parts = [];
121
121
  for (const id of ids.split(/\s+/)) {
@@ -144,7 +144,7 @@ function textWithoutHidden(node) {
144
144
  if (node.nodeType !== Node.ELEMENT_NODE)
145
145
  return '';
146
146
  const el = node;
147
- if (el.getAttribute('aria-hidden') === 'true')
147
+ if ('true' === el.getAttribute('aria-hidden'))
148
148
  return '';
149
149
  let out = '';
150
150
  for (const child of el.childNodes)
@@ -191,9 +191,9 @@ export function getAccessibleName(el) {
191
191
  }
192
192
  function ariaBool(el, attr) {
193
193
  const value = el.getAttribute(attr);
194
- if (value === null)
194
+ if (null === value)
195
195
  return undefined;
196
- return value === 'true';
196
+ return 'true' === value;
197
197
  }
198
198
  /**
199
199
  * The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
@@ -203,12 +203,12 @@ export function getStates(el, visible = isVisible(el)) {
203
203
  const states = [ElementState.PRESENT];
204
204
  states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
205
205
  const disabledProp = (isButton(el) || isInput(el) || isSelect(el) || isTextArea(el)) && el.disabled;
206
- const disabled = disabledProp || ariaBool(el, 'aria-disabled') === true;
206
+ const disabled = disabledProp || true === ariaBool(el, 'aria-disabled');
207
207
  states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
208
- const checkedProp = isInput(el) && (el.type === 'checkbox' || el.type === 'radio') && el.checked;
209
- if (checkedProp || ariaBool(el, 'aria-checked') === true)
208
+ const checkedProp = isInput(el) && ('checkbox' === el.type || 'radio' === el.type) && el.checked;
209
+ if (checkedProp || true === ariaBool(el, 'aria-checked'))
210
210
  states.push(ElementState.CHECKED);
211
- if (ariaBool(el, 'aria-expanded') === true)
211
+ if (true === ariaBool(el, 'aria-expanded'))
212
212
  states.push(ElementState.EXPANDED);
213
213
  if (el.ownerDocument.activeElement === el)
214
214
  states.push(ElementState.FOCUSED);
@@ -232,7 +232,7 @@ export function isSensitiveField(el) {
232
232
  el.getAttribute('aria-label') ?? '',
233
233
  ];
234
234
  const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
235
- return ((isInput(el) && el.type.toLowerCase() === 'password') ||
235
+ return ((isInput(el) && 'password' === el.type.toLowerCase()) ||
236
236
  sensitiveAutocomplete ||
237
237
  identifiers.some(isSensitiveKey));
238
238
  }
@@ -247,19 +247,19 @@ export function getValue(el) {
247
247
  }
248
248
  /** Whether the element's OWN box hides it — one forced-style resolution, no ancestor walk. */
249
249
  function selfHidden(el) {
250
- if (el.getAttribute('aria-hidden') === 'true')
250
+ if ('true' === el.getAttribute('aria-hidden'))
251
251
  return true;
252
252
  if (isHtmlElement(el) && el.hidden)
253
253
  return true;
254
254
  const view = el.ownerDocument.defaultView;
255
255
  if (view !== null) {
256
256
  const style = view.getComputedStyle(el);
257
- if (style.display === 'none' ||
258
- style.visibility === 'hidden' ||
259
- style.visibility === 'collapse') {
257
+ if ('none' === style.display ||
258
+ 'hidden' === style.visibility ||
259
+ 'collapse' === style.visibility) {
260
260
  return true;
261
261
  }
262
- if (Number.parseFloat(style.opacity || '1') === 0)
262
+ if (0 === Number.parseFloat(style.opacity || '1'))
263
263
  return true;
264
264
  }
265
265
  return false;
@@ -282,7 +282,7 @@ export function isVisible(el, memo) {
282
282
  // Each cached boolean already folds in that node's own aria-hidden/[hidden]/display/visibility/opacity,
283
283
  // so inherited visibility composes by AND up the chain and a sibling short-circuits at the first
284
284
  // cached ancestor.
285
- const result = !selfHidden(el) && (parent === null || isVisible(parent, memo));
285
+ const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
286
286
  if (memo !== undefined)
287
287
  memo.set(el, result);
288
288
  return result;
@@ -32,7 +32,7 @@ export const AnchorStrategy = {
32
32
  POSITION: 'position',
33
33
  };
34
34
  function nonEmpty(s) {
35
- return typeof s === 'string' && s.length > 0;
35
+ return 'string' === typeof s && s.length > 0;
36
36
  }
37
37
  /** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
38
38
  function sourceTag(source) {
@@ -10,7 +10,7 @@ export function classifyCrossOriginFrames(frames) {
10
10
  }
11
11
  /** Probe the live document: count iframes whose document is unreachable (cross-origin). */
12
12
  export function countCrossOriginFrames() {
13
- if (typeof document === 'undefined')
13
+ if ('undefined' === typeof document)
14
14
  return 0;
15
15
  const frames = [];
16
16
  for (const frame of document.querySelectorAll('iframe')) {
package/dist/dom/chart.js CHANGED
@@ -54,12 +54,12 @@ export function hasNonFiniteCoordinate(value) {
54
54
  /** True when the geometry encloses no area — no coordinates, or every coordinate pair identical. */
55
55
  export function isDegenerate(value) {
56
56
  const nums = geometryNumbers(value).filter((n) => Number.isFinite(n));
57
- if (nums.length === 0)
57
+ if (0 === nums.length)
58
58
  return true;
59
59
  // A polyline of one repeated point draws nothing. Compare pairs, not raw numbers, so a legitimately
60
60
  // flat line (varying x, constant y) is NOT flagged — that is a real chart of constant data.
61
- const xs = nums.filter((_, i) => i % 2 === 0);
62
- const ys = nums.filter((_, i) => i % 2 === 1);
61
+ const xs = nums.filter((_, i) => 0 === i % 2);
62
+ const ys = nums.filter((_, i) => 1 === i % 2);
63
63
  const allSame = (arr) => arr.every((v) => v === arr[0]);
64
64
  return xs.length > 1 && allSame(xs) && allSame(ys);
65
65
  }
@@ -74,14 +74,14 @@ export function inspectChart(root) {
74
74
  for (const el of Array.from(root.querySelectorAll(tag))) {
75
75
  for (const attr of GEOMETRY_ATTRS) {
76
76
  const value = el.getAttribute(attr);
77
- if (value === null)
77
+ if (null === value)
78
78
  continue;
79
79
  examined += 1;
80
80
  const sample = value.length > SAMPLE_MAX ? `${value.slice(0, SAMPLE_MAX)}…` : value;
81
81
  if (hasNonFiniteCoordinate(value)) {
82
82
  findings.push({ kind: 'non-finite-coordinates', tag, attr, sample });
83
83
  }
84
- else if (value.trim().length === 0) {
84
+ else if (0 === value.trim().length) {
85
85
  findings.push({ kind: 'empty-geometry', tag, attr, sample });
86
86
  }
87
87
  else if (isDegenerate(value)) {
@@ -20,7 +20,7 @@
20
20
  * (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
21
21
  * rather than guessing.
22
22
  */
23
- export interface CaptureResult {
23
+ interface CaptureResult {
24
24
  ok: boolean;
25
25
  /**
26
26
  * Filesystem path of the captured PNG, written by the desktop shell.
@@ -34,3 +34,4 @@ export interface CaptureResult {
34
34
  reason?: string;
35
35
  }
36
36
  export declare function captureDesktopWindow(fullPage?: boolean): Promise<CaptureResult>;
37
+ export {};
@@ -48,8 +48,8 @@ function tauriCapture() {
48
48
  return undefined;
49
49
  const invoke = internals.invoke.bind(internals);
50
50
  return async (fullPage) => {
51
- const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage: fullPage === true });
52
- return typeof path === 'string' ? path : null;
51
+ const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage: true === fullPage });
52
+ return 'string' === typeof path ? path : null;
53
53
  };
54
54
  }
55
55
  /**
@@ -94,13 +94,13 @@ async function withReticleUiHidden(capture) {
94
94
  }
95
95
  export async function captureDesktopWindow(fullPage = false) {
96
96
  const channel = window[RETICLE_IPC_GLOBAL];
97
- const capture = typeof channel?.capture === 'function' ? channel.capture : tauriCapture();
97
+ const capture = 'function' === typeof channel?.capture ? channel.capture : tauriCapture();
98
98
  if (capture === undefined) {
99
99
  return { ok: false, reason: 'no desktop capture helper installed' };
100
100
  }
101
101
  try {
102
102
  const path = await withReticleUiHidden(() => capture(fullPage));
103
- return typeof path === 'string' && path.length > 0
103
+ return 'string' === typeof path && path.length > 0
104
104
  ? { ok: true, path }
105
105
  : { ok: false, reason: 'capture returned no image' };
106
106
  }
@@ -7,13 +7,13 @@ import { isReticleUi } from './dom-ignore.js';
7
7
  * we never false-positive an occlusion. Reticle's own HUD is never treated as an occluder.
8
8
  */
9
9
  export function hitTestOccluder(el, rect) {
10
- if (rect.width === 0 || rect.height === 0)
10
+ if (0 === rect.width || 0 === rect.height)
11
11
  return null;
12
12
  const doc = el.ownerDocument;
13
13
  if (typeof doc.elementFromPoint !== 'function')
14
14
  return null;
15
15
  const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
16
- if (top === null || isReticleUi(top))
16
+ if (null === top || isReticleUi(top))
17
17
  return null;
18
18
  const lands = top === el || el.contains(top) || top.contains(el);
19
19
  return lands ? null : top;