@reticlehq/browser 3.1.0 → 3.2.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.d.ts +1 -0
- package/dist/actions/actions-dom.js +26 -40
- package/dist/actions/actions.js +10 -6
- package/dist/dom/query.js +22 -5
- package/dist/observers/departure.js +16 -8
- package/dist/observers/network.js +49 -8
- package/dist/presenter/presenter-offer.d.ts +25 -0
- package/dist/presenter/presenter-offer.js +87 -0
- package/dist/presenter/presenter-report-styles.js +4 -0
- package/dist/presenter/presenter-report.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +4 -0
- package/dist/presenter/presenter-settings.js +38 -1
- package/dist/presenter/presenter-shell-styles.js +12 -0
- package/dist/presenter/presenter-shell.d.ts +4 -0
- package/dist/presenter/presenter-shell.js +22 -0
- package/dist/presenter/presenter-styles.js +2 -0
- package/dist/presenter/presenter.js +3 -0
- package/dist/presenter/tour/tour-view.js +7 -3
- package/dist/presenter/tour/tour.js +21 -2
- package/dist/reticle.js +5 -1
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ interface ClickGeometry {
|
|
|
4
4
|
scrolledIntoView: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
7
|
+
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
7
8
|
export declare function fireClickSequence(el: HTMLElement, hold?: {
|
|
8
9
|
ms: number;
|
|
9
10
|
sleep: (ms: number) => Promise<void>;
|
|
@@ -7,11 +7,20 @@ export const NO_GEOMETRY = {
|
|
|
7
7
|
occludedBy: null,
|
|
8
8
|
scrolledIntoView: false,
|
|
9
9
|
};
|
|
10
|
+
export function mouseEventFor(el, type, init = {}) {
|
|
11
|
+
return new MouseEvent(type, { ...init, view: el.ownerDocument.defaultView });
|
|
12
|
+
}
|
|
13
|
+
function pointerEventFor(el, type, init) {
|
|
14
|
+
const full = { ...init, view: el.ownerDocument.defaultView };
|
|
15
|
+
return 'function' === typeof PointerEvent
|
|
16
|
+
? new PointerEvent(type, full)
|
|
17
|
+
: new MouseEvent(type, full);
|
|
18
|
+
}
|
|
10
19
|
export async function fireClickSequence(el, hold) {
|
|
11
20
|
const doc = el.ownerDocument;
|
|
12
21
|
const from = doc.activeElement ?? doc.body;
|
|
13
22
|
firePointer(el, 'pointerdown', from);
|
|
14
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
23
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', { bubbles: true, cancelable: true })));
|
|
15
24
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
16
25
|
el.focus();
|
|
17
26
|
let heldMs = 0;
|
|
@@ -21,8 +30,8 @@ export async function fireClickSequence(el, hold) {
|
|
|
21
30
|
heldMs = hold.now() - startedAt;
|
|
22
31
|
}
|
|
23
32
|
firePointer(el, 'pointerup', from);
|
|
24
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
25
|
-
const notPrevented = asSyntheticInput(() => el.dispatchEvent(
|
|
33
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', { bubbles: true, cancelable: true })));
|
|
34
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
|
|
26
35
|
return { prevented: !notPrevented, heldMs };
|
|
27
36
|
}
|
|
28
37
|
function isMeasurable(rect) {
|
|
@@ -57,24 +66,10 @@ export function clickGeometry(el) {
|
|
|
57
66
|
return { ...hitTest(el, rect), scrolledIntoView };
|
|
58
67
|
}
|
|
59
68
|
export function firePointer(el, type, relatedTarget = null) {
|
|
60
|
-
asSyntheticInput(() => {
|
|
61
|
-
if ('function' === typeof PointerEvent) {
|
|
62
|
-
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
66
|
-
}
|
|
67
|
-
});
|
|
69
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
|
|
68
70
|
}
|
|
69
71
|
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
70
|
-
asSyntheticInput(() => {
|
|
71
|
-
if ('function' === typeof PointerEvent) {
|
|
72
|
-
el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
el.dispatchEvent(new MouseEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
76
|
-
}
|
|
77
|
-
});
|
|
72
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget })));
|
|
78
73
|
}
|
|
79
74
|
function makeDataTransfer(data) {
|
|
80
75
|
if (typeof DataTransfer !== 'function')
|
|
@@ -117,12 +112,9 @@ export async function dragElement(source, target, data) {
|
|
|
117
112
|
button: 0,
|
|
118
113
|
...(related !== undefined ? { relatedTarget: related } : {}),
|
|
119
114
|
};
|
|
120
|
-
|
|
121
|
-
el
|
|
122
|
-
|
|
123
|
-
else {
|
|
124
|
-
el.dispatchEvent(new MouseEvent(type, init));
|
|
125
|
-
}
|
|
115
|
+
el.dispatchEvent(type.startsWith('pointer')
|
|
116
|
+
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
117
|
+
: mouseEventFor(el, type, init));
|
|
126
118
|
};
|
|
127
119
|
const sourceRect = source.getBoundingClientRect();
|
|
128
120
|
const destRect = target !== null ? dest.getBoundingClientRect() : null;
|
|
@@ -190,24 +182,18 @@ export async function fireTapSequence(el, hold) {
|
|
|
190
182
|
}
|
|
191
183
|
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
|
|
192
184
|
firePointerTouch(el, 'pointerup');
|
|
193
|
-
const clicked = asSyntheticInput(() => el.dispatchEvent(
|
|
185
|
+
const clicked = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
|
|
194
186
|
return { prevented: !clicked, heldMs };
|
|
195
187
|
}
|
|
196
188
|
function firePointerTouch(el, type) {
|
|
197
|
-
asSyntheticInput(() => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
height: 23,
|
|
206
|
-
}));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
|
|
210
|
-
});
|
|
189
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, {
|
|
190
|
+
bubbles: true,
|
|
191
|
+
cancelable: true,
|
|
192
|
+
pointerType: 'touch',
|
|
193
|
+
isPrimary: true,
|
|
194
|
+
width: 23,
|
|
195
|
+
height: 23,
|
|
196
|
+
})));
|
|
211
197
|
}
|
|
212
198
|
function touchListFor(el) {
|
|
213
199
|
if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
|
package/dist/actions/actions.js
CHANGED
|
@@ -124,7 +124,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
124
124
|
throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, } from './actions-dom.js';
|
|
127
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, } from './actions-dom.js';
|
|
128
128
|
import { scrollFrom } from './scroll.js';
|
|
129
129
|
function enabledOf(el) {
|
|
130
130
|
return !getStates(el).includes(ElementState.DISABLED);
|
|
@@ -167,16 +167,16 @@ function asFiniteNumber(raw) {
|
|
|
167
167
|
async function dispatchOther(el, action, args) {
|
|
168
168
|
switch (action) {
|
|
169
169
|
case ActionType.DBLCLICK:
|
|
170
|
-
return !asSyntheticInput(() => el.dispatchEvent(
|
|
170
|
+
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', { bubbles: true, cancelable: true })));
|
|
171
171
|
case ActionType.HOVER: {
|
|
172
172
|
const doc = el.ownerDocument;
|
|
173
173
|
const from = doc.activeElement ?? doc.body;
|
|
174
174
|
firePointer(el, 'pointerover', from);
|
|
175
|
-
el.dispatchEvent(
|
|
175
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
|
|
176
176
|
firePointerNonBubbling(el, 'pointerenter', from);
|
|
177
|
-
el.dispatchEvent(
|
|
177
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
|
|
178
178
|
firePointer(el, 'pointermove', from);
|
|
179
|
-
const moved = el.dispatchEvent(
|
|
179
|
+
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
180
180
|
const holdMs = clampHold(args['holdMs']);
|
|
181
181
|
if (holdMs > 0)
|
|
182
182
|
await sleep(holdMs);
|
|
@@ -252,7 +252,11 @@ async function dispatchOther(el, action, args) {
|
|
|
252
252
|
}
|
|
253
253
|
if (alreadyAtCheckedState(el, action))
|
|
254
254
|
return false;
|
|
255
|
-
const event =
|
|
255
|
+
const event = mouseEventFor(el, 'click', {
|
|
256
|
+
bubbles: true,
|
|
257
|
+
cancelable: true,
|
|
258
|
+
composed: true,
|
|
259
|
+
});
|
|
256
260
|
const notPrevented = asSyntheticInput(() => el.dispatchEvent(event));
|
|
257
261
|
return !notPrevented || event.defaultPrevented;
|
|
258
262
|
}
|
package/dist/dom/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
-
import { isFrame, isHtmlElement, isInput, isSelect, isTextArea } from './realm.js';
|
|
2
|
+
import { isElement, isFrame, isHtmlElement, 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';
|
|
@@ -27,20 +27,35 @@ function fuzzyVisibleText(actual, expected) {
|
|
|
27
27
|
function exactVisibleText(actual, expected) {
|
|
28
28
|
return normaliseVisibleText(actual) === normaliseVisibleText(expected);
|
|
29
29
|
}
|
|
30
|
+
const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
|
|
31
|
+
function isNonRendered(el) {
|
|
32
|
+
return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
|
|
33
|
+
}
|
|
30
34
|
function directText(el) {
|
|
31
35
|
if (isInput(el)) {
|
|
32
36
|
const type = el.type.toLowerCase();
|
|
33
37
|
if ('submit' === type || 'button' === type || 'reset' === type)
|
|
34
38
|
return el.value;
|
|
35
39
|
}
|
|
36
|
-
|
|
37
|
-
if ('script' === tag || 'style' === tag)
|
|
40
|
+
if (isNonRendered(el))
|
|
38
41
|
return '';
|
|
39
42
|
return Array.from(el.childNodes)
|
|
40
43
|
.filter((node) => Node.TEXT_NODE === node.nodeType)
|
|
41
44
|
.map((node) => node.textContent ?? '')
|
|
42
45
|
.join('');
|
|
43
46
|
}
|
|
47
|
+
function visibleTextOf(el, memo) {
|
|
48
|
+
if (isNonRendered(el) || isIgnored(el) || !isVisible(el, memo))
|
|
49
|
+
return '';
|
|
50
|
+
let text = '';
|
|
51
|
+
for (const node of Array.from(el.childNodes)) {
|
|
52
|
+
if (Node.TEXT_NODE === node.nodeType)
|
|
53
|
+
text += node.textContent ?? '';
|
|
54
|
+
else if (isElement(node))
|
|
55
|
+
text += visibleTextOf(node, memo);
|
|
56
|
+
}
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
44
59
|
function semanticNameTarget(el) {
|
|
45
60
|
if (isInput(el) || isTextArea(el) || isSelect(el))
|
|
46
61
|
return true;
|
|
@@ -205,7 +220,8 @@ function findCandidates(query) {
|
|
|
205
220
|
if (true === query.self) {
|
|
206
221
|
if (query.scope === undefined)
|
|
207
222
|
return { candidates: [], scopeMissing };
|
|
208
|
-
if (query.text !== undefined &&
|
|
223
|
+
if (query.text !== undefined &&
|
|
224
|
+
!fuzzyVisibleText(visibleTextOf(container, new Map()), query.text)) {
|
|
209
225
|
return { candidates: [], scopeMissing };
|
|
210
226
|
}
|
|
211
227
|
return { candidates: isIgnored(container) ? [] : [container], scopeMissing };
|
|
@@ -342,10 +358,11 @@ function buildPresentRegions(query) {
|
|
|
342
358
|
function splitTextOwner(container, wanted) {
|
|
343
359
|
let best;
|
|
344
360
|
let bestDepth = -1;
|
|
361
|
+
const memo = new Map();
|
|
345
362
|
for (const el of elementsUnder(container)) {
|
|
346
363
|
if (isIgnored(el))
|
|
347
364
|
continue;
|
|
348
|
-
if (!fuzzyVisibleText(el
|
|
365
|
+
if (!fuzzyVisibleText(visibleTextOf(el, memo), wanted))
|
|
349
366
|
continue;
|
|
350
367
|
let depth = 0;
|
|
351
368
|
for (let parent = el.parentElement; parent !== null; parent = parent.parentElement)
|
|
@@ -35,15 +35,23 @@ export function installDeparture(emit) {
|
|
|
35
35
|
return;
|
|
36
36
|
const href = anchor.getAttribute('href') ?? '';
|
|
37
37
|
const here = document.location.href;
|
|
38
|
-
|
|
38
|
+
const download = anchor.hasAttribute('download');
|
|
39
|
+
if (!isDeparture(href, download, here))
|
|
39
40
|
return;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
const url = new URL(href, here).toString();
|
|
42
|
+
queueMicrotask(() => {
|
|
43
|
+
observeSafely(() => {
|
|
44
|
+
if (event.defaultPrevented)
|
|
45
|
+
return;
|
|
46
|
+
seq += 1;
|
|
47
|
+
emit(EventType.NET_PENDING, {
|
|
48
|
+
id: `nav-${String(seq)}`,
|
|
49
|
+
method: 'GET',
|
|
50
|
+
url,
|
|
51
|
+
initiator: NetInitiator.NAVIGATION,
|
|
52
|
+
...(download ? { download: true } : {}),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
47
55
|
});
|
|
48
56
|
});
|
|
49
57
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlindSpotKind, EventType, RETICLE_WS_PATH, StreamTransport, StreamDirection, } from '@reticlehq/core';
|
|
1
|
+
import { BlindSpotKind, EventType, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { observeSafely, observeValue } from './types.js';
|
|
4
4
|
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './net-detail/network-body.js';
|
|
@@ -31,26 +31,66 @@ function frameFields(data, captureBodies) {
|
|
|
31
31
|
}
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
+
const ShapeToken = { NULL: '0', STRING: 's', NUMBER: 'n', BOOLEAN: 'b', OTHER: '?' };
|
|
35
|
+
function shapeOf(value) {
|
|
36
|
+
if (null === value)
|
|
37
|
+
return ShapeToken.NULL;
|
|
38
|
+
if (Array.isArray(value))
|
|
39
|
+
return `[${value.map(shapeOf).join(',')}]`;
|
|
40
|
+
if ('object' === typeof value) {
|
|
41
|
+
const fields = value;
|
|
42
|
+
return `{${Object.keys(fields)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((key) => `${key}:${shapeOf(fields[key])}`)
|
|
45
|
+
.join(',')}}`;
|
|
46
|
+
}
|
|
47
|
+
if ('string' === typeof value)
|
|
48
|
+
return ShapeToken.STRING;
|
|
49
|
+
if ('number' === typeof value)
|
|
50
|
+
return ShapeToken.NUMBER;
|
|
51
|
+
if ('boolean' === typeof value)
|
|
52
|
+
return ShapeToken.BOOLEAN;
|
|
53
|
+
return ShapeToken.OTHER;
|
|
54
|
+
}
|
|
55
|
+
function fingerprintBody(shape, length) {
|
|
56
|
+
return fnv1a(`${shape}#${String(length)}`);
|
|
57
|
+
}
|
|
58
|
+
function jsonShape(text) {
|
|
59
|
+
try {
|
|
60
|
+
return shapeOf(JSON.parse(text));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return ShapeToken.OTHER;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
34
66
|
function projectRequestBody(body, captureBodies) {
|
|
35
|
-
if (!captureBodies)
|
|
36
|
-
return {};
|
|
37
67
|
let text;
|
|
38
68
|
let contentType = 'application/json';
|
|
69
|
+
let shape = REQUEST_SHAPE_NONE;
|
|
39
70
|
if ('string' === typeof body) {
|
|
40
71
|
text = body;
|
|
72
|
+
if (text.length > 0)
|
|
73
|
+
shape = fingerprintBody(jsonShape(text), text.length);
|
|
41
74
|
}
|
|
42
75
|
else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
43
76
|
text = body.toString();
|
|
44
77
|
contentType = 'application/x-www-form-urlencoded';
|
|
78
|
+
if (text.length > 0) {
|
|
79
|
+
const fields = Object.fromEntries(new URLSearchParams(text));
|
|
80
|
+
shape = fingerprintBody(shapeOf(fields), text.length);
|
|
81
|
+
}
|
|
45
82
|
}
|
|
46
83
|
else if (body !== undefined && body !== null) {
|
|
47
|
-
const
|
|
48
|
-
return { requestBodyType:
|
|
84
|
+
const kind = body.constructor?.name ?? typeof body;
|
|
85
|
+
return captureBodies ? { requestBodyType: kind } : {};
|
|
49
86
|
}
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
const fields = { [REQUEST_SHAPE_FIELD]: shape };
|
|
88
|
+
if (!captureBodies || text === undefined || 0 === text.length)
|
|
89
|
+
return fields;
|
|
52
90
|
const { body: out, truncated } = projectBody(text, contentType);
|
|
53
|
-
return truncated
|
|
91
|
+
return truncated
|
|
92
|
+
? { ...fields, requestBody: out, requestBodyTruncated: true }
|
|
93
|
+
: { ...fields, requestBody: out };
|
|
54
94
|
}
|
|
55
95
|
function statusIsOk(status) {
|
|
56
96
|
return status >= 200 && status < 400;
|
|
@@ -428,6 +468,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
428
468
|
durationMs: 0,
|
|
429
469
|
initiator: 'beacon',
|
|
430
470
|
...(initiatorStack === undefined ? {} : { initiatorStack }),
|
|
471
|
+
...projectRequestBody(data, false),
|
|
431
472
|
});
|
|
432
473
|
});
|
|
433
474
|
return sent;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const OFFER_ATTR = "data-reticle-offer";
|
|
2
|
+
export declare const OFFER_SLOT_ATTR = "data-reticle-offer-slot";
|
|
3
|
+
export declare const OFFER_CLAIM_ATTR = "data-reticle-offer-claim";
|
|
4
|
+
export declare const OFFER_DISMISS_ATTR = "data-reticle-offer-dismiss";
|
|
5
|
+
export declare const OFFER_DISMISSED_KEY = "reticle.harnessOffer.dismissed";
|
|
6
|
+
export declare const OFFER_TEXT: {
|
|
7
|
+
readonly HEADLINE: "Reticle drives your app for you";
|
|
8
|
+
readonly BODY: "The harness explores your app, proves what works, and saves every journey as a test that replays for free. Built on Jev — no model API key needed.";
|
|
9
|
+
readonly FREE: "Free for 3 months";
|
|
10
|
+
readonly CLAIM: "Claim now";
|
|
11
|
+
readonly DISMISS: "Not now";
|
|
12
|
+
readonly claimed: (days: number) => string;
|
|
13
|
+
readonly ending: (days: number) => string;
|
|
14
|
+
};
|
|
15
|
+
export declare function offerDismissed(storage: Pick<Storage, 'getItem'> | undefined): boolean;
|
|
16
|
+
export declare function dismissOffer(storage: Pick<Storage, 'setItem'> | undefined): void;
|
|
17
|
+
export interface OfferState {
|
|
18
|
+
claimed: boolean;
|
|
19
|
+
daysRemaining?: number | undefined;
|
|
20
|
+
claimUrl?: string | undefined;
|
|
21
|
+
eligible?: boolean | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function offerHtml(offer: OfferState | undefined, dismissed: boolean): string;
|
|
24
|
+
export declare function paintOffer(root: ParentNode, offer: OfferState | undefined, storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
|
|
25
|
+
export declare const OFFER_CSS = "\n.reticle-offer{margin:8px 10px;padding:10px 12px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}\n.reticle-offer-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}\n.reticle-offer-title{font-weight:600;font-size:12px;}\n.reticle-offer-free{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--reticle-accent,#4f7cff);color:#fff;white-space:nowrap;}\n.reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}\n.reticle-offer-actions{display:flex;align-items:center;gap:8px;}\n.reticle-offer-claim{font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}\n.reticle-offer-dismiss{font-size:11px;padding:4px 8px;border-radius:6px;background:none;border:0;color:inherit;opacity:.6;cursor:pointer;}\n.reticle-offer-dismiss:hover{opacity:1;}\n.reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}\n";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const OFFER_ATTR = 'data-reticle-offer';
|
|
2
|
+
export const OFFER_SLOT_ATTR = 'data-reticle-offer-slot';
|
|
3
|
+
export const OFFER_CLAIM_ATTR = 'data-reticle-offer-claim';
|
|
4
|
+
export const OFFER_DISMISS_ATTR = 'data-reticle-offer-dismiss';
|
|
5
|
+
export const OFFER_DISMISSED_KEY = 'reticle.harnessOffer.dismissed';
|
|
6
|
+
export const OFFER_TEXT = {
|
|
7
|
+
HEADLINE: 'Reticle drives your app for you',
|
|
8
|
+
BODY: 'The harness explores your app, proves what works, and saves every journey as a test that replays for free. Built on Jev — no model API key needed.',
|
|
9
|
+
FREE: 'Free for 3 months',
|
|
10
|
+
CLAIM: 'Claim now',
|
|
11
|
+
DISMISS: 'Not now',
|
|
12
|
+
claimed: (days) => `Harness active — ${String(days)} days left`,
|
|
13
|
+
ending: (days) => 1 >= days
|
|
14
|
+
? 'Harness free period ends today'
|
|
15
|
+
: `Harness free period ends in ${String(days)} days`,
|
|
16
|
+
};
|
|
17
|
+
const ENDING_SOON_DAYS = 14;
|
|
18
|
+
export function offerDismissed(storage) {
|
|
19
|
+
try {
|
|
20
|
+
return '1' === storage?.getItem(OFFER_DISMISSED_KEY);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function dismissOffer(storage) {
|
|
27
|
+
try {
|
|
28
|
+
storage?.setItem(OFFER_DISMISSED_KEY, '1');
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const escape = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
34
|
+
export function offerHtml(offer, dismissed) {
|
|
35
|
+
if (offer === undefined)
|
|
36
|
+
return '';
|
|
37
|
+
if (offer.claimed) {
|
|
38
|
+
const days = offer.daysRemaining;
|
|
39
|
+
if (days === undefined || 0 > days)
|
|
40
|
+
return '';
|
|
41
|
+
const text = ENDING_SOON_DAYS >= days ? OFFER_TEXT.ending(days) : OFFER_TEXT.claimed(days);
|
|
42
|
+
return `<div ${OFFER_ATTR} class="reticle-offer reticle-offer-claimed">${escape(text)}</div>`;
|
|
43
|
+
}
|
|
44
|
+
if (false === offer.eligible)
|
|
45
|
+
return '';
|
|
46
|
+
if (dismissed)
|
|
47
|
+
return '';
|
|
48
|
+
const url = offer.claimUrl;
|
|
49
|
+
if (url === undefined || 0 === url.length)
|
|
50
|
+
return '';
|
|
51
|
+
return `<div ${OFFER_ATTR} class="reticle-offer" role="note">
|
|
52
|
+
<div class="reticle-offer-head">
|
|
53
|
+
<span class="reticle-offer-title">${escape(OFFER_TEXT.HEADLINE)}</span>
|
|
54
|
+
<span class="reticle-offer-free">${escape(OFFER_TEXT.FREE)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<p class="reticle-offer-body">${escape(OFFER_TEXT.BODY)}</p>
|
|
57
|
+
<div class="reticle-offer-actions">
|
|
58
|
+
<a ${OFFER_CLAIM_ATTR} class="reticle-offer-claim" href="${escape(url)}" target="_blank" rel="noopener noreferrer">${escape(OFFER_TEXT.CLAIM)}</a>
|
|
59
|
+
<button type="button" ${OFFER_DISMISS_ATTR} class="reticle-offer-dismiss">${escape(OFFER_TEXT.DISMISS)}</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>`;
|
|
62
|
+
}
|
|
63
|
+
export function paintOffer(root, offer, storage) {
|
|
64
|
+
const slot = root.querySelector(`[${OFFER_SLOT_ATTR}]`);
|
|
65
|
+
if (!(slot instanceof HTMLElement))
|
|
66
|
+
return;
|
|
67
|
+
slot.innerHTML = offerHtml(offer, offerDismissed(storage));
|
|
68
|
+
const dismiss = slot.querySelector(`[${OFFER_DISMISS_ATTR}]`);
|
|
69
|
+
dismiss?.addEventListener('click', (event) => {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
event.stopPropagation();
|
|
72
|
+
dismissOffer(storage);
|
|
73
|
+
slot.innerHTML = '';
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export const OFFER_CSS = `
|
|
77
|
+
.reticle-offer{margin:8px 10px;padding:10px 12px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
|
|
78
|
+
.reticle-offer-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
79
|
+
.reticle-offer-title{font-weight:600;font-size:12px;}
|
|
80
|
+
.reticle-offer-free{font-size:10px;padding:2px 6px;border-radius:999px;background:var(--reticle-accent,#4f7cff);color:#fff;white-space:nowrap;}
|
|
81
|
+
.reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}
|
|
82
|
+
.reticle-offer-actions{display:flex;align-items:center;gap:8px;}
|
|
83
|
+
.reticle-offer-claim{font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}
|
|
84
|
+
.reticle-offer-dismiss{font-size:11px;padding:4px 8px;border-radius:6px;background:none;border:0;color:inherit;opacity:.6;cursor:pointer;}
|
|
85
|
+
.reticle-offer-dismiss:hover{opacity:1;}
|
|
86
|
+
.reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}
|
|
87
|
+
`;
|
|
@@ -68,6 +68,10 @@ export const REPORT_CSS = `
|
|
|
68
68
|
display:block;margin-top:2px;color:var(--reticle-c-active);opacity:.8;
|
|
69
69
|
font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;cursor:help;}
|
|
70
70
|
[${REPORT_PANEL_ATTR}] .reticle-report-defects-wrap{margin-top:14px;}
|
|
71
|
+
/* The heading and its push control on one line, so the button reads as belonging to this section
|
|
72
|
+
rather than floating above the list. */
|
|
73
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defects-head{
|
|
74
|
+
display:flex;align-items:center;justify-content:space-between;gap:8px;}
|
|
71
75
|
[${REPORT_PANEL_ATTR}] .reticle-report-defects{
|
|
72
76
|
display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;}
|
|
73
77
|
/* A left rule rather than a card per row: ten cards in a 320px panel is a wall, and the rule keeps
|
|
@@ -79,7 +79,8 @@ function defects(scope, dashboardUrl) {
|
|
|
79
79
|
const more = !linked
|
|
80
80
|
? ''
|
|
81
81
|
: `<a class="reticle-report-defects-more" href="${esc(dashboardUrl)}" target="_blank" rel="noreferrer noopener">${REPORT_TEXT.DEFECTS_MORE}${scope.counts.failed > list.length ? ` (${String(scope.counts.failed)})` : ''}</a>`;
|
|
82
|
-
|
|
82
|
+
const sync = syncButtonHtml(dashboardUrl);
|
|
83
|
+
return `<div class="reticle-report-defects-wrap"><div class="reticle-report-defects-head"><span class="reticle-report-section">${REPORT_TEXT.DEFECTS}</span>${sync}</div><ul class="reticle-report-defects">${rows}</ul>${more}</div>`;
|
|
83
84
|
}
|
|
84
85
|
export function reportBodyHtml(scope, dashboardUrl, account, projectName) {
|
|
85
86
|
const c = scope.counts;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HarnessConfig } from '@reticlehq/core';
|
|
1
2
|
import { SETTINGS_CSS } from './presenter-settings-styles.js';
|
|
2
3
|
import type { AccountState } from '@reticlehq/core';
|
|
3
4
|
import { type AccountDetails } from './presenter-account.js';
|
|
@@ -41,10 +42,12 @@ export interface PresenterSettings {
|
|
|
41
42
|
export declare function getPresenterSettings(): PresenterSettings;
|
|
42
43
|
export declare function loadPresenterSettings(): PresenterSettings;
|
|
43
44
|
export declare const SETTINGS_ACCOUNT_ATTR = "data-reticle-settings-account";
|
|
45
|
+
export declare function paintHarnessRow(root: ParentNode, config: HarnessConfig | undefined): void;
|
|
44
46
|
export declare function paintSettingsAccount(root: HTMLElement, account: AccountState | undefined, dashboardUrl: string | undefined, details?: AccountDetails): void;
|
|
45
47
|
export declare function settingsPanelHtml(): string;
|
|
46
48
|
export interface SettingsHost {
|
|
47
49
|
onHideUntilRestart?: () => void;
|
|
50
|
+
onHarness?: (enabled: boolean) => void;
|
|
48
51
|
onSettingsChange?: (settings: PresenterSettings) => void;
|
|
49
52
|
onBeforeOpen?: () => void;
|
|
50
53
|
}
|
|
@@ -56,6 +59,7 @@ export declare class PresenterSettingsPanel {
|
|
|
56
59
|
constructor(host?: SettingsHost);
|
|
57
60
|
contains(node: Node): boolean;
|
|
58
61
|
mount(root: HTMLElement): void;
|
|
62
|
+
paintHarness(config: HarnessConfig | undefined): void;
|
|
59
63
|
teardown(): void;
|
|
60
64
|
isOpen(): boolean;
|
|
61
65
|
open(): void;
|
|
@@ -149,7 +149,29 @@ function settingsCheckRow(key, label, checked) {
|
|
|
149
149
|
}
|
|
150
150
|
export const SETTINGS_ACCOUNT_ATTR = 'data-reticle-settings-account';
|
|
151
151
|
const SETTINGS_ACCOUNT_ROW_ATTR = 'data-reticle-settings-account-row';
|
|
152
|
+
const SETTINGS_HARNESS_ROW_ATTR = 'data-reticle-settings-harness-row';
|
|
153
|
+
const HARNESS_HELP = 'Let Reticle drive this app by itself to find defects. Set here or in your dashboard — both write to the same place.';
|
|
154
|
+
const HARNESS_LOCKED_HELP = 'Autonomous driving runs on Reticle’s model spend, so it needs a plan or the free period. Claim it from the chat panel.';
|
|
152
155
|
const ACCOUNT_HELP = 'Whether this machine is signed in to a Reticle workspace. Signing in happens in your terminal.';
|
|
156
|
+
export function paintHarnessRow(root, config) {
|
|
157
|
+
const row = root.querySelector(`[${SETTINGS_HARNESS_ROW_ATTR}]`);
|
|
158
|
+
if (!(row instanceof HTMLElement))
|
|
159
|
+
return;
|
|
160
|
+
if (config === undefined) {
|
|
161
|
+
row.hidden = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
row.hidden = false;
|
|
165
|
+
const toggle = row.querySelector(`[${SETTING_KEY_ATTR}="harnessEnabled"]`);
|
|
166
|
+
if (!(toggle instanceof HTMLElement))
|
|
167
|
+
return;
|
|
168
|
+
const usable = config.harnessEntitled;
|
|
169
|
+
toggle.setAttribute('aria-checked', config.harnessEnabled && usable ? 'true' : 'false');
|
|
170
|
+
toggle.setAttribute('aria-disabled', usable ? 'false' : 'true');
|
|
171
|
+
const help = row.querySelector('[data-reticle-help]');
|
|
172
|
+
if (help instanceof HTMLElement)
|
|
173
|
+
help.title = usable ? HARNESS_HELP : HARNESS_LOCKED_HELP;
|
|
174
|
+
}
|
|
153
175
|
export function paintSettingsAccount(root, account, dashboardUrl, details = {}) {
|
|
154
176
|
const row = root.querySelector(`[${SETTINGS_ACCOUNT_ROW_ATTR}]`);
|
|
155
177
|
const slot = root.querySelector(`[${SETTINGS_ACCOUNT_ATTR}]`);
|
|
@@ -165,6 +187,7 @@ export function settingsPanelHtml() {
|
|
|
165
187
|
const outputHelp = 'How much detail is included when you copy or export the run';
|
|
166
188
|
const reactHelp = 'Include React component paths in exported run state when available';
|
|
167
189
|
const hideHelp = 'Hide the Reticle HUD until you reload the page';
|
|
190
|
+
const harnessHelp = HARNESS_HELP;
|
|
168
191
|
const tallyHelp = 'Show the pass/fail score pill in the toolbar';
|
|
169
192
|
const timestampsHelp = 'Show relative timestamps on each activity-log row';
|
|
170
193
|
const autoChatHelp = 'On by default. Opens the agent chat by itself when a session starts and when you expand the HUD. Off leaves the toolbar bare until you ask for the chat.';
|
|
@@ -191,6 +214,7 @@ export function settingsPanelHtml() {
|
|
|
191
214
|
${settingsCheckRow('blockPageInteractions', 'Block page interactions', true)}
|
|
192
215
|
${settingsCheckRow('clearOnCopy', 'Clear on copy/send', false)}
|
|
193
216
|
${settingsToggleRow('hideUntilRestart', 'Hide Until Restart', hideHelp)}
|
|
217
|
+
${settingsToggleRow('harnessEnabled', 'Autonomous driving', harnessHelp, `${SETTINGS_HARNESS_ROW_ATTR} hidden`)}
|
|
194
218
|
${settingsToggleRow('reduceMotion', 'Reduce motion', motionHelp)}
|
|
195
219
|
<div class="reticle-settings-section">Account</div>
|
|
196
220
|
<div class="reticle-settings-row" ${SETTINGS_ACCOUNT_ROW_ATTR} hidden>
|
|
@@ -286,7 +310,14 @@ export class PresenterSettingsPanel {
|
|
|
286
310
|
if (!(toggle instanceof HTMLElement))
|
|
287
311
|
return;
|
|
288
312
|
const key = toggle.getAttribute(SETTING_KEY_ATTR);
|
|
289
|
-
if ('
|
|
313
|
+
if ('harnessEnabled' === key) {
|
|
314
|
+
if ('true' === toggle.getAttribute('aria-disabled'))
|
|
315
|
+
return;
|
|
316
|
+
const next = 'true' !== toggle.getAttribute('aria-checked');
|
|
317
|
+
toggle.setAttribute('aria-checked', next ? 'true' : 'false');
|
|
318
|
+
this.#host.onHarness?.(next);
|
|
319
|
+
}
|
|
320
|
+
else if ('reactComponents' === key) {
|
|
290
321
|
this.#update({ reactComponents: !activeSettings.reactComponents });
|
|
291
322
|
}
|
|
292
323
|
else if ('hideUntilRestart' === key) {
|
|
@@ -368,6 +399,12 @@ export class PresenterSettingsPanel {
|
|
|
368
399
|
applyPresenterSettings(root, activeSettings);
|
|
369
400
|
this.#host.onSettingsChange?.(activeSettings);
|
|
370
401
|
}
|
|
402
|
+
paintHarness(config) {
|
|
403
|
+
const root = this.#panel;
|
|
404
|
+
if (root === undefined)
|
|
405
|
+
return;
|
|
406
|
+
paintHarnessRow(root, config);
|
|
407
|
+
}
|
|
371
408
|
teardown() {
|
|
372
409
|
this.#root = undefined;
|
|
373
410
|
this.#panel = undefined;
|
|
@@ -83,6 +83,18 @@ export const SHELL_CSS = `
|
|
|
83
83
|
right:auto;left:0;}
|
|
84
84
|
[${OVERLAY}][${CHAT_ATTR}="1"] [${CHAT_PANEL}]{
|
|
85
85
|
display:flex;pointer-events:auto;}
|
|
86
|
+
/**
|
|
87
|
+
* Idle, and not blocking the page: the card is not in use.
|
|
88
|
+
*
|
|
89
|
+
* The log fills the panel and sets pointer-events itself, so a click meant for a control sitting
|
|
90
|
+
* under the card landed on an empty transcript. Playwright named that div. The card passes the
|
|
91
|
+
* event through. The buttons stay clickable, which is the chrome a person still needs (#992).
|
|
92
|
+
*/
|
|
93
|
+
[${OVERLAY}][data-reticle-mode="idle"][data-reticle-block="0"] [${CHAT_PANEL}],
|
|
94
|
+
[${OVERLAY}][data-reticle-mode="idle"][data-reticle-block="0"] [${CHAT_PANEL}] [data-reticle-log]{
|
|
95
|
+
pointer-events:none;}
|
|
96
|
+
[${OVERLAY}][data-reticle-mode="idle"][data-reticle-block="0"] [${CHAT_PANEL}] :is(button,a,input,select,textarea){
|
|
97
|
+
pointer-events:auto;}
|
|
86
98
|
/**
|
|
87
99
|
* Minimising the chat leaves a CAPSULE, not a hole: the same glass, the same state dot, the last
|
|
88
100
|
* thing the agent did - sitting directly above the toolbar capsule and reopening the panel when
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { HarnessConfig } from '@reticlehq/core';
|
|
1
2
|
import { PresenterReport } from './presenter-report.js';
|
|
2
3
|
import type { AccountState } from '@reticlehq/core';
|
|
3
4
|
import { type AccountDetails } from './presenter-account.js';
|
|
5
|
+
import { type OfferState } from './presenter-offer.js';
|
|
4
6
|
import { type SettingsHost } from './presenter-settings.js';
|
|
5
7
|
interface HudShellCallbacks {
|
|
6
8
|
onChatOpen?: () => void;
|
|
@@ -13,7 +15,9 @@ interface HudShellCallbacks {
|
|
|
13
15
|
}
|
|
14
16
|
export declare class HudShell {
|
|
15
17
|
#private;
|
|
18
|
+
paintHarness(config: HarnessConfig | undefined): void;
|
|
16
19
|
paintAccount(account: AccountState | undefined, dashboardUrl: string | undefined, details?: AccountDetails): void;
|
|
20
|
+
paintOffer(offer: OfferState | undefined): void;
|
|
17
21
|
constructor(callbacks?: HudShellCallbacks);
|
|
18
22
|
static dockHtml(actStripHtml: string, bannerHtml: string, logAttr: string, flowsHtml: string, footHtml: string): string;
|
|
19
23
|
get report(): PresenterReport;
|
|
@@ -3,6 +3,7 @@ import { paintToolbarAccount, TOOLBAR_ACCOUNT_ATTR } from './presenter-workspace
|
|
|
3
3
|
import { mountAccountControl } from './presenter-account.js';
|
|
4
4
|
import { paintSettingsAccount } from './presenter-settings.js';
|
|
5
5
|
import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, REPORT_BTN_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, SETTINGS_ATTR, SETTINGS_BTN_ATTR, } from './presenter-config.js';
|
|
6
|
+
import { OFFER_SLOT_ATTR, paintOffer } from './presenter-offer.js';
|
|
6
7
|
import { BRAND_NAME, FAB_TOGGLE_HTML, MARK_SVG } from './chrome/presenter-brand.js';
|
|
7
8
|
import { settleLogAtLatest } from './chrome/presenter-log.js';
|
|
8
9
|
import { installHudDragHandles, installHudPositionGuards } from './presenter-drag.js';
|
|
@@ -45,6 +46,9 @@ export class HudShell {
|
|
|
45
46
|
#callbacks;
|
|
46
47
|
#pushedAccount;
|
|
47
48
|
#accountTeardown;
|
|
49
|
+
paintHarness(config) {
|
|
50
|
+
this.#settings.paintHarness(config);
|
|
51
|
+
}
|
|
48
52
|
paintAccount(account, dashboardUrl, details = {}) {
|
|
49
53
|
this.#pushedAccount = { account, dashboardUrl, details };
|
|
50
54
|
if (this.#root === undefined)
|
|
@@ -52,6 +56,21 @@ export class HudShell {
|
|
|
52
56
|
paintToolbarAccount(this.#root, account, dashboardUrl, details);
|
|
53
57
|
paintSettingsAccount(this.#root, account, dashboardUrl, details);
|
|
54
58
|
}
|
|
59
|
+
#pushedOffer;
|
|
60
|
+
paintOffer(offer) {
|
|
61
|
+
this.#pushedOffer = offer;
|
|
62
|
+
if (this.#root === undefined)
|
|
63
|
+
return;
|
|
64
|
+
paintOffer(this.#root, offer, this.#storage());
|
|
65
|
+
}
|
|
66
|
+
#storage() {
|
|
67
|
+
try {
|
|
68
|
+
return globalThis.localStorage;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
55
74
|
constructor(callbacks = {}) {
|
|
56
75
|
this.#callbacks = callbacks;
|
|
57
76
|
this.#settings = new PresenterSettingsPanel({
|
|
@@ -78,6 +97,7 @@ export class HudShell {
|
|
|
78
97
|
${actStripHtml}
|
|
79
98
|
<span class="reticle-tally" data-reticle-tally hidden></span>
|
|
80
99
|
${bannerHtml}
|
|
100
|
+
<div ${OFFER_SLOT_ATTR}></div>
|
|
81
101
|
<div class="${HUD_LOG_WELL_CLASS}"><div ${logAttr}></div></div>
|
|
82
102
|
${flowsHtml}
|
|
83
103
|
${footHtml}
|
|
@@ -228,6 +248,8 @@ export class HudShell {
|
|
|
228
248
|
const pushed = this.#pushedAccount;
|
|
229
249
|
this.paintAccount(pushed.account, pushed.dashboardUrl, pushed.details);
|
|
230
250
|
}
|
|
251
|
+
if (this.#pushedOffer !== undefined)
|
|
252
|
+
this.paintOffer(this.#pushedOffer);
|
|
231
253
|
}
|
|
232
254
|
teardown() {
|
|
233
255
|
this.#accountTeardown?.();
|
|
@@ -6,6 +6,7 @@ import { SETTINGS_CSS } from './presenter-settings-styles.js';
|
|
|
6
6
|
import { ACCOUNT_CSS } from './presenter-account-styles.js';
|
|
7
7
|
import { HUD_CHROME_CSS, HUD_LOG_WELL_CSS } from './chrome/presenter-hud-chrome.js';
|
|
8
8
|
import { REPORT_CSS } from './presenter-report-styles.js';
|
|
9
|
+
import { OFFER_CSS } from './presenter-offer.js';
|
|
9
10
|
export { Z_OVERLAY };
|
|
10
11
|
export const PRESENTER_CSS = `
|
|
11
12
|
[data-reticle-overlay]{position:fixed;inset:0;pointer-events:none;z-index:${String(Z_OVERLAY)};}
|
|
@@ -63,5 +64,6 @@ ${SHELL_CSS}
|
|
|
63
64
|
${SETTINGS_CSS}
|
|
64
65
|
${REPORT_CSS}
|
|
65
66
|
${ACCOUNT_CSS}
|
|
67
|
+
${OFFER_CSS}
|
|
66
68
|
${LOG_CSS}
|
|
67
69
|
${CONTROLS_CSS}`;
|
|
@@ -93,6 +93,7 @@ export class Presenter {
|
|
|
93
93
|
},
|
|
94
94
|
onHideUntilRestart: () => this.#applyHideUntilRestart(),
|
|
95
95
|
onSettingsChange: (s) => this.#onSettingsChange(s),
|
|
96
|
+
onHarness: (enabled) => this.#onControl?.({ kind: HumanControlKind.HARNESS, text: enabled ? 'on' : 'off' }),
|
|
96
97
|
},
|
|
97
98
|
});
|
|
98
99
|
}
|
|
@@ -119,6 +120,8 @@ export class Presenter {
|
|
|
119
120
|
const snapshot = parseImpactSnapshot(a['snapshot']);
|
|
120
121
|
if (snapshot !== undefined) {
|
|
121
122
|
this.#shell.report.setSnapshot(snapshot);
|
|
123
|
+
this.#shell.paintOffer(snapshot.harnessOffer);
|
|
124
|
+
this.#shell.paintHarness(snapshot.harnessConfig);
|
|
122
125
|
this.#shell.paintAccount(snapshot.account, snapshot.dashboardUrl, {
|
|
123
126
|
projectName: snapshot.projectName,
|
|
124
127
|
dashboardUrl: snapshot.dashboardUrl,
|
|
@@ -111,9 +111,13 @@ export const TOUR_CSS = `
|
|
|
111
111
|
[${TOUR_ATTR}] .reticle-tour-blocker{position:absolute;pointer-events:auto;z-index:0;}
|
|
112
112
|
/* Ringed slides dim through the ring's own 9999px shadow, which leaves a hole over the thing being
|
|
113
113
|
pointed at. The scrim would cover that hole -- dimming the HUD the slide is ABOUT, and washing
|
|
114
|
-
the page twice (~80% rather than 55%).
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
the page twice (~80% rather than 55%).
|
|
115
|
+
It is cleared for the clicks too, and that half was missing. A scrim blocks only while it is
|
|
116
|
+
VISIBLY blocking: reported from the field on 3.1.0 by several users, one of whom removed the
|
|
117
|
+
plugin, because a cleared scrim went on eating every press on a page that looked lit and
|
|
118
|
+
reachable. An overlay that looks transparent and behaves opaque reads as the app being broken. */
|
|
119
|
+
[${TOUR_ATTR}] .reticle-tour-scrim.is-clear{background:transparent;backdrop-filter:none;
|
|
120
|
+
pointer-events:none;}
|
|
117
121
|
/* Above the ring, and the z-index is the whole point.
|
|
118
122
|
The ring is appended after the card, and both are positioned with an auto z-index, so the ring
|
|
119
123
|
painted on top of it -- and the ring's dimming IS a 9999px shadow, which therefore fell across the
|
|
@@ -92,15 +92,27 @@ export function mountTour(deps) {
|
|
|
92
92
|
let index = 0;
|
|
93
93
|
let open = true;
|
|
94
94
|
let detachKeys = () => undefined;
|
|
95
|
-
|
|
95
|
+
let stopWatch = () => undefined;
|
|
96
|
+
const remove = () => {
|
|
96
97
|
if (!open)
|
|
97
98
|
return;
|
|
98
99
|
open = false;
|
|
99
100
|
detachKeys();
|
|
100
|
-
|
|
101
|
+
stopWatch();
|
|
101
102
|
root.remove();
|
|
102
103
|
style.remove();
|
|
103
104
|
};
|
|
105
|
+
const close = () => {
|
|
106
|
+
if (!open)
|
|
107
|
+
return;
|
|
108
|
+
markSeen(deps.storage, deps.projectId);
|
|
109
|
+
remove();
|
|
110
|
+
};
|
|
111
|
+
const yieldToDrive = () => {
|
|
112
|
+
if (!deps.isDriving())
|
|
113
|
+
return;
|
|
114
|
+
remove();
|
|
115
|
+
};
|
|
104
116
|
const draw = () => {
|
|
105
117
|
const slide = slides[index];
|
|
106
118
|
if (undefined === slide)
|
|
@@ -229,6 +241,13 @@ export function mountTour(deps) {
|
|
|
229
241
|
};
|
|
230
242
|
draw();
|
|
231
243
|
doc.body.appendChild(root);
|
|
244
|
+
const overlay = doc.querySelector('[data-reticle-overlay]');
|
|
245
|
+
if (null !== overlay) {
|
|
246
|
+
const observer = new MutationObserver(yieldToDrive);
|
|
247
|
+
observer.observe(overlay, { attributes: true, attributeFilter: ['data-reticle-mode'] });
|
|
248
|
+
stopWatch = () => observer.disconnect();
|
|
249
|
+
yieldToDrive();
|
|
250
|
+
}
|
|
232
251
|
return {
|
|
233
252
|
destroy: close,
|
|
234
253
|
isOpen: () => open,
|
package/dist/reticle.js
CHANGED
|
@@ -255,7 +255,9 @@ export class Reticle {
|
|
|
255
255
|
panel.handlePush(buffered);
|
|
256
256
|
this.#pendingPushes.clear();
|
|
257
257
|
void import('./presenter/tour/tour.js').then((tour) => {
|
|
258
|
-
|
|
258
|
+
if (!this.#connected)
|
|
259
|
+
return;
|
|
260
|
+
const shown = tour.mountTour({
|
|
259
261
|
document,
|
|
260
262
|
storage: tour.safeLocalStorage(),
|
|
261
263
|
projectId: reticleParamsFromSearch(window.location.search).projectId ?? window.location.origin,
|
|
@@ -272,6 +274,8 @@ export class Reticle {
|
|
|
272
274
|
selection.addRange(range);
|
|
273
275
|
},
|
|
274
276
|
});
|
|
277
|
+
if (shown !== undefined)
|
|
278
|
+
this.#teardowns.push(shown.destroy);
|
|
275
279
|
});
|
|
276
280
|
if (this.#bridgeConnected)
|
|
277
281
|
panel.sessionStart();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "3.
|
|
16
|
+
"@reticlehq/core": "3.2.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@iconify-json/heroicons-outline": "^1.2.1",
|