@reticlehq/browser 3.6.0 → 3.7.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 (83) hide show
  1. package/dist/actions/action-report.d.ts +54 -0
  2. package/dist/actions/action-report.js +60 -0
  3. package/dist/actions/actions-dom.d.ts +1 -0
  4. package/dist/actions/actions-dom.js +12 -0
  5. package/dist/actions/actions-press.d.ts +1 -0
  6. package/dist/actions/actions-press.js +25 -1
  7. package/dist/actions/actions.d.ts +1 -37
  8. package/dist/actions/actions.js +24 -66
  9. package/dist/actions/appeared-text.js +3 -0
  10. package/dist/actions/danger-context.d.ts +2 -0
  11. package/dist/actions/danger-context.js +28 -2
  12. package/dist/actions/value-input.d.ts +1 -0
  13. package/dist/actions/value-input.js +11 -0
  14. package/dist/commands/commands.js +4 -0
  15. package/dist/dom/a11y.js +251 -10
  16. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  17. package/dist/dom/addressing/coverage-markers.js +13 -0
  18. package/dist/dom/dom-ignore.d.ts +2 -0
  19. package/dist/dom/dom-ignore.js +15 -2
  20. package/dist/dom/query.d.ts +1 -1
  21. package/dist/dom/query.js +78 -16
  22. package/dist/dom/render-page.d.ts +12 -0
  23. package/dist/dom/render-page.js +135 -0
  24. package/dist/dom/snapshot.js +55 -7
  25. package/dist/observers/dom.js +32 -16
  26. package/dist/observers/field.js +3 -21
  27. package/dist/observers/network.js +56 -7
  28. package/dist/observers/perf.js +53 -1
  29. package/dist/observers/route.js +6 -5
  30. package/dist/presenter/carousel/carousel.js +2 -7
  31. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  32. package/dist/presenter/carousel/panel-slides.js +17 -15
  33. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  34. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  35. package/dist/presenter/chrome/presenter-log.js +48 -8
  36. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  37. package/dist/presenter/chrome/presenter-morph.js +34 -0
  38. package/dist/presenter/chrome/presenter-report-copy.d.ts +17 -4
  39. package/dist/presenter/chrome/presenter-report-copy.js +26 -4
  40. package/dist/presenter/hud-telemetry.js +55 -4
  41. package/dist/presenter/presenter-account-styles.js +3 -3
  42. package/dist/presenter/presenter-account.d.ts +27 -5
  43. package/dist/presenter/presenter-account.js +83 -6
  44. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  45. package/dist/presenter/presenter-agent-link.js +62 -0
  46. package/dist/presenter/presenter-chat-views.d.ts +27 -5
  47. package/dist/presenter/presenter-chat-views.js +312 -72
  48. package/dist/presenter/presenter-config.d.ts +2 -0
  49. package/dist/presenter/presenter-config.js +1 -0
  50. package/dist/presenter/presenter-controls.d.ts +0 -1
  51. package/dist/presenter/presenter-controls.js +13 -91
  52. package/dist/presenter/presenter-design-tokens.js +1 -1
  53. package/dist/presenter/presenter-frame.d.ts +1 -1
  54. package/dist/presenter/presenter-frame.js +8 -10
  55. package/dist/presenter/presenter-harness-row.d.ts +67 -0
  56. package/dist/presenter/presenter-harness-row.js +209 -0
  57. package/dist/presenter/presenter-personas.d.ts +35 -0
  58. package/dist/presenter/presenter-personas.js +95 -0
  59. package/dist/presenter/presenter-plan.js +25 -24
  60. package/dist/presenter/presenter-report-styles.js +40 -22
  61. package/dist/presenter/presenter-report.d.ts +2 -1
  62. package/dist/presenter/presenter-report.js +83 -17
  63. package/dist/presenter/presenter-settings-styles.js +8 -4
  64. package/dist/presenter/presenter-settings.d.ts +7 -6
  65. package/dist/presenter/presenter-settings.js +75 -34
  66. package/dist/presenter/presenter-shell-styles.js +12 -3
  67. package/dist/presenter/presenter-shell.d.ts +10 -4
  68. package/dist/presenter/presenter-shell.js +31 -23
  69. package/dist/presenter/presenter-styles.js +0 -2
  70. package/dist/presenter/presenter-workspace.js +10 -3
  71. package/dist/presenter/presenter.d.ts +3 -2
  72. package/dist/presenter/presenter.js +81 -20
  73. package/dist/registry/declared-platform.js +2 -0
  74. package/dist/reticle-inject.js +777 -700
  75. package/dist/reticle.js +36 -2
  76. package/dist/storage-keys.d.ts +3 -2
  77. package/dist/storage-keys.js +7 -3
  78. package/dist/transport/transport.js +5 -3
  79. package/dist/transport/unreachable-message.d.ts +1 -1
  80. package/dist/transport/unreachable-message.js +3 -5
  81. package/package.json +2 -2
  82. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  83. package/dist/presenter/carousel/offer-card.js +0 -56
