@reticlehq/browser 3.5.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 -0
- package/dist/actions/actions.js +25 -16
- package/dist/actions/danger-context.js +1 -1
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +13 -4
- 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/query.d.ts +1 -0
- package/dist/dom/query.js +38 -11
- 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/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
1
|
+
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR, DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
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';
|
|
@@ -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('*'))];
|
|
@@ -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) => {
|
|
@@ -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[];
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
import { offerHtml } from './offer-card.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { HudNoticeSchema } from '@reticlehq/core/hud';
|
|
3
|
+
import { esc } from '../chrome/presenter-safe-html.js';
|
|
4
|
+
import { DEFAULT_PLATFORM_URL } from '@reticlehq/core';
|
|
5
|
+
const HARNESS_PATH = '/settings?group=billing';
|
|
6
|
+
export const SLIDE_ID = {
|
|
7
|
+
HARNESS: 'harness-journeys',
|
|
8
|
+
REPLAY: 'replay-confidence',
|
|
9
|
+
OFFER: 'harness-offer',
|
|
10
|
+
};
|
|
11
|
+
const valueSlides = (base) => [
|
|
12
|
+
{
|
|
13
|
+
id: SLIDE_ID.HARNESS,
|
|
14
|
+
html: `<div class="reticle-promo-copy"><span class="reticle-promo-kicker">RETICLE HARNESS</span><strong class="reticle-promo-title">Let Reticle drive your app for you</strong><span class="reticle-promo-detail">Describe a user; it drives the journey and saves it to replay.</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">Explore →</a></div>`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: SLIDE_ID.REPLAY,
|
|
18
|
+
html: `<div class="reticle-promo-copy"><span class="reticle-promo-kicker">FROM RUN TO REGRESSION</span><strong class="reticle-promo-title">Keep every journey replayable</strong><span class="reticle-promo-detail">Catch what changes before users do.</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">See how →</a></div>`,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
const NOTICE_SLIDE_PREFIX = 'notice-';
|
|
22
|
+
function noticeSlide(notice) {
|
|
23
|
+
const kicker = notice.kicker === undefined
|
|
24
|
+
? ''
|
|
25
|
+
: `<span class="reticle-promo-kicker">${esc(notice.kicker)}</span>`;
|
|
26
|
+
const detail = notice.detail === undefined
|
|
27
|
+
? ''
|
|
28
|
+
: `<span class="reticle-promo-detail">${esc(notice.detail)}</span>`;
|
|
29
|
+
const link = notice.cta === undefined
|
|
30
|
+
? ''
|
|
31
|
+
: `<a data-reticle-promo-link class="reticle-promo-link" href="${esc(notice.cta.url)}" target="_blank" rel="noopener noreferrer">${esc(notice.cta.label)} →</a>`;
|
|
32
|
+
return {
|
|
33
|
+
id: `${NOTICE_SLIDE_PREFIX}${notice.id}`,
|
|
34
|
+
html: `<div class="reticle-promo-copy">${kicker}<strong class="reticle-promo-title">${esc(notice.title)}</strong>${detail}${link}</div>`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function panelSlides(offer, offerDeclined, notices = [], base = DEFAULT_PLATFORM_URL) {
|
|
38
|
+
const harnessOffer = offerHtml(offer, offerDeclined);
|
|
39
|
+
const valid = notices.flatMap((n) => {
|
|
40
|
+
const parsed = HudNoticeSchema.safeParse(n);
|
|
41
|
+
return parsed.success ? [parsed.data] : [];
|
|
42
|
+
});
|
|
6
43
|
return [
|
|
7
|
-
...('' ===
|
|
8
|
-
|
|
44
|
+
...('' === harnessOffer ? [] : [{ id: SLIDE_ID.OFFER, html: harnessOffer }]),
|
|
45
|
+
...(0 < valid.length ? valid.map(noticeSlide) : valueSlides(base)),
|
|
9
46
|
];
|
|
10
47
|
}
|
|
@@ -5,4 +5,4 @@ export declare const TALK_TEXT: {
|
|
|
5
5
|
readonly BOOK: "Book a call";
|
|
6
6
|
};
|
|
7
7
|
export declare function talkCardHtml(): string;
|
|
8
|
-
export declare const TALK_CSS
|
|
8
|
+
export declare const TALK_CSS: string;
|
|
@@ -10,3 +10,8 @@ export declare const LOG_RESULT: {
|
|
|
10
10
|
readonly FAIL: "fail";
|
|
11
11
|
};
|
|
12
12
|
export type LogResult = (typeof LOG_RESULT)[keyof typeof LOG_RESULT];
|
|
13
|
+
export declare const LOG_ACTOR: {
|
|
14
|
+
readonly AGENT: "agent";
|
|
15
|
+
readonly HARNESS: "harness";
|
|
16
|
+
};
|
|
17
|
+
export type LogActor = (typeof LOG_ACTOR)[keyof typeof LOG_ACTOR];
|