@reticlehq/browser 3.4.0 → 3.6.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.
- package/dist/actions/actions-dom.js +44 -43
- package/dist/actions/actions-press.d.ts +10 -4
- package/dist/actions/actions-press.js +108 -17
- package/dist/actions/actions.d.ts +2 -1
- package/dist/actions/actions.js +25 -26
- package/dist/actions/danger-context.js +1 -1
- package/dist/commands/commands.js +2 -6
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +43 -11
- package/dist/dom/addressing/refs.js +2 -1
- package/dist/dom/addressing/testid-attr.d.ts +4 -0
- package/dist/dom/addressing/testid-attr.js +14 -0
- package/dist/dom/occlusion.js +5 -1
- package/dist/dom/query.d.ts +1 -0
- package/dist/dom/query.js +42 -15
- package/dist/dom/snapshot.js +1 -0
- package/dist/observers/departure.js +51 -9
- package/dist/observers/dom.js +3 -2
- package/dist/observers/field.js +5 -3
- package/dist/observers/health.js +63 -2
- package/dist/observers/network-stream.d.ts +1 -1
- package/dist/observers/network-stream.js +3 -3
- package/dist/observers/network.js +1 -1
- package/dist/observers/storage.js +6 -1
- package/dist/presenter/carousel/carousel.d.ts +5 -2
- package/dist/presenter/carousel/carousel.js +34 -7
- package/dist/presenter/carousel/offer-card.d.ts +2 -2
- package/dist/presenter/carousel/offer-card.js +2 -1
- package/dist/presenter/carousel/panel-slides.d.ts +4 -3
- package/dist/presenter/carousel/panel-slides.js +43 -6
- package/dist/presenter/carousel/talk-card.d.ts +1 -1
- package/dist/presenter/chrome/log-kinds.d.ts +5 -0
- package/dist/presenter/chrome/log-kinds.js +1 -0
- package/dist/presenter/chrome/log-memory.d.ts +12 -0
- package/dist/presenter/chrome/log-memory.js +39 -0
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
- package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
- package/dist/presenter/chrome/presenter-log.d.ts +5 -4
- package/dist/presenter/chrome/presenter-log.js +55 -5
- package/dist/presenter/chrome/presenter-report-copy.d.ts +11 -1
- package/dist/presenter/chrome/presenter-report-copy.js +14 -1
- package/dist/presenter/hud-telemetry.js +31 -2
- package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
- package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
- package/dist/presenter/icons/presenter-icons.d.ts +2 -0
- package/dist/presenter/icons/presenter-icons.js +2 -0
- package/dist/presenter/presenter-account.d.ts +5 -4
- package/dist/presenter/presenter-account.js +23 -10
- package/dist/presenter/presenter-chat-views.d.ts +21 -0
- package/dist/presenter/presenter-chat-views.js +382 -0
- package/dist/presenter/presenter-config.d.ts +3 -2
- package/dist/presenter/presenter-config.js +4 -3
- package/dist/presenter/presenter-controls.d.ts +4 -2
- package/dist/presenter/presenter-controls.js +231 -51
- package/dist/presenter/presenter-design-tokens.d.ts +1 -0
- package/dist/presenter/presenter-design-tokens.js +34 -0
- package/dist/presenter/presenter-dock-layout.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +12 -0
- package/dist/presenter/presenter-frame.js +189 -0
- package/dist/presenter/presenter-plan.d.ts +10 -0
- package/dist/presenter/presenter-plan.js +185 -0
- package/dist/presenter/presenter-report-styles.js +7 -0
- package/dist/presenter/presenter-report.d.ts +1 -1
- package/dist/presenter/presenter-report.js +53 -16
- package/dist/presenter/presenter-settings-styles.js +10 -0
- package/dist/presenter/presenter-settings.d.ts +5 -0
- package/dist/presenter/presenter-settings.js +71 -11
- package/dist/presenter/presenter-shell-styles.js +39 -42
- package/dist/presenter/presenter-shell.d.ts +8 -1
- package/dist/presenter/presenter-shell.js +124 -72
- package/dist/presenter/presenter-styles.js +9 -1
- package/dist/presenter/presenter.d.ts +4 -3
- package/dist/presenter/presenter.js +101 -13
- package/dist/presenter/tour/tour.d.ts +4 -1
- package/dist/presenter/tour/tour.js +6 -4
- package/dist/recorder/recorder.js +2 -2
- package/dist/registry/auto-testids.js +3 -4
- package/dist/registry/declared-platform.js +2 -0
- package/dist/reticle-inject.js +880 -513
- package/dist/reticle.d.ts +10 -1
- package/dist/reticle.js +73 -9
- package/dist/review/annotator-styles.d.ts +1 -1
- package/dist/review/annotator.d.ts +7 -0
- package/dist/review/annotator.js +20 -2
- package/dist/review/mark-anchor.js +2 -2
- package/dist/session-continuity.js +2 -1
- package/dist/storage-keys.d.ts +14 -0
- package/dist/storage-keys.js +17 -0
- package/package.json +3 -2
package/dist/dom/occlusion.js
CHANGED
|
@@ -14,9 +14,13 @@ export function hitTestOccluder(el, rect) {
|
|
|
14
14
|
if (typeof doc.elementFromPoint !== 'function')
|
|
15
15
|
return null;
|
|
16
16
|
const withStack = doc;
|
|
17
|
+
const isOursButNotTheTarget = (candidate) => isReticleUi(candidate) &&
|
|
18
|
+
candidate !== el &&
|
|
19
|
+
!el.contains(candidate) &&
|
|
20
|
+
!candidate.contains(el);
|
|
17
21
|
const topAt = (x, y) => {
|
|
18
22
|
const found = withStack.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
|
|
19
|
-
return found.find((c) => null !== c && !
|
|
23
|
+
return found.find((c) => null !== c && !isOursButNotTheTarget(c)) ?? null;
|
|
20
24
|
};
|
|
21
25
|
const blockers = new Map();
|
|
22
26
|
for (const [fx, fy] of SAMPLE_POINTS) {
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
|
|
2
2
|
export declare const ATTR_VALUE_MAX = 512;
|
|
3
3
|
export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number): MatchResult;
|
|
4
|
+
export declare function elementsMatching(query: ElementQuery): HTMLElement[];
|
|
4
5
|
export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
|
package/dist/dom/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
-
import { isElement, isFrame,
|
|
1
|
+
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR, DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
+
import { isActionTarget, isElement, isFrame, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
4
|
import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisible, } from './a11y.js';
|
|
5
5
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
@@ -7,11 +7,11 @@ import { isSensitiveKey } from '../security/serialization.js';
|
|
|
7
7
|
import { declaredTestids } from '../registry/capabilities.js';
|
|
8
8
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
9
9
|
import { refs } from './addressing/refs.js';
|
|
10
|
-
|
|
10
|
+
import { getTestIdAttr, readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
11
11
|
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
12
12
|
const MAX_PRESENT_TESTIDS = 12;
|
|
13
13
|
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
14
|
-
const
|
|
14
|
+
const componentCandidateSelector = () => `[${SOURCE_ATTR}], ${testIdSelector()}, button, a, input, select, textarea, [role]`;
|
|
15
15
|
function elementsUnder(container) {
|
|
16
16
|
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
17
17
|
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
@@ -71,11 +71,11 @@ function resolveContainer(scope) {
|
|
|
71
71
|
if (scope === undefined)
|
|
72
72
|
return { container: document.body, scopeMissing: false };
|
|
73
73
|
const byRef = refs.resolve(scope);
|
|
74
|
-
if (
|
|
74
|
+
if (isActionTarget(byRef))
|
|
75
75
|
return { container: byRef, scopeMissing: false };
|
|
76
76
|
try {
|
|
77
77
|
const found = document.querySelector(scope);
|
|
78
|
-
if (
|
|
78
|
+
if (isActionTarget(found))
|
|
79
79
|
return { container: found, scopeMissing: false };
|
|
80
80
|
}
|
|
81
81
|
catch {
|
|
@@ -94,7 +94,7 @@ function findBySource(container, source) {
|
|
|
94
94
|
function findByComponentName(container, component) {
|
|
95
95
|
const out = [];
|
|
96
96
|
let scanned = 0;
|
|
97
|
-
for (const el of Array.from(container.querySelectorAll(
|
|
97
|
+
for (const el of Array.from(container.querySelectorAll(componentCandidateSelector()))) {
|
|
98
98
|
if (scanned >= MAX_COMPONENT_CANDIDATES)
|
|
99
99
|
break;
|
|
100
100
|
scanned += 1;
|
|
@@ -115,8 +115,16 @@ function findByComponent(container, query) {
|
|
|
115
115
|
}
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
|
+
function matchesRole(actual, queried) {
|
|
119
|
+
if (actual === queried)
|
|
120
|
+
return true;
|
|
121
|
+
if ('textbox' === queried && 'searchbox' === actual)
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
118
125
|
function queryByRoleAndName(container, role, name) {
|
|
119
|
-
return elementsUnder(container).filter((el) => getRole(el)
|
|
126
|
+
return elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
|
|
127
|
+
(name === undefined || exactVisibleText(getAccessibleName(el), name)));
|
|
120
128
|
}
|
|
121
129
|
function queryByText(container, value) {
|
|
122
130
|
return elementsUnder(container).filter((el) => fuzzyVisibleText(directText(el), value));
|
|
@@ -131,7 +139,7 @@ function queryByPlaceholder(container, value) {
|
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
function queryByTestId(container, value) {
|
|
134
|
-
return elementsUnder(container).filter((el) => el
|
|
142
|
+
return elementsUnder(container).filter((el) => readTestId(el) === value);
|
|
135
143
|
}
|
|
136
144
|
function queryByAlt(container, value) {
|
|
137
145
|
return elementsUnder(container).filter((el) => {
|
|
@@ -269,7 +277,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
|
269
277
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
270
278
|
const attrs = query.attrs;
|
|
271
279
|
const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
|
|
272
|
-
const stampViewport = described.length > 1;
|
|
280
|
+
const stampViewport = described.length > 1 || ElementState.IN_VIEWPORT === state;
|
|
273
281
|
const descriptors = described.map((el) => {
|
|
274
282
|
let base = describe(el, visMemo);
|
|
275
283
|
if (stampViewport &&
|
|
@@ -329,7 +337,7 @@ function buildPresentRegions(query) {
|
|
|
329
337
|
continue;
|
|
330
338
|
const name = el.getAttribute('aria-label') ??
|
|
331
339
|
resolveLabelledBy(el) ??
|
|
332
|
-
el
|
|
340
|
+
readTestId(el) ??
|
|
333
341
|
undefined;
|
|
334
342
|
const children = el.querySelectorAll('[role]');
|
|
335
343
|
const sample = [];
|
|
@@ -338,7 +346,7 @@ function buildPresentRegions(query) {
|
|
|
338
346
|
break;
|
|
339
347
|
const childRole = child.getAttribute('role');
|
|
340
348
|
const childName = child.getAttribute('aria-label') ??
|
|
341
|
-
child
|
|
349
|
+
readTestId(child) ??
|
|
342
350
|
child.textContent?.trim().slice(0, 40) ??
|
|
343
351
|
'';
|
|
344
352
|
if (childRole !== null && childName.length > 0) {
|
|
@@ -381,7 +389,7 @@ function wantedTextOf(query) {
|
|
|
381
389
|
}
|
|
382
390
|
const MAX_NAME_NEAR_MISSES = 5;
|
|
383
391
|
function nameNearMisses(container, query) {
|
|
384
|
-
const role = QueryBy.ROLE === query.by ? query.value :
|
|
392
|
+
const role = QueryBy.ROLE === query.by ? query.value : query.role;
|
|
385
393
|
const wanted = query.name;
|
|
386
394
|
if (role === undefined || wanted === undefined || 0 === wanted.length)
|
|
387
395
|
return [];
|
|
@@ -405,15 +413,28 @@ function nameNearMisses(container, query) {
|
|
|
405
413
|
}
|
|
406
414
|
return out;
|
|
407
415
|
}
|
|
416
|
+
function testidFoundUnder(container, query) {
|
|
417
|
+
if (QueryBy.TESTID !== query.by || undefined === query.value)
|
|
418
|
+
return undefined;
|
|
419
|
+
const configured = getTestIdAttr();
|
|
420
|
+
for (const attr of [DEFAULT_TESTID_ATTR, ...ALTERNATIVE_TESTID_ATTRS]) {
|
|
421
|
+
if (attr === configured)
|
|
422
|
+
continue;
|
|
423
|
+
const found = elementsUnder(container).some((el) => el.getAttribute(attr) === query.value && !isIgnored(el));
|
|
424
|
+
if (found)
|
|
425
|
+
return attr;
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
408
429
|
function buildEmptyHint(query) {
|
|
409
430
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
410
|
-
const all = container.querySelectorAll(
|
|
431
|
+
const all = container.querySelectorAll(testIdSelector());
|
|
411
432
|
const present = [];
|
|
412
433
|
const seen = new Set();
|
|
413
434
|
for (const el of Array.from(all)) {
|
|
414
435
|
if (isIgnored(el))
|
|
415
436
|
continue;
|
|
416
|
-
const id = el
|
|
437
|
+
const id = readTestId(el);
|
|
417
438
|
if (null === id || 0 === id.length || seen.has(id))
|
|
418
439
|
continue;
|
|
419
440
|
seen.add(id);
|
|
@@ -439,8 +460,14 @@ function buildEmptyHint(query) {
|
|
|
439
460
|
const near = nameNearMisses(container, query);
|
|
440
461
|
if (near.length > 0)
|
|
441
462
|
hint.nameNearMiss = near;
|
|
463
|
+
const elsewhere = testidFoundUnder(container, query);
|
|
464
|
+
if (elsewhere !== undefined)
|
|
465
|
+
hint.testidFoundUnder = elsewhere;
|
|
442
466
|
return hint;
|
|
443
467
|
}
|
|
468
|
+
export function elementsMatching(query) {
|
|
469
|
+
return findCandidates(query).candidates;
|
|
470
|
+
}
|
|
444
471
|
export function runQuery(query, limit) {
|
|
445
472
|
const result = matchQuery(query, undefined, limit);
|
|
446
473
|
const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { EventType, NetInitiator } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
const NON_NAVIGATING = /^(javascript|mailto|tel|sms|blob|data):/i;
|
|
4
|
+
function navigationOf() {
|
|
5
|
+
if ('undefined' === typeof window)
|
|
6
|
+
return undefined;
|
|
7
|
+
const nav = window.navigation;
|
|
8
|
+
return nav instanceof EventTarget ? nav : undefined;
|
|
9
|
+
}
|
|
4
10
|
function anchorFor(target) {
|
|
5
11
|
if (!(target instanceof Element))
|
|
6
12
|
return undefined;
|
|
7
13
|
const anchor = target.closest('a');
|
|
8
14
|
return anchor instanceof HTMLAnchorElement ? anchor : undefined;
|
|
9
15
|
}
|
|
16
|
+
const DUPLICATE_WINDOW_MS = 1000;
|
|
10
17
|
export function isDeparture(href, downloadAttr, here) {
|
|
11
18
|
if (0 === href.length || NON_NAVIGATING.test(href))
|
|
12
19
|
return false;
|
|
@@ -28,6 +35,24 @@ export function installDeparture(emit) {
|
|
|
28
35
|
if ('undefined' === typeof document)
|
|
29
36
|
return () => undefined;
|
|
30
37
|
let seq = 0;
|
|
38
|
+
let lastRecorded;
|
|
39
|
+
const record = (url, download) => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (lastRecorded !== undefined &&
|
|
42
|
+
lastRecorded.url === url &&
|
|
43
|
+
now - lastRecorded.at < DUPLICATE_WINDOW_MS) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
lastRecorded = { url, at: now };
|
|
47
|
+
seq += 1;
|
|
48
|
+
emit(EventType.NET_PENDING, {
|
|
49
|
+
id: `nav-${String(seq)}`,
|
|
50
|
+
method: 'GET',
|
|
51
|
+
url,
|
|
52
|
+
initiator: NetInitiator.NAVIGATION,
|
|
53
|
+
...(download ? { download: true } : {}),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
31
56
|
const onClick = (event) => {
|
|
32
57
|
observeSafely(() => {
|
|
33
58
|
const anchor = anchorFor(event.target);
|
|
@@ -43,18 +68,35 @@ export function installDeparture(emit) {
|
|
|
43
68
|
observeSafely(() => {
|
|
44
69
|
if (event.defaultPrevented)
|
|
45
70
|
return;
|
|
46
|
-
|
|
47
|
-
emit(EventType.NET_PENDING, {
|
|
48
|
-
id: `nav-${String(seq)}`,
|
|
49
|
-
method: 'GET',
|
|
50
|
-
url,
|
|
51
|
-
initiator: NetInitiator.NAVIGATION,
|
|
52
|
-
...(download ? { download: true } : {}),
|
|
53
|
-
});
|
|
71
|
+
record(url, download);
|
|
54
72
|
});
|
|
55
73
|
});
|
|
56
74
|
});
|
|
57
75
|
};
|
|
76
|
+
const onNavigate = (event) => {
|
|
77
|
+
observeSafely(() => {
|
|
78
|
+
const destination = event.destination;
|
|
79
|
+
if (destination === undefined || destination.sameDocument)
|
|
80
|
+
return;
|
|
81
|
+
const url = destination.url;
|
|
82
|
+
if (!isDeparture(url, false, document.location.href))
|
|
83
|
+
return;
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
observeSafely(() => {
|
|
86
|
+
if (event.defaultPrevented)
|
|
87
|
+
return;
|
|
88
|
+
record(url, false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const navigation = navigationOf();
|
|
94
|
+
if (navigation !== undefined)
|
|
95
|
+
navigation.addEventListener('navigate', onNavigate);
|
|
58
96
|
document.addEventListener('click', onClick, true);
|
|
59
|
-
return () =>
|
|
97
|
+
return () => {
|
|
98
|
+
document.removeEventListener('click', onClick, true);
|
|
99
|
+
if (navigation !== undefined)
|
|
100
|
+
navigation.removeEventListener('navigate', onNavigate);
|
|
101
|
+
};
|
|
60
102
|
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
1
2
|
import { EventType, TruncationChannel } from '@reticlehq/core';
|
|
2
3
|
import { getAccessibleName, getRole, isVisible } from '../dom/a11y.js';
|
|
3
4
|
import { refs } from '../dom/addressing/refs.js';
|
|
@@ -5,8 +6,8 @@ function regionKeyOf(target) {
|
|
|
5
6
|
const el = isElement(target) ? target : null;
|
|
6
7
|
if (null === el)
|
|
7
8
|
return undefined;
|
|
8
|
-
const labelled = el.closest(
|
|
9
|
-
return labelled
|
|
9
|
+
const labelled = el.closest(testIdSelector());
|
|
10
|
+
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
10
11
|
}
|
|
11
12
|
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
12
13
|
import { capturedRoots, installShadowRegistry, onShadowRoot } from '../dom/shadow-registry.js';
|
package/dist/observers/field.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
+
import { DEFAULT_TESTID_ATTR, EventType, isSensitiveKey } from '@reticlehq/core';
|
|
2
3
|
import { getAccessibleName } from '../dom/a11y.js';
|
|
3
4
|
const MAX_VALUE_LEN = 160;
|
|
4
5
|
const BURST_MS = 150;
|
|
@@ -9,7 +10,7 @@ function isField(target) {
|
|
|
9
10
|
target instanceof HTMLSelectElement);
|
|
10
11
|
}
|
|
11
12
|
function fieldName(element) {
|
|
12
|
-
const testid = element
|
|
13
|
+
const testid = readTestId(element);
|
|
13
14
|
if (null !== testid && testid.length > 0)
|
|
14
15
|
return testid;
|
|
15
16
|
if (element.name.length > 0)
|
|
@@ -28,7 +29,8 @@ function isSecret(element) {
|
|
|
28
29
|
return true;
|
|
29
30
|
for (const key of [
|
|
30
31
|
element.name,
|
|
31
|
-
element
|
|
32
|
+
readTestId(element),
|
|
33
|
+
element.getAttribute(DEFAULT_TESTID_ATTR),
|
|
32
34
|
element.getAttribute('id'),
|
|
33
35
|
]) {
|
|
34
36
|
if (null !== key && key.length > 0 && isSensitiveKey(key))
|
package/dist/observers/health.js
CHANGED
|
@@ -67,25 +67,86 @@ function detectBrand() {
|
|
|
67
67
|
return BrowserBrand.SAFARI;
|
|
68
68
|
return BrowserBrand.OTHER;
|
|
69
69
|
}
|
|
70
|
-
function
|
|
70
|
+
const pageFetch = 'undefined' !== typeof window && 'function' === typeof window.fetch
|
|
71
|
+
? window.fetch.bind(window)
|
|
72
|
+
: undefined;
|
|
73
|
+
function tauriWindowLabel() {
|
|
74
|
+
const internals = window['__TAURI_INTERNALS__'];
|
|
75
|
+
const label = internals?.metadata?.currentWindow?.label;
|
|
76
|
+
return 'string' === typeof label && label.length > 0 ? label : undefined;
|
|
77
|
+
}
|
|
78
|
+
function navigationStatus() {
|
|
79
|
+
try {
|
|
80
|
+
const [entry] = performance.getEntriesByType('navigation');
|
|
81
|
+
const known = entry?.responseStatus;
|
|
82
|
+
return 'number' === typeof known && known > 0 ? known : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function documentStatus(runtime) {
|
|
89
|
+
const known = navigationStatus();
|
|
90
|
+
if (known !== undefined)
|
|
91
|
+
return known;
|
|
92
|
+
try {
|
|
93
|
+
if ('web' === runtime || pageFetch === undefined || !/^https?:$/.test(location.protocol)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return (await pageFetch(location.href, { method: 'HEAD', cache: 'no-store' })).status;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function electronWindowLabel() {
|
|
103
|
+
const channel = window[RETICLE_IPC_GLOBAL];
|
|
104
|
+
if ('function' !== typeof channel?.windowLabel)
|
|
105
|
+
return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const label = await channel.windowLabel();
|
|
108
|
+
return 'string' === typeof label && label.length > 0 ? label : undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function snapshotHealth(windowLabel) {
|
|
71
115
|
return {
|
|
72
116
|
hidden: 'hidden' === document.visibilityState,
|
|
73
117
|
focused: document.hasFocus(),
|
|
74
118
|
runtime: detectRuntime(),
|
|
75
119
|
engine: detectEngine(),
|
|
76
120
|
brand: detectBrand(),
|
|
121
|
+
...(windowLabel === undefined ? {} : { windowLabel }),
|
|
77
122
|
};
|
|
78
123
|
}
|
|
79
124
|
export function installHealth(emit) {
|
|
80
125
|
const ac = new AbortController();
|
|
81
126
|
const { signal } = ac;
|
|
127
|
+
let status;
|
|
128
|
+
let label = tauriWindowLabel();
|
|
82
129
|
const report = (reason) => {
|
|
83
|
-
emit(EventType.PAGE_HEALTH, {
|
|
130
|
+
emit(EventType.PAGE_HEALTH, {
|
|
131
|
+
...snapshotHealth(label),
|
|
132
|
+
...(status === undefined ? {} : { documentStatus: status }),
|
|
133
|
+
reason,
|
|
134
|
+
});
|
|
84
135
|
};
|
|
85
136
|
document.addEventListener('visibilitychange', () => report(HealthReason.VISIBILITY), { signal });
|
|
86
137
|
window.addEventListener('focus', () => report(HealthReason.FOCUS), { signal });
|
|
87
138
|
window.addEventListener('blur', () => report(HealthReason.BLUR), { signal });
|
|
88
139
|
report(HealthReason.INITIAL);
|
|
140
|
+
void Promise.all([
|
|
141
|
+
documentStatus(detectRuntime()),
|
|
142
|
+
label === undefined ? electronWindowLabel() : Promise.resolve(label),
|
|
143
|
+
]).then(([known, named]) => {
|
|
144
|
+
if (signal.aborted || (known === undefined && named === label))
|
|
145
|
+
return;
|
|
146
|
+
status = known;
|
|
147
|
+
label = named;
|
|
148
|
+
report(HealthReason.HEARTBEAT);
|
|
149
|
+
});
|
|
89
150
|
const stopHeartbeat = nativeSetInterval(() => report(HealthReason.HEARTBEAT), SESSION_HEALTH.HEARTBEAT_MS);
|
|
90
151
|
return () => {
|
|
91
152
|
stopHeartbeat();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Emit } from './types.js';
|
|
2
|
-
export declare function watchStreamedBody(emit: Emit, res: Response, id: string,
|
|
2
|
+
export declare function watchStreamedBody(emit: Emit, res: Response, id: string, urlFields: Readonly<Record<string, unknown>>, contentType: string | null, contentLength: string | null): void;
|
|
@@ -2,7 +2,7 @@ import { EventType, StreamDirection, StreamTransport } from '@reticlehq/core';
|
|
|
2
2
|
import { isStreamingBody } from './net-detail/network-body.js';
|
|
3
3
|
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native/native-timers.js';
|
|
4
4
|
const STREAM_WATCH_MS = 10_000;
|
|
5
|
-
export function watchStreamedBody(emit, res, id,
|
|
5
|
+
export function watchStreamedBody(emit, res, id, urlFields, contentType, contentLength) {
|
|
6
6
|
if (!isStreamingBody(contentType, contentLength))
|
|
7
7
|
return;
|
|
8
8
|
if (typeof res.body?.getReader !== 'function')
|
|
@@ -20,7 +20,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
|
|
|
20
20
|
emit(EventType.NET_STREAM, {
|
|
21
21
|
transport: StreamTransport.FETCH,
|
|
22
22
|
direction: StreamDirection.OPEN,
|
|
23
|
-
|
|
23
|
+
...urlFields,
|
|
24
24
|
id,
|
|
25
25
|
});
|
|
26
26
|
void (async () => {
|
|
@@ -51,7 +51,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
|
|
|
51
51
|
emit(EventType.NET_STREAM, {
|
|
52
52
|
transport: StreamTransport.FETCH,
|
|
53
53
|
direction: StreamDirection.CLOSE,
|
|
54
|
-
|
|
54
|
+
...urlFields,
|
|
55
55
|
id,
|
|
56
56
|
...(gaveUp ? { gaveUp: true } : {}),
|
|
57
57
|
});
|
|
@@ -220,7 +220,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
220
220
|
const headersAt = performance.now();
|
|
221
221
|
const contentType = observeValue(() => res.headers.get('content-type')) ?? null;
|
|
222
222
|
observeSafely(() => {
|
|
223
|
-
watchStreamedBody(emit, res, id,
|
|
223
|
+
watchStreamedBody(emit, res, id, urlFields, contentType, res.headers.get('content-length'));
|
|
224
224
|
});
|
|
225
225
|
reportedNetUrls.add(rawUrl);
|
|
226
226
|
const emitRequest = (responseBodyFields) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventType, REDACTED_VALUE, StorageArea } from '@reticlehq/core';
|
|
2
2
|
import { isSensitiveKey, scrubKnownSecrets } from '../security/serialization.js';
|
|
3
|
+
import { isReticleStorageKey } from '../storage-keys.js';
|
|
3
4
|
import { observeSafely, observeValue } from './types.js';
|
|
4
5
|
function safeArea(get) {
|
|
5
6
|
try {
|
|
@@ -95,6 +96,8 @@ export function installStorage(emit) {
|
|
|
95
96
|
origSet.call(this, key, value);
|
|
96
97
|
if (old === value)
|
|
97
98
|
return;
|
|
99
|
+
if (isReticleStorageKey(key))
|
|
100
|
+
return;
|
|
98
101
|
observeSafely(() => {
|
|
99
102
|
emit(EventType.STORAGE_CHANGE, {
|
|
100
103
|
area: areaOf(this),
|
|
@@ -108,6 +111,8 @@ export function installStorage(emit) {
|
|
|
108
111
|
const patchedRemoveItem = function (key) {
|
|
109
112
|
const old = observeValue(() => readOld(this, key)) ?? null;
|
|
110
113
|
origRemove.call(this, key);
|
|
114
|
+
if (isReticleStorageKey(key))
|
|
115
|
+
return;
|
|
111
116
|
observeSafely(() => {
|
|
112
117
|
emit(EventType.STORAGE_CHANGE, {
|
|
113
118
|
area: areaOf(this),
|
|
@@ -123,7 +128,7 @@ export function installStorage(emit) {
|
|
|
123
128
|
try {
|
|
124
129
|
for (let i = 0; i < this.length; i += 1) {
|
|
125
130
|
const key = this.key(i);
|
|
126
|
-
if (key !== null)
|
|
131
|
+
if (key !== null && !isReticleStorageKey(key))
|
|
127
132
|
removed.push({ key, old: readOld(this, key) });
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -2,7 +2,10 @@ export declare const CAROUSEL_ATTR = "data-reticle-carousel";
|
|
|
2
2
|
export declare const CAROUSEL_SLIDE_ATTR = "data-reticle-carousel-slide";
|
|
3
3
|
export declare const CAROUSEL_DOT_ATTR = "data-reticle-carousel-dot";
|
|
4
4
|
export declare const CAROUSEL_CLOSE_ATTR = "data-reticle-carousel-close";
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const CAROUSEL_PREV_ATTR = "data-reticle-carousel-prev";
|
|
6
|
+
export declare const CAROUSEL_NEXT_ATTR = "data-reticle-carousel-next";
|
|
7
|
+
export declare const CAROUSEL_POSITION_ATTR = "data-reticle-carousel-position";
|
|
8
|
+
export declare const CAROUSEL_DISMISSED_KEY: "reticle.carousel.dismissed";
|
|
6
9
|
export declare const ROTATE_MS = 6000;
|
|
7
10
|
export interface Slide {
|
|
8
11
|
id: string;
|
|
@@ -10,4 +13,4 @@ export interface Slide {
|
|
|
10
13
|
}
|
|
11
14
|
export declare function carouselHtml(slides: readonly Slide[], active: number): string;
|
|
12
15
|
export declare function paintCarousel(log: HTMLElement, slides: readonly Slide[], storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
|
|
13
|
-
export declare const CAROUSEL_CSS
|
|
16
|
+
export declare const CAROUSEL_CSS: string;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { ReticleStorageKey } from '../../storage-keys.js';
|
|
1
2
|
export const CAROUSEL_ATTR = 'data-reticle-carousel';
|
|
2
3
|
export const CAROUSEL_SLIDE_ATTR = 'data-reticle-carousel-slide';
|
|
3
4
|
export const CAROUSEL_DOT_ATTR = 'data-reticle-carousel-dot';
|
|
4
5
|
export const CAROUSEL_CLOSE_ATTR = 'data-reticle-carousel-close';
|
|
5
|
-
export const
|
|
6
|
+
export const CAROUSEL_PREV_ATTR = 'data-reticle-carousel-prev';
|
|
7
|
+
export const CAROUSEL_NEXT_ATTR = 'data-reticle-carousel-next';
|
|
8
|
+
export const CAROUSEL_POSITION_ATTR = 'data-reticle-carousel-position';
|
|
9
|
+
export const CAROUSEL_DISMISSED_KEY = ReticleStorageKey.CAROUSEL_DISMISSED;
|
|
6
10
|
const CLOSE_LABEL = 'Close';
|
|
7
11
|
export const ROTATE_MS = 6000;
|
|
8
12
|
const rotations = new WeakMap();
|
|
@@ -22,7 +26,11 @@ function prefersReducedMotion() {
|
|
|
22
26
|
}
|
|
23
27
|
function show(root, index) {
|
|
24
28
|
root.setAttribute('data-active', String(index));
|
|
25
|
-
root.querySelectorAll(`[${CAROUSEL_SLIDE_ATTR}]`)
|
|
29
|
+
const slides = root.querySelectorAll(`[${CAROUSEL_SLIDE_ATTR}]`);
|
|
30
|
+
const position = root.querySelector(`[${CAROUSEL_POSITION_ATTR}]`);
|
|
31
|
+
if (position !== null)
|
|
32
|
+
position.textContent = `${String(index + 1)} / ${String(slides.length)}`;
|
|
33
|
+
slides.forEach((slide, i) => {
|
|
26
34
|
slide.hidden = i !== index;
|
|
27
35
|
});
|
|
28
36
|
root.querySelectorAll(`[${CAROUSEL_DOT_ATTR}]`).forEach((dot, i) => {
|
|
@@ -37,15 +45,15 @@ export function carouselHtml(slides, active) {
|
|
|
37
45
|
const track = slides
|
|
38
46
|
.map((slide, i) => `<div ${CAROUSEL_SLIDE_ATTR}="${slide.id}" class="reticle-carousel-slide"${i === current ? '' : ' hidden'}>${slide.html}</div>`)
|
|
39
47
|
.join('');
|
|
40
|
-
const
|
|
48
|
+
const controls = 1 === slides.length
|
|
41
49
|
? ''
|
|
42
|
-
: `<div class="reticle-carousel-dots">${slides
|
|
43
|
-
.map((slide, i) => `<button type="button" ${CAROUSEL_DOT_ATTR}="${String(i)}" class="reticle-carousel-dot" aria-label="${slide.id}" aria-current="${String(i === current)}"></button>`)
|
|
44
|
-
.join('')}</div>`;
|
|
50
|
+
: `<div class="reticle-carousel-controls"><button type="button" ${CAROUSEL_PREV_ATTR} aria-label="Previous message" class="reticle-carousel-arrow">‹</button><div class="reticle-carousel-dots">${slides
|
|
51
|
+
.map((slide, i) => `<button type="button" ${CAROUSEL_DOT_ATTR}="${String(i)}" class="reticle-carousel-dot" aria-label="Show ${slide.id}" aria-current="${String(i === current)}"></button>`)
|
|
52
|
+
.join('')}</div><span ${CAROUSEL_POSITION_ATTR} aria-live="polite">${String(current + 1)} / ${String(slides.length)}</span><button type="button" ${CAROUSEL_NEXT_ATTR} aria-label="Next message" class="reticle-carousel-arrow">›</button></div>`;
|
|
45
53
|
return `<div ${CAROUSEL_ATTR} class="reticle-carousel" role="region" aria-label="Reticle" data-active="${String(current)}">
|
|
46
54
|
<button type="button" ${CAROUSEL_CLOSE_ATTR} class="reticle-carousel-close" aria-label="${CLOSE_LABEL}" title="${CLOSE_LABEL}">×</button>
|
|
47
55
|
<div class="reticle-carousel-track">${track}</div>
|
|
48
|
-
${
|
|
56
|
+
${controls}
|
|
49
57
|
</div>`;
|
|
50
58
|
}
|
|
51
59
|
function dismissed(storage) {
|
|
@@ -88,6 +96,16 @@ export function paintCarousel(log, slides, storage) {
|
|
|
88
96
|
show(root, Number(dot.getAttribute(CAROUSEL_DOT_ATTR)));
|
|
89
97
|
});
|
|
90
98
|
}
|
|
99
|
+
root.querySelector(`[${CAROUSEL_PREV_ATTR}]`)?.addEventListener('click', (event) => {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
show(root, (Number(root.getAttribute('data-active')) + slides.length - 1) % slides.length);
|
|
103
|
+
});
|
|
104
|
+
root.querySelector(`[${CAROUSEL_NEXT_ATTR}]`)?.addEventListener('click', (event) => {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
show(root, (Number(root.getAttribute('data-active')) + 1) % slides.length);
|
|
108
|
+
});
|
|
91
109
|
startRotation(log, root, slides.length);
|
|
92
110
|
}
|
|
93
111
|
function startRotation(log, root, count) {
|
|
@@ -123,4 +141,13 @@ export const CAROUSEL_CSS = `
|
|
|
123
141
|
.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}
|
|
124
142
|
.reticle-carousel-dot{width:6px;height:6px;padding:0;border:0;border-radius:999px;background:currentColor;opacity:.3;cursor:pointer;}
|
|
125
143
|
.reticle-carousel-dot[aria-current="true"]{opacity:.9;}
|
|
144
|
+
.reticle-carousel-controls{display:flex;align-items:center;gap:8px;margin-top:6px;}
|
|
145
|
+
.reticle-carousel-arrow{width:24px;height:24px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--reticle-line);border-radius:6px;background:rgba(255,255,255,.04);color:var(--reticle-fg);font:inherit;font-size:18px;line-height:1;cursor:pointer;}
|
|
146
|
+
.reticle-carousel-arrow:hover{border-color:var(--reticle-accent);background:var(--reticle-accent-soft);}
|
|
147
|
+
.reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:10px;font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
148
|
+
.reticle-carousel-slide .reticle-offer{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:3px 8px;margin:0;padding:0;border:0;background:none;}
|
|
149
|
+
.reticle-carousel-slide .reticle-offer-title{grid-column:1/-1;overflow:hidden;padding:0;font-size:11px;text-overflow:ellipsis;white-space:nowrap;}
|
|
150
|
+
.reticle-carousel-slide .reticle-offer-body{grid-column:1;grid-row:2;margin:0;overflow:hidden;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap;}
|
|
151
|
+
.reticle-carousel-slide .reticle-offer-claim{grid-column:2;grid-row:2;padding:3px 7px;font-size:10px;white-space:nowrap;}
|
|
152
|
+
.reticle-carousel-slide .reticle-offer-claimed{overflow:hidden;margin:0;text-overflow:ellipsis;white-space:nowrap;}
|
|
126
153
|
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const OFFER_ATTR = "data-reticle-offer";
|
|
2
2
|
export declare const OFFER_CLAIM_ATTR = "data-reticle-offer-claim";
|
|
3
|
-
export declare const OFFER_DISMISSED_KEY
|
|
3
|
+
export declare const OFFER_DISMISSED_KEY: "reticle.harnessOffer.dismissed";
|
|
4
4
|
export declare const OFFER_TEXT: {
|
|
5
5
|
readonly HEADLINE: "Get Harness free for 3 months";
|
|
6
6
|
readonly BODY: "Explores your app and saves every journey as a free replay.";
|
|
@@ -18,4 +18,4 @@ export interface OfferState {
|
|
|
18
18
|
drivable?: boolean | undefined;
|
|
19
19
|
}
|
|
20
20
|
export declare function offerHtml(offer: OfferState | undefined, dismissed: boolean): string;
|
|
21
|
-
export declare const OFFER_CSS
|
|
21
|
+
export declare const OFFER_CSS: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ReticleStorageKey } from '../../storage-keys.js';
|
|
1
2
|
export const OFFER_ATTR = 'data-reticle-offer';
|
|
2
3
|
export const OFFER_CLAIM_ATTR = 'data-reticle-offer-claim';
|
|
3
|
-
export const OFFER_DISMISSED_KEY =
|
|
4
|
+
export const OFFER_DISMISSED_KEY = ReticleStorageKey.HARNESS_OFFER_DISMISSED;
|
|
4
5
|
export const OFFER_TEXT = {
|
|
5
6
|
HEADLINE: 'Get Harness free for 3 months',
|
|
6
7
|
BODY: 'Explores your app and saves every journey as a free replay.',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type OfferState } from './offer-card.js';
|
|
2
2
|
import type { Slide } from './carousel.js';
|
|
3
3
|
export declare const SLIDE_ID: {
|
|
4
|
-
readonly HARNESS: "harness";
|
|
5
|
-
readonly
|
|
4
|
+
readonly HARNESS: "harness-journeys";
|
|
5
|
+
readonly REPLAY: "replay-confidence";
|
|
6
|
+
readonly OFFER: "harness-offer";
|
|
6
7
|
};
|
|
7
|
-
export declare function panelSlides(offer: OfferState | undefined, offerDeclined: boolean): Slide[];
|
|
8
|
+
export declare function panelSlides(offer: OfferState | undefined, offerDeclined: boolean, notices?: readonly unknown[], base?: string): Slide[];
|