@@ -0,0 +1,54 @@
1
+ import { type SettleReason } from '@reticlehq/core';
2
+ export interface ActionEffect {
3
+ dispatched: true;
4
+ targetMatched: boolean;
5
+ visible: boolean;
6
+ enabled: boolean;
7
+ defaultPrevented: boolean;
8
+ focusMoved: string | null;
9
+ valueChanged: boolean;
10
+ autofillSafe?: true;
11
+ domMutatedWithin: number;
12
+ appeared?: string;
13
+ occluded: boolean;
14
+ occludedBy: string | null;
15
+ scrolledIntoView: boolean;
16
+ heldMs?: number;
17
+ alreadyAtValue?: true;
18
+ }
19
+ export interface ActionResult {
20
+ ok: true;
21
+ ref: string;
22
+ action: string;
23
+ dispatched: boolean;
24
+ settled: boolean;
25
+ settleReason: SettleReason | null;
26
+ effect: ActionEffect;
27
+ testid?: string;
28
+ component?: string;
29
+ role?: string;
30
+ name?: string;
31
+ nth?: number;
32
+ of?: number;
33
+ source?: {
34
+ file: string;
35
+ line: number;
36
+ column?: number;
37
+ };
38
+ warning?: string;
39
+ }
40
+ export interface CapturedAnchor {
41
+ testid?: string;
42
+ component?: string;
43
+ source?: {
44
+ file: string;
45
+ line: number;
46
+ column?: number;
47
+ };
48
+ role?: string;
49
+ name?: string;
50
+ nth?: number;
51
+ of?: number;
52
+ }
53
+ export declare function anchorOf(el: Element): CapturedAnchor;
54
+ export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
@@ -0,0 +1,60 @@
1
+ import { readTestId } from '../dom/addressing/testid-attr.js';
2
+ import { QueryBy } from '@reticlehq/core';
3
+ import { elementsMatching } from '../dom/query.js';
4
+ import { getAccessibleName, getRole } from '../dom/a11y.js';
5
+ import { identifyComponent } from '../registry/stores/adapters.js';
6
+ export function anchorOf(el) {
7
+ const testid = readTestId(el) ?? undefined;
8
+ const info = identifyComponent(el);
9
+ const out = {};
10
+ if (testid !== undefined)
11
+ out.testid = testid;
12
+ const component = info?.componentStack[0];
13
+ if (component !== undefined)
14
+ out.component = component;
15
+ if (info?.source !== undefined)
16
+ out.source = info.source;
17
+ const role = getRole(el);
18
+ const name = getAccessibleName(el);
19
+ if (role.length > 0)
20
+ out.role = role;
21
+ if (name.length > 0)
22
+ out.name = name;
23
+ if (role.length > 0 && name.length > 0) {
24
+ const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
25
+ const at = same.indexOf(el);
26
+ if (1 < same.length && 0 <= at) {
27
+ out.nth = at;
28
+ out.of = same.length;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ export const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
34
+ const base = {
35
+ ok: true,
36
+ ref,
37
+ action,
38
+ dispatched: true,
39
+ settled,
40
+ settleReason,
41
+ effect,
42
+ };
43
+ if (anchor.testid !== undefined)
44
+ base.testid = anchor.testid;
45
+ if (anchor.component !== undefined)
46
+ base.component = anchor.component;
47
+ if (anchor.source !== undefined)
48
+ base.source = anchor.source;
49
+ if (anchor.role !== undefined)
50
+ base.role = anchor.role;
51
+ if (anchor.name !== undefined)
52
+ base.name = anchor.name;
53
+ if (anchor.nth !== undefined)
54
+ base.nth = anchor.nth;
55
+ if (anchor.of !== undefined)
56
+ base.of = anchor.of;
57
+ if (warning !== undefined)
58
+ base.warning = warning;
59
+ return base;
60
+ };
@@ -26,4 +26,5 @@ export declare function fireTapSequence(el: ActionTarget, hold: {
26
26
  prevented: boolean;
27
27
  heldMs: number;
28
28
  }>;
29
+ export declare function assertNotLeaving(el: Element): void;
29
30
  export {};
@@ -231,3 +231,15 @@ function makeTouchEvent(type, touches) {
231
231
  }
232
232
  return new Event(type, { bubbles: true, cancelable: true });
233
233
  }
234
+ const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialog', 'tooltip']);
235
+ const CLOSED_STATE = 'closed';
236
+ export function assertNotLeaving(el) {
237
+ for (let up = el.parentElement; up !== null; up = up.parentElement) {
238
+ if (CLOSED_STATE !== up.getAttribute('data-state'))
239
+ continue;
240
+ if (!POPUP_ROLES.has(up.getAttribute('role') ?? ''))
241
+ continue;
242
+ throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
243
+ 'so a person could not click it: look again once it has gone and act on what is open');
244
+ }
245
+ }
@@ -8,6 +8,7 @@ export interface ModifierFlags {
8
8
  }
9
9
  export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
10
10
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
11
+ export declare function dispatchKeypress(el: ActionTarget, key: string, code: string, mods: ModifierFlags, repeat?: boolean): void;
11
12
  export declare function focusedOrDocument(): HTMLElement;
12
13
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
13
14
  export declare function pressKeys(args: Record<string, unknown>): string[];
@@ -57,6 +57,26 @@ export function pressCode(args, key) {
57
57
  }
58
58
  return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
59
59
  }
