@reticlehq/browser 2.3.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 (79) 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 +39 -22
  6. package/dist/connect-options.d.ts +103 -0
  7. package/dist/connect-options.js +1 -0
  8. package/dist/dom/a11y.js +20 -20
  9. package/dist/dom/auto-anchor.js +1 -1
  10. package/dist/dom/blind-spots.js +1 -1
  11. package/dist/dom/chart.js +5 -5
  12. package/dist/dom/desktop-capture.d.ts +2 -1
  13. package/dist/dom/desktop-capture.js +4 -4
  14. package/dist/dom/occlusion.js +2 -2
  15. package/dist/dom/query.js +28 -18
  16. package/dist/dom/realm.js +13 -13
  17. package/dist/dom/refs.d.ts +14 -0
  18. package/dist/dom/refs.js +20 -1
  19. package/dist/dom/shadow-registry.js +2 -2
  20. package/dist/dom/snapshot.js +7 -7
  21. package/dist/dom/source.js +3 -3
  22. package/dist/dom/theme.js +6 -6
  23. package/dist/dom/virtualized.js +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +2 -2
  26. package/dist/observers/blind-spots.js +2 -2
  27. package/dist/observers/console.js +3 -3
  28. package/dist/observers/dom.js +5 -5
  29. package/dist/observers/download.d.ts +2 -1
  30. package/dist/observers/download.js +2 -2
  31. package/dist/observers/focus.js +1 -1
  32. package/dist/observers/frames.js +1 -1
  33. package/dist/observers/health.js +70 -2
  34. package/dist/observers/install-all.d.ts +2 -1
  35. package/dist/observers/ipc.d.ts +2 -7
  36. package/dist/observers/ipc.js +7 -7
  37. package/dist/observers/network-body.js +2 -2
  38. package/dist/observers/network-redact.js +5 -5
  39. package/dist/observers/network-stream.js +2 -2
  40. package/dist/observers/network.d.ts +3 -3
  41. package/dist/observers/network.js +9 -9
  42. package/dist/observers/perf.js +1 -1
  43. package/dist/observers/scroll.js +1 -1
  44. package/dist/observers/state.js +1 -1
  45. package/dist/observers/storage.js +7 -7
  46. package/dist/presenter/presenter-brand.d.ts +19 -0
  47. package/dist/presenter/presenter-brand.js +21 -0
  48. package/dist/presenter/presenter-config.d.ts +17 -0
  49. package/dist/presenter/presenter-config.js +21 -0
  50. package/dist/presenter/presenter-controls.d.ts +1 -11
  51. package/dist/presenter/presenter-controls.js +6 -7
  52. package/dist/presenter/presenter-log.js +2 -2
  53. package/dist/presenter/presenter-run-state.js +1 -1
  54. package/dist/presenter/presenter-styles.d.ts +1 -1
  55. package/dist/presenter/presenter-styles.js +20 -9
  56. package/dist/presenter/presenter-tally.js +3 -3
  57. package/dist/presenter/presenter-test-helpers.d.ts +2 -1
  58. package/dist/presenter/presenter-test-helpers.js +2 -2
  59. package/dist/presenter/presenter-verbs.js +16 -11
  60. package/dist/presenter/presenter.js +13 -10
  61. package/dist/recorder/recorder.js +10 -9
  62. package/dist/registry/capabilities.d.ts +2 -0
  63. package/dist/registry/capabilities.js +16 -0
  64. package/dist/registry/store-adapters.d.ts +127 -1
  65. package/dist/registry/store-adapters.js +178 -0
  66. package/dist/registry/stores.js +3 -3
  67. package/dist/reticle-presenter-helpers.js +3 -3
  68. package/dist/reticle.d.ts +2 -57
  69. package/dist/reticle.js +57 -14
  70. package/dist/review/annotator.js +7 -7
  71. package/dist/security/serialization.js +9 -9
  72. package/dist/session-continuity.d.ts +17 -0
  73. package/dist/session-continuity.js +42 -0
  74. package/dist/timers/clock.js +4 -4
  75. package/dist/timers/native-console.js +1 -1
  76. package/dist/timers/native-timers.js +4 -4
  77. package/dist/transport/transport.d.ts +10 -0
  78. package/dist/transport/transport.js +19 -5
  79. package/package.json +11 -4
