@reticlehq/browser 3.1.0 → 3.3.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 +2 -1
- package/dist/actions/actions-dom.js +28 -41
- package/dist/actions/actions.d.ts +0 -1
- package/dist/actions/actions.js +13 -7
- package/dist/actions/value-input.js +14 -5
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/addressing/source.d.ts +2 -1
- package/dist/dom/dom-ignore.d.ts +1 -0
- package/dist/dom/dom-ignore.js +13 -0
- package/dist/dom/query.js +28 -9
- package/dist/dom/snapshot.d.ts +1 -1
- package/dist/observers/console.js +38 -13
- package/dist/observers/departure.js +16 -8
- package/dist/observers/dom.js +3 -1
- package/dist/observers/download.js +3 -2
- package/dist/observers/error-repeats.d.ts +18 -0
- package/dist/observers/error-repeats.js +41 -0
- package/dist/observers/field.d.ts +2 -0
- package/dist/observers/field.js +86 -0
- package/dist/observers/frames.js +3 -4
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +24 -7
- package/dist/observers/network.js +49 -8
- package/dist/observers/route.js +4 -4
- package/dist/observers/state.js +18 -8
- package/dist/presenter/carousel/carousel.d.ts +13 -0
- package/dist/presenter/carousel/carousel.js +126 -0
- package/dist/presenter/carousel/offer-card.d.ts +21 -0
- package/dist/presenter/carousel/offer-card.js +55 -0
- package/dist/presenter/carousel/panel-slides.d.ts +7 -0
- package/dist/presenter/carousel/panel-slides.js +10 -0
- package/dist/presenter/carousel/talk-card.d.ts +8 -0
- package/dist/presenter/carousel/talk-card.js +20 -0
- package/dist/presenter/chrome/presenter-log.d.ts +2 -1
- package/dist/presenter/chrome/presenter-log.js +14 -3
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -1
- package/dist/presenter/chrome/presenter-report-copy.js +1 -1
- package/dist/presenter/hud-telemetry.d.ts +3 -0
- package/dist/presenter/hud-telemetry.js +87 -0
- package/dist/presenter/presenter-account.js +1 -1
- package/dist/presenter/presenter-config.d.ts +5 -1
- package/dist/presenter/presenter-config.js +4 -1
- package/dist/presenter/presenter-controls.d.ts +1 -0
- package/dist/presenter/presenter-controls.js +12 -1
- package/dist/presenter/presenter-report-styles.js +4 -0
- package/dist/presenter/presenter-report.js +8 -7
- package/dist/presenter/presenter-settings-styles.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +6 -4
- package/dist/presenter/presenter-settings.js +51 -5
- package/dist/presenter/presenter-shell-styles.js +16 -0
- package/dist/presenter/presenter-shell.d.ts +7 -0
- package/dist/presenter/presenter-shell.js +65 -5
- package/dist/presenter/presenter-styles.js +6 -0
- package/dist/presenter/presenter-workspace.d.ts +0 -7
- package/dist/presenter/presenter-workspace.js +7 -7
- package/dist/presenter/presenter.js +46 -10
- package/dist/presenter/tour/tour-view.js +7 -3
- package/dist/presenter/tour/tour.d.ts +3 -0
- package/dist/presenter/tour/tour.js +26 -2
- package/dist/recorder/recorder.js +25 -22
- package/dist/reticle-inject.d.ts +2 -0
- package/dist/reticle-inject.js +1337 -0
- package/dist/reticle.js +11 -1
- package/dist/review/annotator.d.ts +3 -0
- package/dist/review/annotator.js +37 -37
- package/dist/review/marks-for-agent.d.ts +9 -0
- package/dist/review/marks-for-agent.js +12 -0
- package/dist/util/captured-method.d.ts +0 -1
- package/dist/util/captured-method.js +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EventType, isSensitiveKey } from '@reticlehq/core';
|
|
2
|
+
import { getAccessibleName } from '../dom/a11y.js';
|
|
3
|
+
const MAX_VALUE_LEN = 160;
|
|
4
|
+
const BURST_MS = 150;
|
|
5
|
+
const PAYMENT_AUTOCOMPLETE = /^(cc-|current-password|new-password)/i;
|
|
6
|
+
function isField(target) {
|
|
7
|
+
return (target instanceof HTMLInputElement ||
|
|
8
|
+
target instanceof HTMLTextAreaElement ||
|
|
9
|
+
target instanceof HTMLSelectElement);
|
|
10
|
+
}
|
|
11
|
+
function fieldName(element) {
|
|
12
|
+
const testid = element.getAttribute('data-testid');
|
|
13
|
+
if (null !== testid && testid.length > 0)
|
|
14
|
+
return testid;
|
|
15
|
+
if (element.name.length > 0)
|
|
16
|
+
return element.name;
|
|
17
|
+
const accessible = getAccessibleName(element);
|
|
18
|
+
if (accessible.length > 0)
|
|
19
|
+
return accessible;
|
|
20
|
+
const id = element.getAttribute('id');
|
|
21
|
+
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function isSecret(element) {
|
|
24
|
+
if (element instanceof HTMLInputElement && 'password' === element.type)
|
|
25
|
+
return true;
|
|
26
|
+
const hint = element.getAttribute('autocomplete');
|
|
27
|
+
if (null !== hint && PAYMENT_AUTOCOMPLETE.test(hint))
|
|
28
|
+
return true;
|
|
29
|
+
for (const key of [
|
|
30
|
+
element.name,
|
|
31
|
+
element.getAttribute('data-testid'),
|
|
32
|
+
element.getAttribute('id'),
|
|
33
|
+
]) {
|
|
34
|
+
if (null !== key && key.length > 0 && isSensitiveKey(key))
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export function installField(emit) {
|
|
40
|
+
const ac = new AbortController();
|
|
41
|
+
const { signal } = ac;
|
|
42
|
+
const pending = new Map();
|
|
43
|
+
const report = (element, kind) => {
|
|
44
|
+
const value = element.value;
|
|
45
|
+
const secret = isSecret(element);
|
|
46
|
+
emit(EventType.FIELD_CHANGE, {
|
|
47
|
+
field: fieldName(element),
|
|
48
|
+
kind,
|
|
49
|
+
length: value.length,
|
|
50
|
+
...(secret
|
|
51
|
+
? { redacted: true }
|
|
52
|
+
: { value: value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value }),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const clear = (element) => {
|
|
56
|
+
const timer = pending.get(element);
|
|
57
|
+
if (timer !== undefined)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
pending.delete(element);
|
|
60
|
+
};
|
|
61
|
+
const onInput = (event) => {
|
|
62
|
+
const element = event.target;
|
|
63
|
+
if (!isField(element))
|
|
64
|
+
return;
|
|
65
|
+
clear(element);
|
|
66
|
+
pending.set(element, setTimeout(() => {
|
|
67
|
+
pending.delete(element);
|
|
68
|
+
report(element, 'input');
|
|
69
|
+
}, BURST_MS));
|
|
70
|
+
};
|
|
71
|
+
const onChange = (event) => {
|
|
72
|
+
const element = event.target;
|
|
73
|
+
if (!isField(element))
|
|
74
|
+
return;
|
|
75
|
+
clear(element);
|
|
76
|
+
report(element, 'change');
|
|
77
|
+
};
|
|
78
|
+
document.addEventListener('input', onInput, { signal, capture: true });
|
|
79
|
+
document.addEventListener('change', onChange, { signal, capture: true });
|
|
80
|
+
return () => {
|
|
81
|
+
for (const timer of pending.values())
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
pending.clear();
|
|
84
|
+
ac.abort();
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/observers/frames.js
CHANGED
|
@@ -24,6 +24,7 @@ export function sameOriginFrameBodies(root) {
|
|
|
24
24
|
return found;
|
|
25
25
|
}
|
|
26
26
|
export function observeSameOriginFrames(attach) {
|
|
27
|
+
const ac = new AbortController();
|
|
27
28
|
const attachAll = () => {
|
|
28
29
|
for (const body of sameOriginFrameBodies(document))
|
|
29
30
|
attach(body);
|
|
@@ -33,8 +34,6 @@ export function observeSameOriginFrames(attach) {
|
|
|
33
34
|
if (event.target instanceof HTMLIFrameElement)
|
|
34
35
|
attachAll();
|
|
35
36
|
};
|
|
36
|
-
document.addEventListener('load', onLoad, true);
|
|
37
|
-
return () =>
|
|
38
|
-
document.removeEventListener('load', onLoad, true);
|
|
39
|
-
};
|
|
37
|
+
document.addEventListener('load', onLoad, { capture: true, signal: ac.signal });
|
|
38
|
+
return () => ac.abort();
|
|
40
39
|
}
|
|
@@ -2,6 +2,7 @@ import { installDom } from './dom.js';
|
|
|
2
2
|
import { installStorage } from './storage.js';
|
|
3
3
|
import { installStoreState } from './state.js';
|
|
4
4
|
import { installFocus } from './focus.js';
|
|
5
|
+
import { installField } from './field.js';
|
|
5
6
|
import { installBlindSpots } from './blind-spots.js';
|
|
6
7
|
import { installDownload } from './download.js';
|
|
7
8
|
import { installContextOpen } from './context-open.js';
|
|
@@ -59,6 +60,7 @@ export function installAllObservers(emit, options) {
|
|
|
59
60
|
guard(emit, SdkSite.STORE_ADAPTER, () => installStorage(emit)),
|
|
60
61
|
guard(emit, SdkSite.STORE_ADAPTER, () => installStoreState(emit)),
|
|
61
62
|
guard(emit, SdkSite.DOM_OBSERVER, () => installFocus(emit)),
|
|
63
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installField(emit)),
|
|
62
64
|
guard(emit, SdkSite.NETWORK_OBSERVER, () => installDownload(emit, { capturePreview: options.captureBodies })),
|
|
63
65
|
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)),
|
|
64
66
|
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
|
|
2
2
|
export declare function withBodyDeadline(read: Promise<string>): Promise<string | undefined>;
|
|
3
3
|
export declare function isCapturableType(contentType: string | null): boolean;
|
|
4
|
+
export declare function setNetworkBodyMaxChars(chars: number): void;
|
|
5
|
+
export declare function networkBodyMaxChars(): number;
|
|
4
6
|
export declare function shouldCaptureErrorBody(status: number, enabled: boolean): boolean;
|
|
5
7
|
export declare function projectErrorBody(rawText: string, contentType: string | null): {
|
|
6
8
|
body: string;
|
|
@@ -32,8 +32,23 @@ export function isCapturableType(contentType) {
|
|
|
32
32
|
return false;
|
|
33
33
|
return CAPTURABLE_CONTENT.test(contentType);
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
35
|
+
const DEFAULT_BODY_CHARS = 8192;
|
|
36
|
+
const MAX_CONFIGURABLE_BODY_CHARS = 262144;
|
|
37
|
+
const MIN_CONFIGURABLE_BODY_CHARS = 256;
|
|
38
|
+
let bodyMaxChars = DEFAULT_BODY_CHARS;
|
|
39
|
+
export function setNetworkBodyMaxChars(chars) {
|
|
40
|
+
if (!Number.isFinite(chars))
|
|
41
|
+
return;
|
|
42
|
+
bodyMaxChars = Math.min(MAX_CONFIGURABLE_BODY_CHARS, Math.max(MIN_CONFIGURABLE_BODY_CHARS, Math.floor(chars)));
|
|
43
|
+
}
|
|
44
|
+
export function networkBodyMaxChars() {
|
|
45
|
+
return bodyMaxChars;
|
|
46
|
+
}
|
|
47
|
+
const REDACT_SCAN_CEILING = DEFAULT_BODY_CHARS * 2;
|
|
48
|
+
function scanWidth(redacting) {
|
|
49
|
+
const generous = bodyMaxChars * 2;
|
|
50
|
+
return redacting ? Math.min(generous, REDACT_SCAN_CEILING) : generous;
|
|
51
|
+
}
|
|
37
52
|
const AUTH_SCHEME_TOKEN = /\b(Bearer|Basic)\s+(?=[A-Za-z0-9._~+/=-]{16,})(?=[A-Za-z0-9._~+/=-]*[\d._~+/=-])[A-Za-z0-9._~+/=-]+/gi;
|
|
38
53
|
function redactText(text) {
|
|
39
54
|
return (text
|
|
@@ -51,10 +66,12 @@ export function projectErrorBody(rawText, contentType) {
|
|
|
51
66
|
return { body: projected.body.slice(0, ERROR_BODY_CHARS), truncated: true };
|
|
52
67
|
}
|
|
53
68
|
export function projectBody(rawText, contentType) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
69
|
+
const isJson = contentType !== null && /json|graphql/i.test(contentType);
|
|
70
|
+
const scanChars = scanWidth(!isJson);
|
|
71
|
+
const oversized = rawText.length > scanChars;
|
|
72
|
+
const text = oversized ? rawText.slice(0, scanChars) : rawText;
|
|
56
73
|
let out;
|
|
57
|
-
if (
|
|
74
|
+
if (isJson) {
|
|
58
75
|
try {
|
|
59
76
|
out = safeStringify(JSON.parse(text));
|
|
60
77
|
}
|
|
@@ -66,6 +83,6 @@ export function projectBody(rawText, contentType) {
|
|
|
66
83
|
out = redactText(text);
|
|
67
84
|
}
|
|
68
85
|
out = scrubKnownSecrets(out);
|
|
69
|
-
const truncated = oversized || out.length >
|
|
70
|
-
return { body: out.length >
|
|
86
|
+
const truncated = oversized || out.length > bodyMaxChars;
|
|
87
|
+
return { body: out.length > bodyMaxChars ? out.slice(0, bodyMaxChars) : out, truncated };
|
|
71
88
|
}
|
|
@@ -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;
|
package/dist/observers/route.js
CHANGED
|
@@ -10,6 +10,7 @@ function snapshotLocation() {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
export function installRoute(emit) {
|
|
13
|
+
const ac = new AbortController();
|
|
13
14
|
const origPush = captureMethod(history, 'pushState');
|
|
14
15
|
const origReplace = captureMethod(history, 'replaceState');
|
|
15
16
|
const callPush = origPush.bind(history);
|
|
@@ -45,14 +46,13 @@ export function installRoute(emit) {
|
|
|
45
46
|
const onNav = () => {
|
|
46
47
|
fire(lastHref);
|
|
47
48
|
};
|
|
48
|
-
window.addEventListener('popstate', onNav);
|
|
49
|
-
window.addEventListener('hashchange', onNav);
|
|
49
|
+
window.addEventListener('popstate', onNav, { signal: ac.signal });
|
|
50
|
+
window.addEventListener('hashchange', onNav, { signal: ac.signal });
|
|
50
51
|
return () => {
|
|
51
52
|
if (history.pushState === patchedPush)
|
|
52
53
|
history.pushState = origPush;
|
|
53
54
|
if (history.replaceState === patchedReplace)
|
|
54
55
|
history.replaceState = origReplace;
|
|
55
|
-
|
|
56
|
-
window.removeEventListener('hashchange', onNav);
|
|
56
|
+
ac.abort();
|
|
57
57
|
};
|
|
58
58
|
}
|
package/dist/observers/state.js
CHANGED
|
@@ -19,6 +19,16 @@ export function diffState(prev, next) {
|
|
|
19
19
|
function project(path, value) {
|
|
20
20
|
return isSensitiveKey(path) ? REDACTED_VALUE : sanitizeForTransport(value);
|
|
21
21
|
}
|
|
22
|
+
function samePresentedValue(oldValue, newValue) {
|
|
23
|
+
if (Object.is(oldValue, newValue))
|
|
24
|
+
return true;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(oldValue) === JSON.stringify(newValue);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
function safeRead(getter) {
|
|
23
33
|
try {
|
|
24
34
|
return getter();
|
|
@@ -39,15 +49,15 @@ export function installStoreState(emit) {
|
|
|
39
49
|
active.set(name, subscribe(() => {
|
|
40
50
|
observeSafely(() => {
|
|
41
51
|
const next = safeRead(getter);
|
|
42
|
-
|
|
43
|
-
emit(EventType.STATE_CHANGE, {
|
|
44
|
-
name,
|
|
45
|
-
path: change.path,
|
|
46
|
-
value: project(change.path, change.new),
|
|
47
|
-
old: project(change.path, change.old),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
52
|
+
const changes = diffState(last, next);
|
|
50
53
|
last = next;
|
|
54
|
+
for (const change of changes) {
|
|
55
|
+
const value = project(change.path, change.new);
|
|
56
|
+
const old = project(change.path, change.old);
|
|
57
|
+
if (!isSensitiveKey(change.path) && samePresentedValue(old, value))
|
|
58
|
+
continue;
|
|
59
|
+
observeSafely(() => emit(EventType.STATE_CHANGE, { name, path: change.path, value, old }));
|
|
60
|
+
}
|
|
51
61
|
});
|
|
52
62
|
}));
|
|
53
63
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CAROUSEL_ATTR = "data-reticle-carousel";
|
|
2
|
+
export declare const CAROUSEL_SLIDE_ATTR = "data-reticle-carousel-slide";
|
|
3
|
+
export declare const CAROUSEL_DOT_ATTR = "data-reticle-carousel-dot";
|
|
4
|
+
export declare const CAROUSEL_CLOSE_ATTR = "data-reticle-carousel-close";
|
|
5
|
+
export declare const CAROUSEL_DISMISSED_KEY = "reticle.carousel.dismissed";
|
|
6
|
+
export declare const ROTATE_MS = 6000;
|
|
7
|
+
export interface Slide {
|
|
8
|
+
id: string;
|
|
9
|
+
html: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function carouselHtml(slides: readonly Slide[], active: number): string;
|
|
12
|
+
export declare function paintCarousel(log: HTMLElement, slides: readonly Slide[], storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
|
|
13
|
+
export declare const CAROUSEL_CSS = "\n.reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}\n.reticle-carousel-track{height:100px;overflow:hidden;}\n.reticle-carousel-slide .reticle-offer,.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}\n.reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}\n.reticle-carousel-close:hover{opacity:1;}\n.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}\n.reticle-carousel-dot{width:6px;height:6px;padding:0;border:0;border-radius:999px;background:currentColor;opacity:.3;cursor:pointer;}\n.reticle-carousel-dot[aria-current=\"true\"]{opacity:.9;}\n";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const CAROUSEL_ATTR = 'data-reticle-carousel';
|
|
2
|
+
export const CAROUSEL_SLIDE_ATTR = 'data-reticle-carousel-slide';
|
|
3
|
+
export const CAROUSEL_DOT_ATTR = 'data-reticle-carousel-dot';
|
|
4
|
+
export const CAROUSEL_CLOSE_ATTR = 'data-reticle-carousel-close';
|
|
5
|
+
export const CAROUSEL_DISMISSED_KEY = 'reticle.carousel.dismissed';
|
|
6
|
+
const CLOSE_LABEL = 'Close';
|
|
7
|
+
export const ROTATE_MS = 6000;
|
|
8
|
+
const rotations = new WeakMap();
|
|
9
|
+
function stopRotation(log) {
|
|
10
|
+
const timer = rotations.get(log);
|
|
11
|
+
if (timer !== undefined)
|
|
12
|
+
clearInterval(timer);
|
|
13
|
+
rotations.delete(log);
|
|
14
|
+
}
|
|
15
|
+
function prefersReducedMotion() {
|
|
16
|
+
try {
|
|
17
|
+
return true === globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function show(root, index) {
|
|
24
|
+
root.setAttribute('data-active', String(index));
|
|
25
|
+
root.querySelectorAll(`[${CAROUSEL_SLIDE_ATTR}]`).forEach((slide, i) => {
|
|
26
|
+
slide.hidden = i !== index;
|
|
27
|
+
});
|
|
28
|
+
root.querySelectorAll(`[${CAROUSEL_DOT_ATTR}]`).forEach((dot, i) => {
|
|
29
|
+
dot.setAttribute('aria-current', String(i === index));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const clampIndex = (index, count) => Number.isInteger(index) && index >= 0 && index < count ? index : 0;
|
|
33
|
+
export function carouselHtml(slides, active) {
|
|
34
|
+
if (0 === slides.length)
|
|
35
|
+
return '';
|
|
36
|
+
const current = clampIndex(active, slides.length);
|
|
37
|
+
const track = slides
|
|
38
|
+
.map((slide, i) => `<div ${CAROUSEL_SLIDE_ATTR}="${slide.id}" class="reticle-carousel-slide"${i === current ? '' : ' hidden'}>${slide.html}</div>`)
|
|
39
|
+
.join('');
|
|
40
|
+
const dots = 1 === slides.length
|
|
41
|
+
? ''
|
|
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>`;
|
|
45
|
+
return `<div ${CAROUSEL_ATTR} class="reticle-carousel" role="region" aria-label="Reticle" data-active="${String(current)}">
|
|
46
|
+
<button type="button" ${CAROUSEL_CLOSE_ATTR} class="reticle-carousel-close" aria-label="${CLOSE_LABEL}" title="${CLOSE_LABEL}">×</button>
|
|
47
|
+
<div class="reticle-carousel-track">${track}</div>
|
|
48
|
+
${dots}
|
|
49
|
+
</div>`;
|
|
50
|
+
}
|
|
51
|
+
function dismissed(storage) {
|
|
52
|
+
try {
|
|
53
|
+
return '1' === storage?.getItem(CAROUSEL_DISMISSED_KEY);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function paintCarousel(log, slides, storage) {
|
|
60
|
+
const existing = log.querySelector(`:scope > [${CAROUSEL_ATTR}]`);
|
|
61
|
+
const active = Number(existing?.getAttribute('data-active') ?? '0');
|
|
62
|
+
existing?.remove();
|
|
63
|
+
stopRotation(log);
|
|
64
|
+
if (dismissed(storage))
|
|
65
|
+
return;
|
|
66
|
+
const html = carouselHtml(slides, active);
|
|
67
|
+
if ('' === html)
|
|
68
|
+
return;
|
|
69
|
+
log.insertAdjacentHTML('afterbegin', html);
|
|
70
|
+
const root = log.firstElementChild;
|
|
71
|
+
if (!(root instanceof HTMLElement))
|
|
72
|
+
return;
|
|
73
|
+
root.querySelector(`[${CAROUSEL_CLOSE_ATTR}]`)?.addEventListener('click', (event) => {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
try {
|
|
77
|
+
storage?.setItem(CAROUSEL_DISMISSED_KEY, '1');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
}
|
|
81
|
+
stopRotation(log);
|
|
82
|
+
root.remove();
|
|
83
|
+
});
|
|
84
|
+
for (const dot of root.querySelectorAll(`[${CAROUSEL_DOT_ATTR}]`)) {
|
|
85
|
+
dot.addEventListener('click', (event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
show(root, Number(dot.getAttribute(CAROUSEL_DOT_ATTR)));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
startRotation(log, root, slides.length);
|
|
92
|
+
}
|
|
93
|
+
function startRotation(log, root, count) {
|
|
94
|
+
if (count < 2 || prefersReducedMotion())
|
|
95
|
+
return;
|
|
96
|
+
let paused = false;
|
|
97
|
+
const pause = () => {
|
|
98
|
+
paused = true;
|
|
99
|
+
};
|
|
100
|
+
const resume = () => {
|
|
101
|
+
paused = false;
|
|
102
|
+
};
|
|
103
|
+
root.addEventListener('mouseenter', pause);
|
|
104
|
+
root.addEventListener('mouseleave', resume);
|
|
105
|
+
root.addEventListener('focusin', pause);
|
|
106
|
+
root.addEventListener('focusout', resume);
|
|
107
|
+
rotations.set(log, setInterval(() => {
|
|
108
|
+
if (!root.isConnected) {
|
|
109
|
+
stopRotation(log);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (paused)
|
|
113
|
+
return;
|
|
114
|
+
show(root, (Number(root.getAttribute('data-active')) + 1) % count);
|
|
115
|
+
}, ROTATE_MS));
|
|
116
|
+
}
|
|
117
|
+
export const CAROUSEL_CSS = `
|
|
118
|
+
.reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
|
|
119
|
+
.reticle-carousel-track{height:100px;overflow:hidden;}
|
|
120
|
+
.reticle-carousel-slide .reticle-offer,.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}
|
|
121
|
+
.reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}
|
|
122
|
+
.reticle-carousel-close:hover{opacity:1;}
|
|
123
|
+
.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}
|
|
124
|
+
.reticle-carousel-dot{width:6px;height:6px;padding:0;border:0;border-radius:999px;background:currentColor;opacity:.3;cursor:pointer;}
|
|
125
|
+
.reticle-carousel-dot[aria-current="true"]{opacity:.9;}
|
|
126
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const OFFER_ATTR = "data-reticle-offer";
|
|
2
|
+
export declare const OFFER_CLAIM_ATTR = "data-reticle-offer-claim";
|
|
3
|
+
export declare const OFFER_DISMISSED_KEY = "reticle.harnessOffer.dismissed";
|
|
4
|
+
export declare const OFFER_TEXT: {
|
|
5
|
+
readonly HEADLINE: "Get Harness free for 3 months";
|
|
6
|
+
readonly BODY: "Explores your app and saves every journey as a free replay.";
|
|
7
|
+
readonly CLAIM: "Claim now";
|
|
8
|
+
readonly DISMISS: "Not now";
|
|
9
|
+
readonly claimed: (days: number) => string;
|
|
10
|
+
readonly ending: (days: number) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare function offerDismissed(storage: Pick<Storage, 'getItem'> | undefined): boolean;
|
|
13
|
+
export interface OfferState {
|
|
14
|
+
claimed: boolean;
|
|
15
|
+
daysRemaining?: number | undefined;
|
|
16
|
+
claimUrl?: string | undefined;
|
|
17
|
+
eligible?: boolean | undefined;
|
|
18
|
+
drivable?: boolean | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function offerHtml(offer: OfferState | undefined, dismissed: boolean): string;
|
|
21
|
+
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-title{display:block;font-weight:600;font-size:12px;padding-right:18px;}\n.reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}\n.reticle-offer-claim{display:inline-block;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-claimed{margin:6px 10px;font-size:10px;opacity:.6;}\n";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const OFFER_ATTR = 'data-reticle-offer';
|
|
2
|
+
export const OFFER_CLAIM_ATTR = 'data-reticle-offer-claim';
|
|
3
|
+
export const OFFER_DISMISSED_KEY = 'reticle.harnessOffer.dismissed';
|
|
4
|
+
export const OFFER_TEXT = {
|
|
5
|
+
HEADLINE: 'Get Harness free for 3 months',
|
|
6
|
+
BODY: 'Explores your app and saves every journey as a free replay.',
|
|
7
|
+
CLAIM: 'Claim now',
|
|
8
|
+
DISMISS: 'Not now',
|
|
9
|
+
claimed: (days) => `Harness active — ${String(days)} days left`,
|
|
10
|
+
ending: (days) => 1 >= days
|
|
11
|
+
? 'Harness free period ends today'
|
|
12
|
+
: `Harness free period ends in ${String(days)} days`,
|
|
13
|
+
};
|
|
14
|
+
const ENDING_SOON_DAYS = 14;
|
|
15
|
+
export function offerDismissed(storage) {
|
|
16
|
+
try {
|
|
17
|
+
return '1' === storage?.getItem(OFFER_DISMISSED_KEY);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const escape = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
24
|
+
export function offerHtml(offer, dismissed) {
|
|
25
|
+
if (offer === undefined)
|
|
26
|
+
return '';
|
|
27
|
+
if (offer.claimed) {
|
|
28
|
+
const days = offer.daysRemaining;
|
|
29
|
+
if (days === undefined || 0 > days)
|
|
30
|
+
return '';
|
|
31
|
+
const text = ENDING_SOON_DAYS >= days ? OFFER_TEXT.ending(days) : OFFER_TEXT.claimed(days);
|
|
32
|
+
return `<div ${OFFER_ATTR} class="reticle-offer reticle-offer-claimed">${escape(text)}</div>`;
|
|
33
|
+
}
|
|
34
|
+
if (false === offer.eligible)
|
|
35
|
+
return '';
|
|
36
|
+
if (false === offer.drivable)
|
|
37
|
+
return '';
|
|
38
|
+
if (dismissed)
|
|
39
|
+
return '';
|
|
40
|
+
const url = offer.claimUrl;
|
|
41
|
+
if (url === undefined || 0 === url.length)
|
|
42
|
+
return '';
|
|
43
|
+
return `<div ${OFFER_ATTR} class="reticle-offer" role="note">
|
|
44
|
+
<span class="reticle-offer-title">${escape(OFFER_TEXT.HEADLINE)}</span>
|
|
45
|
+
<p class="reticle-offer-body">${escape(OFFER_TEXT.BODY)}</p>
|
|
46
|
+
<a ${OFFER_CLAIM_ATTR} class="reticle-offer-claim" href="${escape(url)}" target="_blank" rel="noopener noreferrer">${escape(OFFER_TEXT.CLAIM)}</a>
|
|
47
|
+
</div>`;
|
|
48
|
+
}
|
|
49
|
+
export const OFFER_CSS = `
|
|
50
|
+
.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));}
|
|
51
|
+
.reticle-offer-title{display:block;font-weight:600;font-size:12px;padding-right:18px;}
|
|
52
|
+
.reticle-offer-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}
|
|
53
|
+
.reticle-offer-claim{display:inline-block;font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}
|
|
54
|
+
.reticle-offer-claimed{margin:6px 10px;font-size:10px;opacity:.6;}
|
|
55
|
+
`;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type OfferState } from './offer-card.js';
|
|
2
|
+
import type { Slide } from './carousel.js';
|
|
3
|
+
export declare const SLIDE_ID: {
|
|
4
|
+
readonly HARNESS: "harness";
|
|
5
|
+
readonly FOUNDERS: "founders";
|
|
6
|
+
};
|
|
7
|
+
export declare function panelSlides(offer: OfferState | undefined, offerDeclined: boolean): Slide[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { offerHtml } from './offer-card.js';
|
|
2
|
+
import { talkCardHtml } from './talk-card.js';
|
|
3
|
+
export const SLIDE_ID = { HARNESS: 'harness', FOUNDERS: 'founders' };
|
|
4
|
+
export function panelSlides(offer, offerDeclined) {
|
|
5
|
+
const harness = offerHtml(offer, offerDeclined);
|
|
6
|
+
return [
|
|
7
|
+
...('' === harness ? [] : [{ id: SLIDE_ID.HARNESS, html: harness }]),
|
|
8
|
+
{ id: SLIDE_ID.FOUNDERS, html: talkCardHtml() },
|
|
9
|
+
];
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const TALK_BOOK_ATTR = "data-reticle-talk-book";
|
|
2
|
+
export declare const TALK_TEXT: {
|
|
3
|
+
readonly HEADLINE: "Talk to Founders";
|
|
4
|
+
readonly BODY: "Tell us where Reticle got in your way.";
|
|
5
|
+
readonly BOOK: "Book a call";
|
|
6
|
+
};
|
|
7
|
+
export declare function talkCardHtml(): string;
|
|
8
|
+
export declare const TALK_CSS = "\n.reticle-talk-title{display:block;font-weight:600;font-size:12px;padding-right:18px;}\n.reticle-talk-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}\n.reticle-talk-book{display:inline-block;font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}\n";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DISCOVERY_CALL_URL } from '@reticlehq/core';
|
|
2
|
+
export const TALK_BOOK_ATTR = 'data-reticle-talk-book';
|
|
3
|
+
export const TALK_TEXT = {
|
|
4
|
+
HEADLINE: 'Talk to Founders',
|
|
5
|
+
BODY: 'Tell us where Reticle got in your way.',
|
|
6
|
+
BOOK: 'Book a call',
|
|
7
|
+
};
|
|
8
|
+
const escape = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9
|
+
export function talkCardHtml() {
|
|
10
|
+
return `<div class="reticle-talk">
|
|
11
|
+
<span class="reticle-talk-title">${escape(TALK_TEXT.HEADLINE)}</span>
|
|
12
|
+
<p class="reticle-talk-body">${escape(TALK_TEXT.BODY)}</p>
|
|
13
|
+
<a ${TALK_BOOK_ATTR} class="reticle-talk-book" href="${escape(DISCOVERY_CALL_URL)}" target="_blank" rel="noopener noreferrer">${escape(TALK_TEXT.BOOK)}</a>
|
|
14
|
+
</div>`;
|
|
15
|
+
}
|
|
16
|
+
export const TALK_CSS = `
|
|
17
|
+
.reticle-talk-title{display:block;font-weight:600;font-size:12px;padding-right:18px;}
|
|
18
|
+
.reticle-talk-body{margin:6px 0 8px;font-size:11px;line-height:1.45;opacity:.8;}
|
|
19
|
+
.reticle-talk-book{display:inline-block;font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--reticle-accent,#4f7cff);color:#fff;text-decoration:none;}
|
|
20
|
+
`;
|
|
@@ -6,11 +6,12 @@ export declare const CHIP_LABEL: Record<PresenterMode, string>;
|
|
|
6
6
|
export declare const DATA_RETICLE_LOG = "data-reticle-log";
|
|
7
7
|
export declare const LOG_TIME_ATTR = "data-reticle-log-time";
|
|
8
8
|
export declare const LOG_CSS: string;
|
|
9
|
-
export declare function scrollLogToLatest(container: HTMLElement): void;
|
|
10
9
|
export declare function settleLogAtLatest(container: HTMLElement): void;
|
|
11
10
|
export interface LogHandle {
|
|
12
11
|
result(r: LogResult): void;
|
|
13
12
|
}
|
|
13
|
+
export declare function trimLogRows(container: HTMLElement, max: number): void;
|
|
14
|
+
export declare function clearLogRows(container: HTMLElement): void;
|
|
14
15
|
export declare function clampLogMax(n: number | undefined): number;
|
|
15
16
|
export declare function humanDuration(ms: number): string;
|
|
16
17
|
export declare function formatElapsed(ms: number): string;
|