60
+ export function dispatchKeypress(el, key, code, mods, repeat = false) {
61
+ if (ENTER_KEY !== key && 1 !== key.length)
62
+ return;
63
+ const event = new KeyboardEvent('keypress', {
64
+ key,
65
+ code,
66
+ bubbles: true,
67
+ cancelable: true,
68
+ repeat,
69
+ ...mods,
70
+ });
71
+ const legacy = ENTER_KEY === key ? ENTER_CHAR_CODE : key.charCodeAt(0);
72
+ for (const field of LEGACY_KEY_CODE_FIELDS) {
73
+ Object.defineProperty(event, field, { value: legacy });
74
+ }
75
+ asSyntheticInput(() => el.dispatchEvent(event));
76
+ }
77
+ const ENTER_KEY = 'Enter';
78
+ const ENTER_CHAR_CODE = 13;
79
+ const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
60
80
  const KNOWN_NAMED_KEYS = new Set([
61
81
  'Enter',
62
82
  'Escape',
@@ -139,7 +159,7 @@ export async function holdKey(el, key, code, mods, ms) {
139
159
  if (ms > KEY_REPEAT_DELAY_MS)
140
160
  await sleep(KEY_REPEAT_DELAY_MS);
141
161
  while (Date.now() - started < ms) {
142
- asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
162
+ const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
143
163
  key,
144
164
  code,
145
165
  bubbles: true,
@@ -147,6 +167,8 @@ export async function holdKey(el, key, code, mods, ms) {
147
167
  repeat: true,
148
168
  ...mods,
149
169
  })));
170
+ if (down)
171
+ dispatchKeypress(el, key, code, mods, true);
150
172
  await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
151
173
  }
152
174
  }
@@ -179,6 +201,8 @@ export async function pressCombo(el, keys, mods, holdMs) {
179
201
  const ok = dispatch('keydown', key);
180
202
  if (!ok)
181
203
  prevented = true;
204
+ if (ok)
205
+ dispatchKeypress(el, key, pressCode({}, key), held);
182
206
  closeModalOnEscape(el, key, ok);
183
207
  }
184
208
  if (holdMs > 0)
@@ -1,41 +1,5 @@
1
1
  import { SettleReason } from '@reticlehq/core';