@@ -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;
package/dist/dom/query.js CHANGED
@@ -106,7 +106,13 @@ function findIn(container, query) {
106
106
  // value is the component name;.source (if present) still takes precedence inside.
107
107
  return findByComponent(container, { ...query, component: query.component ?? value });
108
108
  default:
109
- return [];
109
+ // THROW, never `return []`. An unsupported strategy answering "no matches" is
110
+ // indistinguishable from the element genuinely being absent, so `by:'css'` — the first thing
111
+ // anyone arriving from Playwright reaches for — reported a page with a <body> as empty. The
112
+ // server now rejects unknown strategies at the schema; this is the same guarantee for every
113
+ // other path in (replay, reticle_run, an internal caller), because a false negative invented
114
+ // by the tool is the exact failure this product exists to prevent.
115
+ throw new Error(`unsupported query strategy '${String(by)}' — use one of: ${Object.values(QueryBy).join(', ')}`);
110
116
  }
111
117
  }
112
118
  // Auto-anchor (component / source) — checked before the role/text fields so a query carrying
@@ -184,7 +190,7 @@ function embeddedRootsUnder(root) {
184
190
  if (!isFrame(el))
185
191
  continue;
186
192
  const body = readableFrameBody(el);
187
- if (body === null)
193
+ if (null === body)
188
194
  continue; // cross-origin — declared, not searched
189
195
  found.push(body);
190
196
  walk(body, depth + 1);
@@ -218,7 +224,7 @@ function findCandidates(query) {
218
224
  const { container, scopeMissing } = resolveContainer(query.scope);
219
225
  // A given-but-missing scope searches NOTHING — never the whole page. The empty result plus the
220
226
  // scopeMissing flag is what keeps "gone scope" distinct from "absent element".
221
- if (container === null)
227
+ if (null === container)
222
228
  return { candidates: [], scopeMissing: true };
223
229
  const seen = new Set();
224
230
  const out = [];
@@ -259,7 +265,7 @@ function projectAttrs(el, keys) {
259
265
  const out = {};
260
266
  for (const key of keys.slice(0, ATTR_KEYS_MAX)) {
261
267
  const raw = el.getAttribute(key);
262
- if (raw === null)
268
+ if (null === raw)
263
269
  continue;
264
270
  out[key] = isSensitiveKey(key) ? REDACTED_VALUE : raw.slice(0, ATTR_VALUE_MAX);
265
271
  }
@@ -286,16 +292,15 @@ const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
286
292
  * "how many?" stay exact while the answer stays affordable.
287
293
  */
288
294
  export function matchQuery(query, state, limit = MAX_DESCRIBED) {
289
- let elements;
290
- let scopeMissing = false;
291
- try {
292
- const found = findCandidates(query);
293
- elements = found.candidates;
294
- scopeMissing = found.scopeMissing;
295
- }
296
- catch {
297
- elements = [];
298
- }
295
+ // NO blanket try/catch here. It used to turn ANY exception during candidate-finding into
296
+ // `elements = []`, which is the same lie as the `default` arm above: a query that could not run
297
+ // reported that the element is not on the page. The one failure it was plausibly guarding —
298
+ // a scope selector that matches nothing — is already handled explicitly and distinctly, as
299
+ // `scopeMissing`, so what remained was a net that could only convert real faults into false
300
+ // negatives.
301
+ const found = findCandidates(query);
302
+ const elements = found.candidates;
303
+ const scopeMissing = found.scopeMissing;
299
304
  // One visibility cache for the whole (synchronous) query pass. isVisible is an O(depth) forced-style
300
305
  // walk; the state filter runs it over EVERY candidate (the count must be exact) — on a match-heavy
301
306
  // page (e.g. a 3k-row grid) that is tens of thousands of getComputedStyle calls on the host's main
@@ -306,7 +311,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
306
311
  const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
307
312
  const descriptors = described.map((el) => {
308
313
  const base = describe(el, visMemo);
309
- if (attrs === undefined || attrs.length === 0)
314
+ if (attrs === undefined || 0 === attrs.length)
310
315
  return base;
311
316
  const projected = projectAttrs(el, attrs);
312
317
  return projected === undefined ? base : { ...base, attrs: projected };
@@ -316,13 +321,18 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
316
321
  count: filtered.length,
317
322
  elements: descriptors,
318
323
  ...(scopeMissing ? { scopeMissing: true } : {}),
324
+ // On a MISS, carry the same diagnosis `runQuery` has always returned. MATCH is the command every
325
+ // PREDICATE uses, so without this a failed assertion was a dead end ("no element matched") while
326
+ // the identical failure through reticle_query listed the testids that ARE present. Computed only
327
+ // when there is nothing to report, so the hot path pays nothing.
328
+ ...(0 === filtered.length ? { hint: buildEmptyHint(query) } : {}),
319
329
  };
320
330
  }
321
331
  /** Resolve `aria-labelledby` (one or more element IDs) to the referenced elements' text — a bare ID is
322
332
  * not a human-readable name. Undefined when unset or nothing resolves. */
323
333
  function resolveLabelledBy(el) {
324
334
  const ids = el.getAttribute('aria-labelledby');
325
- if (ids === null)
335
+ if (null === ids)
326
336
  return undefined;
327
337
  const text = ids
328
338
  .split(/\s+/)
@@ -426,8 +436,8 @@ function buildEmptyHint(query) {
426
436
  */
427
437
  export function runQuery(query, limit) {
428
438
  const result = matchQuery(query, undefined, limit);
429
- const scopeFields = result.scopeMissing === true ? { scopeMissing: true } : {};
430
- if (result.elements.length === 0) {
439
+ const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
440
+ if (0 === result.elements.length) {
431
441
  return {
432
442
  elements: result.elements,
433
443
  count: result.count,
package/dist/dom/realm.js CHANGED
@@ -15,12 +15,12 @@
15
15
  */
16
16
  /** The constructor named `name` in the realm that owns `node`, or undefined if there isn't one. */
17
17
  function ctorIn(node, name) {
18
- if (node === null || typeof node !== 'object')
18
+ if (null === node || typeof node !== 'object')
19
19
  return undefined;
20
20
  // A Document owns itself; every other node reports an ownerDocument.
21
21
  const doc = node.ownerDocument ?? node;
22
22
  const view = doc.defaultView;
23
- if (view === null || view === undefined)
23
+ if (null === view || view === undefined)
24
24
  return undefined;
25
25
  return view[name];
26
26
  }
@@ -32,17 +32,17 @@ function ctorIn(node, name) {
32
32
  */
33
33
  function isIn(node, name, ambient) {
34
34
  const ctor = ctorIn(node, name) ?? ambient;
35
- return typeof ctor === 'function' && node instanceof ctor;
35
+ return 'function' === typeof ctor && node instanceof ctor;
36
36
  }
37
- export const isElement = (n) => isIn(n, 'Element', typeof Element === 'undefined' ? undefined : Element);
38
- export const isImage = (n) => isIn(n, 'HTMLImageElement', typeof HTMLImageElement === 'undefined' ? undefined : HTMLImageElement);
39
- export const isHtmlElement = (n) => isIn(n, 'HTMLElement', typeof HTMLElement === 'undefined' ? undefined : HTMLElement);
40
- export const isInput = (n) => isIn(n, 'HTMLInputElement', typeof HTMLInputElement === 'undefined' ? undefined : HTMLInputElement);
41
- export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', typeof HTMLTextAreaElement === 'undefined' ? undefined : HTMLTextAreaElement);
42
- export const isSelect = (n) => isIn(n, 'HTMLSelectElement', typeof HTMLSelectElement === 'undefined' ? undefined : HTMLSelectElement);
43
- export const isButton = (n) => isIn(n, 'HTMLButtonElement', typeof HTMLButtonElement === 'undefined' ? undefined : HTMLButtonElement);
44
- export const isForm = (n) => isIn(n, 'HTMLFormElement', typeof HTMLFormElement === 'undefined' ? undefined : HTMLFormElement);
45
- export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', typeof HTMLIFrameElement === 'undefined' ? undefined : HTMLIFrameElement);
37
+ export const isElement = (n) => isIn(n, 'Element', 'undefined' === typeof Element ? undefined : Element);
38
+ export const isImage = (n) => isIn(n, 'HTMLImageElement', 'undefined' === typeof HTMLImageElement ? undefined : HTMLImageElement);
39
+ export const isHtmlElement = (n) => isIn(n, 'HTMLElement', 'undefined' === typeof HTMLElement ? undefined : HTMLElement);
40
+ export const isInput = (n) => isIn(n, 'HTMLInputElement', 'undefined' === typeof HTMLInputElement ? undefined : HTMLInputElement);
41
+ export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', 'undefined' === typeof HTMLTextAreaElement ? undefined : HTMLTextAreaElement);
42
+ export const isSelect = (n) => isIn(n, 'HTMLSelectElement', 'undefined' === typeof HTMLSelectElement ? undefined : HTMLSelectElement);
43
+ export const isButton = (n) => isIn(n, 'HTMLButtonElement', 'undefined' === typeof HTMLButtonElement ? undefined : HTMLButtonElement);
44
+ export const isForm = (n) => isIn(n, 'HTMLFormElement', 'undefined' === typeof HTMLFormElement ? undefined : HTMLFormElement);
45
+ export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', 'undefined' === typeof HTMLIFrameElement ? undefined : HTMLIFrameElement);
46
46
  /**
47
47
  * The prototype whose native value setter drives `el` — React and friends install their own accessor
48
48
  * on the instance, so a value write has to go through the prototype's. It must come from the
@@ -54,5 +54,5 @@ export function valuePrototypeOf(el) {
54
54
  if (typeof ctor !== 'function')
55
55
  return undefined;
56
56
  const proto = ctor.prototype;
57
- return typeof proto === 'object' && proto !== null ? proto : undefined;
57
+ return 'object' === typeof proto && proto !== null ? proto : undefined;
58
58
  }
@@ -17,6 +17,20 @@ import { type Ref } from '@reticlehq/core';
17
17
  * ref is one agent turn, and this is far more than any one turn produces.
18
18
  */
19
19
  export declare const MAX_TRACKED_REFS = 10000;
20
+ /**
21
+ * A ref, made safe to interpolate into an error message.
22
+ *
23
+ * A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
24
+ * an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
25
+ * value directly, so the caller's own bad argument came back as a 100KB error.
26
+ *
27
+ * The server caps error messages centrally, but it cannot help here: the browser's transport
28
+ * serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
29
+ * exact substring the server's recovery table matches on. A stale ref then arrives classified as a
30
+ * possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
31
+ * the message.
32
+ */
33
+ export declare function echoRef(ref: string): string;
20
34
  export declare class RefRegistry {
21
35
  #private;
22
36
  /** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
package/dist/dom/refs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { asRef } from '@reticlehq/core';
1
+ import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
2
2
  /**
3
3
  * Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
4
4
  * element keeps its ref across snapshots, and a ref re-resolves to its element as long as
@@ -17,6 +17,25 @@ import { asRef } from '@reticlehq/core';
17
17
  * ref is one agent turn, and this is far more than any one turn produces.
18
18
  */
19
19
  export const MAX_TRACKED_REFS = 10000;
20
+ /** Marks a ref that was too long to echo in full. */
21
+ const REF_ELISION = '…';
22
+ /**
23
+ * A ref, made safe to interpolate into an error message.
24
+ *
25
+ * A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
26
+ * an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
27
+ * value directly, so the caller's own bad argument came back as a 100KB error.
28
+ *
29
+ * The server caps error messages centrally, but it cannot help here: the browser's transport
30
+ * serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
31
+ * exact substring the server's recovery table matches on. A stale ref then arrives classified as a
32
+ * possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
33
+ * the message.
34
+ */
35
+ export function echoRef(ref) {
36
+ const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
37
+ return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
38
+ }
20
39
  /**
21
40
  * Full-sweep cadence. The deref-sweep is O(retained), and a page legitimately holding ≥ MAX_TRACKED_REFS
22
41
  * live elements (a 10k-node dashboard) can never get under the cap that way, so running it on EVERY mint
@@ -65,7 +65,7 @@ function sweep() {
65
65
  }
66
66
  while (roots.size > MAX_TRACKED_ROOTS) {
67
67
  const oldest = roots.values().next();
68
- if (oldest.done === true)
68
+ if (true === oldest.done)
69
69
  break;
70
70
  roots.delete(oldest.value);
71
71
  }
@@ -144,7 +144,7 @@ export function installShadowRegistry() {
144
144
  const sweep = (node) => {
145
145
  for (const el of node.querySelectorAll('*')) {
146
146
  const root = el.shadowRoot;
147
- if (root === null)
147
+ if (null === root)
148
148
  continue;
149
149
  record(root, el); // idempotent — a root the patch already caught is skipped inside
150
150
  sweep(root);
@@ -31,7 +31,7 @@ const TEXT_MAX = 80;
31
31
  function directText(el) {
32
32
  let out = '';
33
33
  for (const node of el.childNodes) {
34
- if (node.nodeType === 3 /* Node.TEXT_NODE */)
34
+ if (3 === node.nodeType /* Node.TEXT_NODE */)
35
35
  out += node.textContent ?? '';
36
36
  }
37
37
  const collapsed = out.replace(/\s+/g, ' ').trim();
@@ -43,7 +43,7 @@ function skipEarly(el) {
43
43
  return true;
44
44
  if (isIgnored(el))
45
45
  return true; // Reticle overlay + known dev overlays
46
- if (el.getAttribute('aria-hidden') === 'true')
46
+ if ('true' === el.getAttribute('aria-hidden'))
47
47
  return true;
48
48
  if (el instanceof HTMLElement && el.hidden)
49
49
  return true;
@@ -75,13 +75,13 @@ function formatTextLine(depth, text) {
75
75
  * snapshot is blind to it; this line makes it visible. Empty for non-container elements.
76
76
  */
77
77
  function layoutSignature(style) {
78
- if (style === null)
78
+ if (null === style)
79
79
  return '';
80
80
  const display = style.display;
81
81
  // Grid track templates are the high-signal CLS case (column/row count + sizing) and there are
82
82
  // few grid containers per page. Flex is intentionally excluded: nearly every row is a flex
83
83
  // box, so signing them all floods the snapshot for little regression value.
84
- if (display === 'grid' || display === 'inline-grid') {
84
+ if ('grid' === display || 'inline-grid' === display) {
85
85
  const cols = style.gridTemplateColumns;
86
86
  return cols !== '' && cols !== 'none' ? `grid-cols:${cols}` : 'grid';
87
87
  }
@@ -130,7 +130,7 @@ function walk(parent, depth, ctx) {
130
130
  // display-none skip and the layout signature — was two forced style resolutions per node.
131
131
  const view = child.ownerDocument.defaultView;
132
132
  const style = view !== null ? view.getComputedStyle(child) : null;
133
- if (style !== null && style.display === 'none')
133
+ if (style !== null && 'none' === style.display)
134
134
  continue;
135
135
  const role = getRole(child);
136
136
  const name = getAccessibleName(child);
@@ -138,14 +138,14 @@ function walk(parent, depth, ctx) {
138
138
  // A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
139
139
  // so the actionable-only view stays lean while FULL/meaningful views see content regressions.
140
140
  const lean = ctx.mode === SnapshotMode.INTERACTIVE;
141
- const text = !lean && role === 'generic' && name.length === 0 ? directText(child) : '';
141
+ const text = !lean && 'generic' === role && 0 === name.length ? directText(child) : '';
142
142
  // Layout signature for grid/flex containers — makes CLS/layout regressions visible.
143
143
  const layout = lean ? '' : layoutSignature(style);
144
144
  const meaningful = interactive || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
145
145
  const include = lean ? interactive : meaningful;
146
146
  if (include) {
147
147
  ctx.nodes += 1;
148
- ctx.lines.push(text.length > 0 && name.length === 0 && layout.length === 0
148
+ ctx.lines.push(text.length > 0 && 0 === name.length && 0 === layout.length
149
149
  ? formatTextLine(depth, text)
150
150
  : formatLine(child, depth, role, name, layout));
151
151
  walk(child, depth + 1, ctx);
@@ -24,14 +24,14 @@ import { DATA_RETICLE_SOURCE_ATTR } from '@reticlehq/core';
24
24
  const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
25
25
  /** Parse a `data-reticle-source="file:line:column"` value into `{ file, line }` (column dropped). */
26
26
  export function parseSourceAttr(value) {
27
- if (value === null)
27
+ if (null === value)
28
28
  return undefined;
29
29
  const m = /^(.*):(\d+):(\d+)$/.exec(value);
30
- if (m === null)
30
+ if (null === m)
31
31
  return undefined;
32
32
  const file = m[1];
33
33
  const line = Number(m[2]);
34
- if (file === undefined || file.length === 0 || !Number.isFinite(line))
34
+ if (file === undefined || 0 === file.length || !Number.isFinite(line))
35
35
  return undefined;
36
36
  return { file, line };
37
37
  }
package/dist/dom/theme.js CHANGED
@@ -11,12 +11,12 @@
11
11
  let cached = null;
12
12
  /** Resolve any CSS color string to canonical computed `rgb...)`/`rgba...)`, or null if not a color. */
13
13
  function toRgb(value) {
14
- if (value.length === 0)
14
+ if (0 === value.length)
15
15
  return null;
16
16
  const probe = document.createElement('span');
17
17
  probe.style.color = '';
18
18
  probe.style.color = value; // invalid colors leave it empty (the setter rejects them)
19
- if (probe.style.color === '')
19
+ if ('' === probe.style.color)
20
20
  return null;
21
21
  probe.style.position = 'absolute';
22
22
  probe.style.pointerEvents = 'none';
@@ -36,7 +36,7 @@ function collectTokens() {
36
36
  catch {
37
37
  continue;
38
38
  }
39
- if (rules === null)
39
+ if (null === rules)
40
40
  continue;
41
41
  for (const rule of Array.from(rules)) {
42
42
  if (!(rule instanceof CSSStyleRule))
@@ -66,15 +66,15 @@ function palette() {
66
66
  }
67
67
  /** True for a color that carries no visual weight (fully transparent) — never flagged off-theme. */
68
68
  function isTransparent(rgb) {
69
- return rgb === 'rgba(0, 0, 0, 0)' || rgb === 'transparent';
69
+ return 'rgba(0, 0, 0, 0)' === rgb || 'transparent' === rgb;
70
70
  }
71
71
  /** Report an element's theme compliance from its computed style. */
72
72
  export function themeReport(cs) {
73
73
  const p = palette();
74
74
  const colorToken = p.byColor.get(cs.color) ?? null;
75
75
  const backgroundToken = p.byColor.get(cs.backgroundColor) ?? null;
76
- const colorOff = !isTransparent(cs.color) && colorToken === null;
77
- const bgOff = !isTransparent(cs.backgroundColor) && backgroundToken === null;
76
+ const colorOff = !isTransparent(cs.color) && null === colorToken;
77
+ const bgOff = !isTransparent(cs.backgroundColor) && null === backgroundToken;
78
78
  return {
79
79
  colorToken,
80
80
  backgroundToken,
@@ -26,7 +26,7 @@ function rowHost(container) {
26
26
  let host = container;
27
27
  for (let depth = 0; depth < 3; depth += 1) {
28
28
  const children = Array.from(host.children).filter((c) => c instanceof HTMLElement);
29
- const only = children.length === 1 ? children[0] : undefined;
29
+ const only = 1 === children.length ? children[0] : undefined;
30
30
  if (only === undefined)
31
31
  return host;
32
32
  // A spacer is a single child that fills (or over-fills) the scroll area.
package/dist/index.d.ts CHANGED
@@ -8,9 +8,9 @@ export { registerAdapter, identifyComponent, readComponentState, elementHasHover
8
8
  export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/adapters.js';
9
9
  export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, } from './registry/stores.js';
10
10
  export type { StoreGetter, StoreLike, StoreSubscribe } from './registry/stores.js';
11
- export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
12
- export type { QuerySnapshot } from './registry/store-adapters.js';
13
- export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
11
+ export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
12
+ export type { QuerySnapshot, RecoilAtomSnapshot } from './registry/store-adapters.js';
13
+ export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
14
14
  export type { Capabilities, CapabilitiesInput, CapabilityFlow } from './registry/capabilities.js';
15
15
  export { createReticleEmitter } from './registry/emitter.js';
16
16
  export type { ReticleEmitter, EmitterTarget, CreateReticleEmitterOptions, } from './registry/emitter.js';
package/dist/index.js CHANGED
@@ -18,9 +18,9 @@ export { registerStore, unregisterStore, storeNames, readStores, readStoresWithT
18
18
  // Adapters for state libraries that do not natively expose `{getState, subscribe}`. zustand and Redux
19
19
  // need none of these; TanStack Query is the one that matters most, because a stale-cache bug fires no
20
20
  // request and renders a plausible value, so the cache is the only witness.
21
- export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
21
+ export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
22
22
  // Capability registry: the app self-describes its testable surface via reticle.describe.
23
- export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
23
+ export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
24
24
  // SDK helpers: adopt the recommended integration patterns without boilerplate.
25
25
  export { createReticleEmitter } from './registry/emitter.js';
26
26
  export { commitAndSignal } from './registry/commit-and-signal.js';
@@ -51,10 +51,10 @@ export function countClosedShadowRoots() {
51
51
  */
52
52
  export function isCrossOriginFrame(frame) {
53
53
  const src = frame.getAttribute('src');
54
- if (src === null || src.length === 0)
54
+ if (null === src || 0 === src.length)
55
55
  return false;
56
56
  try {
57
- return frame.contentDocument === null;
57
+ return null === frame.contentDocument;
58
58
  }
59
59
  catch {
60
60
  return true; // SecurityError on access is itself proof of cross-origin
@@ -12,7 +12,7 @@ const METHOD_EVENT = {
12
12
  function stringifyArgs(args) {
13
13
  return args
14
14
  .map((a) => {
15
- if (typeof a === 'string')
15
+ if ('string' === typeof a)
16
16
  return a;
17
17
  if (a instanceof Error)
18
18
  return a.message;
@@ -23,7 +23,7 @@ function stringifyArgs(args) {
23
23
  /** Stacks can be long; cap so a deep async trace never blows the event budget. */
24
24
  const MAX_STACK_LEN = TRANSPORT_LIMITS.MAX_STACK_LENGTH;
25
25
  function capStack(stack) {
26
- if (stack === undefined || stack.length === 0)
26
+ if (stack === undefined || 0 === stack.length)
27
27
  return undefined;
28
28
  return stack.length > MAX_STACK_LEN ? stack.slice(0, MAX_STACK_LEN) : stack;
29
29
  }
@@ -47,7 +47,7 @@ export function installConsole(emit) {
47
47
  const callOriginal = original.bind(console);
48
48
  const wrapper = (...args) => {
49
49
  // Only console.error carries a stack — the diagnosis case; log/warn stay lean.
50
- const stack = method === 'error' ? firstErrorStack(args) : undefined;
50
+ const stack = 'error' === method ? firstErrorStack(args) : undefined;
51
51
  emit(METHOD_EVENT[method], {
52
52
  message: stringifyArgs(args),
53
53
  ...(stack === undefined ? {} : { stack }),
@@ -9,7 +9,7 @@ import { refs } from '../dom/refs.js';
9
9
  */
10
10
  function regionKeyOf(target) {
11
11
  const el = isElement(target) ? target : null;
12
- if (el === null)
12
+ if (null === el)
13
13
  return undefined;
14
14
  const labelled = el.closest('[data-testid]');
15
15
  return labelled?.getAttribute('data-testid') ?? refs.refFor(el);
@@ -37,7 +37,7 @@ const WATCHED_ATTRS = [
37
37
  /** Attribute/text values are capped per event — style/src can be huge and would bloat the ledger. */
38
38
  const MAX_ATTR_VALUE_LEN = 120;
39
39
  function capValue(value) {
40
- if (value === null)
40
+ if (null === value)
41
41
  return undefined;
42
42
  return value.length > MAX_ATTR_VALUE_LEN ? `${value.slice(0, MAX_ATTR_VALUE_LEN)}…` : value;
43
43
  }
@@ -82,7 +82,7 @@ export function installDom(emit) {
82
82
  // (it can include not-yet-inspected noise) but `dropped > 0` honestly means "this batch was capped".
83
83
  let dropped = 0;
84
84
  for (const record of records) {
85
- if (record.type === 'attributes') {
85
+ if ('attributes' === record.type) {
86
86
  const target = record.target;
87
87
  if (isElement(target) && record.attributeName !== null && !isReticleOverlay(target)) {
88
88
  if (changed >= MAX_PER_BATCH) {
@@ -101,7 +101,7 @@ export function installDom(emit) {
101
101
  }
102
102
  continue;
103
103
  }
104
- if (record.type === 'characterData') {
104
+ if ('characterData' === record.type) {
105
105
  // In-place text change inside an existing subtree (wizard steps, inline edits) —
106
106
  // childList-only would miss this.
107
107
  const parent = record.target.parentElement;
@@ -158,7 +158,7 @@ export function installDom(emit) {
158
158
  emit(EventType.DOM_ADDED, { role, name, region: regionKeyOf(record.target) }, ref);
159
159
  if (DIALOG_ROLES.has(role) ||
160
160
  LIVE_ROLES.has(role) ||
161
- node.getAttribute('aria-modal') === 'true') {
161
+ 'true' === node.getAttribute('aria-modal')) {
162
162
  if (isVisible(node))
163
163
  emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
164
164
  }
@@ -1,6 +1,7 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- export interface DownloadOptions {
2
+ interface DownloadOptions {
3
3
  /** Include a content preview. Gated for the same reason request bodies are: exports hold PII. */
4
4
  capturePreview?: boolean;
5
5
  }
6
6
  export declare function installDownload(emit: Emit, opts?: DownloadOptions): Teardown;
7
+ export {};
@@ -63,7 +63,7 @@ export function installDownload(emit, opts = {}) {
63
63
  // The LINE COUNT is reported even without preview capture: it is a number, not content, and it
64
64
  // is the whole tell for "25 rows in a file the toast calls 128".
65
65
  data['lines'] = text.split('\n').filter((l) => l.length > 0).length;
66
- if (opts.capturePreview === true) {
66
+ if (true === opts.capturePreview) {
67
67
  const { body, truncated } = projectBody(text.slice(0, PREVIEW_CHARS), record.type);
68
68
  data['preview'] = body;
69
69
  if (truncated)
@@ -79,7 +79,7 @@ export function installDownload(emit, opts = {}) {
79
79
  const onClick = (event) => {
80
80
  const target = event.target;
81
81
  const anchor = target instanceof Element ? target.closest('a[download]') : null;
82
- if (anchor === null)
82
+ if (null === anchor)
83
83
  return;
84
84
  report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
85
85
  };
@@ -9,7 +9,7 @@ export function focusLabel(el) {
9
9
  return name.length > 0 ? `${role} "${name}"` : role;
10
10
  }
11
11
  function toBody(el) {
12
- return el === null || el === document.body;
12
+ return null === el || el === document.body;
13
13
  }
14
14
  /**
15
15
  * Track element focus movement. Focus dropping to <body> after an act — a modal that closed and stranded
@@ -32,7 +32,7 @@ export function sameOriginFrameBodies(root) {
32
32
  return;
33
33
  for (const frame of node.querySelectorAll('iframe')) {
34
34
  const body = bodyOf(frame);
35
- if (body === null)
35
+ if (null === body)
36
36
  continue;
37
37
  found.push(body);
38
38
  walk(body, depth + 1);