2
- interface ActionEffect {
3
- dispatched: true;
4
- targetMatched: boolean;
5
- visible: boolean;
6
- enabled: boolean;
7
- defaultPrevented: boolean;
8
- focusMoved: string | null;
9
- valueChanged: boolean;
10
- domMutatedWithin: number;
11
- appeared?: string;
12
- occluded: boolean;
13
- occludedBy: string | null;
14
- scrolledIntoView: boolean;
15
- heldMs?: number;
16
- alreadyAtValue?: true;
17
- }
18
- interface ActionResult {
19
- ok: true;
20
- ref: string;
21
- action: string;
22
- dispatched: boolean;
23
- settled: boolean;
24
- settleReason: SettleReason | null;
25
- effect: ActionEffect;
26
- testid?: string;
27
- component?: string;
28
- role?: string;
29
- name?: string;
30
- nth?: number;
31
- of?: number;
32
- source?: {
33
- file: string;
34
- line: number;
35
- column?: number;
36
- };
37
- warning?: string;
38
- }
2
+ import { type ActionEffect, type ActionResult } from './action-report.js';
39
3
  export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
40
4
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
41
5
  export interface ActionStep {
@@ -1,17 +1,16 @@
1
- import { readTestId } from '../dom/addressing/testid-attr.js';
2
- import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, QueryBy, SettleReason, } from '@reticlehq/core';
3
- import { elementsMatching } from '../dom/query.js';
1
+ import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
4
2
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
5
3
  import { echoRef, refs } from '../dom/addressing/refs.js';
6
- import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
7
- import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
8
- import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
9
- import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
4
+ import { dangerousActionContext, requiresDangerousConfirmation, assertOutwardAllowed, submitControlFor, } from './danger-context.js';
5
+ import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
6
+ import { getRole, isVisible, getStates } from '../dom/a11y.js';
7
+ import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
10
8
  import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
11
9
  import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
12
10
  import { AppearedText } from './appeared-text.js';
13
11
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
14
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
12
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
13
+ import { anchorOf, result } from './action-report.js';
15
14
  function asString(value, fallback = '') {
16
15
  return 'string' === typeof value ? value : fallback;
17
16
  }
@@ -23,61 +22,6 @@ function requireElement(ref) {
23
22
  throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
24
23
  return el;
25
24
  }
26
- function anchorOf(el) {
27
- const testid = readTestId(el) ?? undefined;
28
- const info = identifyComponent(el);
29
- const out = {};
30
- if (testid !== undefined)
31
- out.testid = testid;
32
- const component = info?.componentStack[0];
33
- if (component !== undefined)
34
- out.component = component;
35
- if (info?.source !== undefined)
36
- out.source = info.source;
37
- const role = getRole(el);
38
- const name = getAccessibleName(el);
39
- if (role.length > 0)
40
- out.role = role;
41
- if (name.length > 0)
42
- out.name = name;
43
- if (role.length > 0 && name.length > 0) {
44
- const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
45
- const at = same.indexOf(el);
46
- if (1 < same.length && 0 <= at) {
47
- out.nth = at;
48
- out.of = same.length;
49
- }
50
- }
51
- return out;
52
- }
53
- const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
54
- const base = {
55
- ok: true,
56
- ref,
57
- action,
58
- dispatched: true,
59
- settled,
60
- settleReason,
61
- effect,
62
- };
63
- if (anchor.testid !== undefined)
64
- base.testid = anchor.testid;
65
- if (anchor.component !== undefined)
66
- base.component = anchor.component;
67
- if (anchor.source !== undefined)
68
- base.source = anchor.source;
69
- if (anchor.role !== undefined)
70
- base.role = anchor.role;
71
- if (anchor.name !== undefined)
72
- base.name = anchor.name;
73
- if (anchor.nth !== undefined)
74
- base.nth = anchor.nth;
75
- if (anchor.of !== undefined)
76
- base.of = anchor.of;
77
- if (warning !== undefined)
78
- base.warning = warning;
79
- return base;
80
- };
81
25
  const FILL_LIKE = new Set([
82
26
  ActionType.FILL,
83
27
  ActionType.TYPE,
@@ -123,6 +67,8 @@ function assertActionAllowed(el, action, args) {
123
67
  (action === ActionType.PRESS && 'Enter' === pressKey(args));
124
68
  const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
125
69
  const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
70
+ if (canTrigger)
71
+ assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
126
72
  const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
127
73
  (submitter !== null &&
128
74
  requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
@@ -134,7 +80,13 @@ function assertActionAllowed(el, action, args) {
134
80
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
135
81
  }
136
82
  }
137
- import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, } from './actions-dom.js';
83
+ function optionValueFor(options, named) {
84
+ if (options.some((option) => option.value === named))
85
+ return named;
86
+ const byLabel = options.filter((option) => option.label.trim() === named.trim());
87
+ return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
88
+ }
89
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
138
90
  import { scrollFrom } from './scroll.js';
139
91
  function enabledOf(el) {
140
92
  return !getStates(el).includes(ElementState.DISABLED);
@@ -209,7 +161,8 @@ async function dispatchOther(el, action, args) {
209
161
  'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
210
162
  }
211
163
  assertEditable(el, 'fill');
212
- el.focus();
164
+ if (!isAutofillSafeTarget(el))
165
+ el.focus();
213
166
  return setNativeValue(el, args['value']);
214
167
  }
215
168
  assertNotRichText(el, 'fill');
@@ -235,8 +188,8 @@ async function dispatchOther(el, action, args) {
235
188
  if (typeof args['value'] !== 'string') {
236
189
  throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
237
190
  }
238
- const wanted = args['value'];
239
191
  const options = Array.from(el.options);
192
+ const wanted = optionValueFor(options, args['value']);
240
193
  if (!options.some((option) => option.value === wanted)) {
241
194
  const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
242
195
  throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
@@ -287,6 +240,8 @@ async function dispatchOther(el, action, args) {
287
240
  const code = pressCode(args, key);
288
241
  const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
289
242
  closeModalOnEscape(el, key, down);
243
+ if (down)
244
+ dispatchKeypress(el, key, code, mods);
290
245
  const hold = clampHoldMs(args['holdMs']);
291
246
  if (hold > 0)
292
247
  await holdKey(el, key, code, mods, hold);
@@ -355,6 +310,8 @@ export async function executeAction(ref, action, args = {}) {
355
310
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
356
311
  if (named)
357
312
  assertActionAllowed(el, action, args);
313
+ if (named)
314
+ assertNotLeaving(el);
358
315
  const anchor = named ? anchorOf(el) : {};
359
316
  const visible = isVisible(el);
360
317
  const enabled = enabledOf(el);
@@ -401,6 +358,7 @@ export async function executeAction(ref, action, args = {}) {
401
358
  defaultPrevented,
402
359
  focusMoved: prevFocus !== nextFocus ? `${prevFocus ?? 'null'}->${nextFocus ?? 'null'}` : null,
403
360
  valueChanged: isFillLike(action) ? valueBefore !== valueAfter : false,
361
+ ...(ActionType.FILL === action && isAutofillSafeTarget(el) ? { autofillSafe: true } : {}),
404
362
  domMutatedWithin: mutated,
405
363
  ...said.effect(valueAfter),
406
364
  occluded: geometry.occluded,
@@ -1,4 +1,5 @@
1
1
  import { isIgnored } from '../dom/dom-ignore.js';
2
+ import { isSensitiveField } from '../dom/a11y.js';
2
3
  const APPEARED_MAX = 200;
3
4
  const JOIN = ' | ';
4
5
  const TEXT_NODE = 3;
@@ -46,6 +47,8 @@ export class AppearedText {
46
47
  #add(raw, owner) {
47
48
  if (null !== owner && isIgnored(owner))
48
49
  return;
50
+ if (null !== owner && isSensitiveField(owner))
51
+ return;
49
52
  const text = (raw ?? '').replace(/\s+/g, ' ').trim();
50
53
  if (0 === text.length)
51
54
  return;
@@ -2,3 +2,5 @@ import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function dangerousActionContext(el: ActionTarget): string;
3
3
  export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
4
4
  export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
5
+ export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
6
+ export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
@@ -1,5 +1,5 @@
1
- import { isDangerousActionText } from '@reticlehq/core';
2
- import { getAccessibleName } from '../dom/a11y.js';
1
+ import { isDangerousActionText, outwardActionOf, outwardRefusal } from '@reticlehq/core';
2
+ import { getAccessibleName, getRole } from '../dom/a11y.js';
3
3
  import { isHtmlElement } from '../dom/realm.js';
4
4
  const LABEL_VALUED_INPUT_TYPES = new Set([
5
5
  'submit',
@@ -16,7 +16,18 @@ function holdsUserText(el) {
16
16
  return !LABEL_VALUED_INPUT_TYPES.has(el.type.toLowerCase());
17
17
  return false;
18
18
  }
19
+ const ANCHOR_TAG = 'A';
20
+ const INERT_HREF = /^\s*(#.*|javascript:.*)?$/i;
21
+ function navigatingHref(el) {
22
+ if (ANCHOR_TAG !== el.tagName)
23
+ return undefined;
24
+ const href = el.getAttribute('href');
25
+ return null === href || INERT_HREF.test(href) ? undefined : href;
26
+ }
19
27
  export function dangerousActionContext(el) {
28
+ const href = navigatingHref(el);
29
+ if (href !== undefined)
30
+ return href;
20
31
  const form = el.closest('form');
21
32
  const ownText = holdsUserText(el)
22
33
  ? ['', '']
@@ -38,3 +49,18 @@ export function submitControlFor(el) {
38
49
  export function requiresDangerousConfirmation(text, role) {
39
50
  return isDangerousActionText(text, role);
40
51
  }
52
+ let outwardAllowed;
53
+ export function setOutwardPolicy(allowed) {
54
+ outwardAllowed = allowed;
55
+ }
56
+ export function assertOutwardAllowed(targets) {
57
+ if (outwardAllowed === undefined)
58
+ return;
59
+ for (const el of targets) {
60
+ if (null === el)
61
+ continue;
62
+ const kind = outwardActionOf(dangerousActionContext(el), getRole(el));
63
+ if (kind !== undefined && !outwardAllowed.includes(kind))
64
+ throw new Error(outwardRefusal(kind));
65
+ }
66
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
+ export declare function isAutofillSafeTarget(el: Element, ua?: string): boolean;
2
3
  export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
3
4
  export declare function assertNotRichText(el: ActionTarget, action: string): void;
4
5
  export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
@@ -1,5 +1,16 @@
1
1
  import { captureValueSetter } from '../patching/capture-method.js';
2
2
  import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
3
+ const PASSWORD_AUTOCOMPLETE = /\b(current|new)-password\b/;
4
+ const NOT_WEBKIT = /Chrome|Chromium|Edg\/|Firefox|Gecko\//;
5
+ export function isAutofillSafeTarget(el, ua = navigator.userAgent) {
6
+ if (!(el instanceof HTMLInputElement))
7
+ return false;
8
+ const password = 'password' === el.type.toLowerCase() ||
9
+ PASSWORD_AUTOCOMPLETE.test(el.getAttribute('autocomplete') ?? '');
10
+ if (!password || NOT_WEBKIT.test(ua))
11
+ return false;
12
+ return 600 <= Number(/AppleWebKit\/(\d+)/.exec(ua)?.[1] ?? 0);
13
+ }
3
14
  export function setNativeValue(el, value) {
4
15
  const proto = valuePrototypeOf(el);
5
16
  const setter = proto === undefined ? undefined : captureValueSetter(proto);
@@ -122,6 +122,9 @@ function readState(ref, store, path, depth) {
122
122
  const names = storeNames();
123
123
  if (path !== undefined || depth !== undefined) {
124
124
  const rawStores = readStoresRaw(store);
125
+ if (store !== undefined && !Object.hasOwn(rawStores, store)) {
126
+ return { store, path, found: false, value: null, storeNames: names };
127
+ }
125
128
  const base = store !== undefined ? rawStores[store] : { stores: rawStores, storeNames: names };
126
129
  const selection = path !== undefined ? selectPath(base, path) : { found: true, value: base };
127
130
  const selected = selection.found && depth !== undefined ? capDepth(selection.value, depth) : selection.value;
@@ -220,6 +223,7 @@ export function createCommandRegistry() {
220
223
  reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
221
224
  reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
222
225
  reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(true === args['fullPage']));
226
+ reg.set(ReticleCommand.RENDER, () => import('../dom/render-page.js').then((m) => m.renderPage()));
223
227
  reg.set(ReticleCommand.SCROLL, (args) => {
224
228
  const dy = args['dy'];
225
229
  const fraction = args['fraction'];