@reticlehq/browser 2.10.0 → 2.12.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 +41 -3
- package/dist/actions/actions.js +37 -5
- package/dist/commands/commands.js +6 -2
- package/dist/dom/a11y.js +92 -9
- package/dist/dom/query.js +113 -94
- package/dist/dom/refs.d.ts +11 -0
- package/dist/dom/refs.js +30 -1
- package/dist/dom/snapshot.d.ts +29 -0
- package/dist/dom/snapshot.js +72 -34
- package/dist/edit-epoch.d.ts +32 -0
- package/dist/edit-epoch.js +108 -0
- package/dist/observers/context-open.d.ts +12 -0
- package/dist/observers/context-open.js +25 -0
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/network.js +59 -0
- package/dist/observers/storage.js +1 -1
- package/dist/presenter/presenter-brand.d.ts +4 -18
- package/dist/presenter/presenter-brand.js +6 -18
- package/dist/presenter/presenter-config.d.ts +0 -6
- package/dist/presenter/presenter-config.js +0 -3
- package/dist/presenter/presenter-controls.d.ts +0 -3
- package/dist/presenter/presenter-controls.js +21 -11
- package/dist/presenter/presenter-drag.js +17 -14
- package/dist/presenter/presenter-settings.js +10 -2
- package/dist/presenter/presenter-shell.d.ts +0 -2
- package/dist/presenter/presenter-shell.js +30 -16
- package/dist/reticle.d.ts +12 -0
- package/dist/reticle.js +32 -1
- package/dist/transport/unreachable-message.js +2 -1
- package/package.json +2 -5
- package/dist/dom/blind-spots.d.ts +0 -13
- package/dist/dom/blind-spots.js +0 -29
package/dist/dom/refs.js
CHANGED
|
@@ -19,6 +19,8 @@ import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
|
19
19
|
export const MAX_TRACKED_REFS = 10000;
|
|
20
20
|
/** Marks a ref that was too long to echo in full. */
|
|
21
21
|
const REF_ELISION = '…';
|
|
22
|
+
/** Every ref Reticle mints is this letter followed by its sequence number. */
|
|
23
|
+
const REF_PREFIX = 'e';
|
|
22
24
|
/**
|
|
23
25
|
* A ref, made safe to interpolate into an error message.
|
|
24
26
|
*
|
|
@@ -105,6 +107,15 @@ export class RefRegistry {
|
|
|
105
107
|
/** The end of the block currently reserved. Minting past it claims the next one. */
|
|
106
108
|
#reserved = 0;
|
|
107
109
|
#mintsSinceSweep = 0;
|
|
110
|
+
/**
|
|
111
|
+
* The highest number handed out before the last hot update was applied, or 0 while none has been.
|
|
112
|
+
*
|
|
113
|
+
* A watermark rather than a per-ref stamp: the sequence never restarts within a document, so one
|
|
114
|
+
* number separates "minted before the code changed" from "minted after it" for every ref at once.
|
|
115
|
+
* The alternative — an epoch on every map entry — is bookkeeping paid on every mint for an answer
|
|
116
|
+
* needed only on the rare refusal.
|
|
117
|
+
*/
|
|
118
|
+
#editedAtSeq = 0;
|
|
108
119
|
/**
|
|
109
120
|
* Storage is injected rather than read from the global for the same reason the clock is: it is an
|
|
110
121
|
* ambient dependency that decides behaviour, and the cross-document property is untestable if the
|
|
@@ -156,6 +167,24 @@ export class RefRegistry {
|
|
|
156
167
|
while (this.#fromRef.size > MAX_TRACKED_REFS)
|
|
157
168
|
this.#evictOldest();
|
|
158
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* A hot update just replaced modules in this document: every ref handed out so far was minted
|
|
172
|
+
* against the DOM the previous version of that code produced.
|
|
173
|
+
*
|
|
174
|
+
* Only the boundary is recorded. Nothing is evicted — a hot update is not a navigation, and most
|
|
175
|
+
* of the page survives one, so refs minted before it very often still resolve. This exists so the
|
|
176
|
+
* ones that DON'T can say why.
|
|
177
|
+
*/
|
|
178
|
+
markEdited() {
|
|
179
|
+
this.#editedAtSeq = this.#seq;
|
|
180
|
+
}
|
|
181
|
+
/** Was this ref handed out before the last hot update? False while no update has been observed. */
|
|
182
|
+
mintedBeforeLastEdit(ref) {
|
|
183
|
+
if (0 === this.#editedAtSeq || !ref.startsWith(REF_PREFIX))
|
|
184
|
+
return false;
|
|
185
|
+
const n = Number(ref.slice(REF_PREFIX.length));
|
|
186
|
+
return Number.isSafeInteger(n) && n > 0 && n <= this.#editedAtSeq;
|
|
187
|
+
}
|
|
159
188
|
/** Get the existing ref for an element, or mint a new one. */
|
|
160
189
|
refFor(el) {
|
|
161
190
|
const existing = this.#toRef.get(el);
|
|
@@ -173,7 +202,7 @@ export class RefRegistry {
|
|
|
173
202
|
if (this.#seq >= this.#reserved)
|
|
174
203
|
this.#reserve();
|
|
175
204
|
this.#seq += 1;
|
|
176
|
-
const ref = asRef(
|
|
205
|
+
const ref = asRef(`${REF_PREFIX}${String(this.#seq)}`);
|
|
177
206
|
this.#toRef.set(el, ref);
|
|
178
207
|
this.#fromRef.set(ref, new WeakRef(el));
|
|
179
208
|
// Keep bounded cheaply on every mint; run the full dead-entry sweep only periodically.
|
package/dist/dom/snapshot.d.ts
CHANGED
|
@@ -21,13 +21,42 @@ export interface SnapshotResult {
|
|
|
21
21
|
* must not receive the entire page as if it were the modal.
|
|
22
22
|
*/
|
|
23
23
|
scopeMissing?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Refs of the subtree roots the walk never entered, present ONLY when `truncated`. This is the
|
|
26
|
+
* cut's own frontier: re-snapshot each with `{ scope: ref, includeRoot: true }` and the union is
|
|
27
|
+
* the whole tree. Without it `truncated` says only THAT the read stopped, never WHERE, so nobody
|
|
28
|
+
* could finish it — and a reader who does not finish it must not conclude anything is ABSENT.
|
|
29
|
+
*/
|
|
30
|
+
unread?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* True when the frontier itself did not fit in `MAX_UNREAD_BRANCHES`, so `unread` is a prefix of
|
|
33
|
+
* the branches that were cut. Completion is then impossible from this read, and the difference
|
|
34
|
+
* between "impossible" and "not attempted" is the whole point of saying so.
|
|
35
|
+
*/
|
|
36
|
+
unreadOverflow?: true;
|
|
24
37
|
}
|
|
25
38
|
interface SnapshotOptions {
|
|
26
39
|
scope?: string | undefined;
|
|
27
40
|
mode?: SnapshotMode | undefined;
|
|
28
41
|
maxNodes?: number | undefined;
|
|
29
42
|
maxDepth?: number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Emit the scope element's OWN line before its descendants. Off by default (a scoped snapshot has
|
|
45
|
+
* always been the subtree UNDER the scope); the completion re-read needs it, because the branch it
|
|
46
|
+
* is re-reading is exactly the node the truncated walk stopped before emitting.
|
|
47
|
+
*/
|
|
48
|
+
includeRoot?: boolean | undefined;
|
|
30
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Cap on how many cut branches one snapshot names.
|
|
52
|
+
*
|
|
53
|
+
* A hostile or merely huge DOM can cut thousands of siblings at once (a non-virtualized grid does it
|
|
54
|
+
* routinely), and a frontier that large is neither affordable to send nor useful to re-read one at a
|
|
55
|
+
* time. Fifty is well above what a real page's cut produces — the frontier is the branches at the
|
|
56
|
+
* node cap, not the nodes themselves — and small enough that the list stays a hint rather than a
|
|
57
|
+
* second snapshot. Past it the read declares `unreadOverflow` instead of quietly sending fewer.
|
|
58
|
+
*/
|
|
59
|
+
export declare const MAX_UNREAD_BRANCHES = 50;
|
|
31
60
|
/** Build the semantic accessibility snapshot of the page or a subtree. */
|
|
32
61
|
export declare function buildSnapshot(options?: SnapshotOptions): SnapshotResult;
|
|
33
62
|
export {};
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -55,6 +55,16 @@ function directText(el) {
|
|
|
55
55
|
const collapsed = out.replace(/\s+/g, ' ').trim();
|
|
56
56
|
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Cap on how many cut branches one snapshot names.
|
|
60
|
+
*
|
|
61
|
+
* A hostile or merely huge DOM can cut thousands of siblings at once (a non-virtualized grid does it
|
|
62
|
+
* routinely), and a frontier that large is neither affordable to send nor useful to re-read one at a
|
|
63
|
+
* time. Fifty is well above what a real page's cut produces — the frontier is the branches at the
|
|
64
|
+
* node cap, not the nodes themselves — and small enough that the list stays a hint rather than a
|
|
65
|
+
* second snapshot. Past it the read declares `unreadOverflow` instead of quietly sending fewer.
|
|
66
|
+
*/
|
|
67
|
+
export const MAX_UNREAD_BRANCHES = 50;
|
|
58
68
|
/** Cheap skips that need no computed style (tags, overlays, aria-hidden, [hidden]). */
|
|
59
69
|
function skipEarly(el) {
|
|
60
70
|
if (SKIP_TAGS.has(el.tagName.toLowerCase()))
|
|
@@ -105,6 +115,16 @@ function layoutSignature(style) {
|
|
|
105
115
|
}
|
|
106
116
|
return '';
|
|
107
117
|
}
|
|
118
|
+
/** Record a cut branch on the frontier, declaring an overflow rather than shortening it silently. */
|
|
119
|
+
function recordUnread(branches, ctx) {
|
|
120
|
+
for (const branch of branches) {
|
|
121
|
+
if (ctx.unread.length >= MAX_UNREAD_BRANCHES) {
|
|
122
|
+
ctx.unreadOverflow = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
ctx.unread.push(refs.refFor(branch));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
/**
|
|
109
129
|
* The children to walk under `parent`, piercing boundaries a plain `.children` misses: an OPEN shadow
|
|
110
130
|
* root renders as part of the element (web components / design systems), and a SAME-ORIGIN iframe's
|
|
@@ -134,46 +154,57 @@ function pierceChildren(parent) {
|
|
|
134
154
|
}
|
|
135
155
|
return out;
|
|
136
156
|
}
|
|
157
|
+
/** Emit one element (if it earns a line) and descend into it. Split out of `walk` so the completion
|
|
158
|
+
* re-read can start AT a branch root — the node the truncated walk stopped just before emitting. */
|
|
159
|
+
function visit(child, depth, ctx, inLive) {
|
|
160
|
+
if (skipEarly(child))
|
|
161
|
+
return;
|
|
162
|
+
// Resolve computed style ONCE per node (the dominant snapshot cost) and thread it into both the
|
|
163
|
+
// display-none skip and the layout signature — was two forced style resolutions per node.
|
|
164
|
+
const view = child.ownerDocument.defaultView;
|
|
165
|
+
const style = view !== null ? view.getComputedStyle(child) : null;
|
|
166
|
+
if (style !== null && 'none' === style.display)
|
|
167
|
+
return;
|
|
168
|
+
const role = getRole(child);
|
|
169
|
+
const name = getAccessibleName(child);
|
|
170
|
+
const interactive = INTERACTIVE.has(role);
|
|
171
|
+
// Announcements are exempt from leanness, and so is everything inside one: a live region whose
|
|
172
|
+
// message sits in a child element would otherwise be included as a contentless `- generic`.
|
|
173
|
+
const announce = inLive || announces(child, role);
|
|
174
|
+
const lean = ctx.mode === SnapshotMode.INTERACTIVE && !announce;
|
|
175
|
+
// A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
|
|
176
|
+
// so the actionable-only view stays lean while FULL/meaningful views see content regressions.
|
|
177
|
+
const text = !lean && 'generic' === role && 0 === name.length ? directText(child) : '';
|
|
178
|
+
// Layout signature for grid/flex containers — makes CLS/layout regressions visible.
|
|
179
|
+
const layout = lean ? '' : layoutSignature(style);
|
|
180
|
+
const meaningful = interactive || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
181
|
+
const include = lean ? interactive : meaningful;
|
|
182
|
+
if (include) {
|
|
183
|
+
ctx.nodes += 1;
|
|
184
|
+
ctx.lines.push(text.length > 0 && 0 === name.length && 0 === layout.length
|
|
185
|
+
? formatTextLine(depth, text)
|
|
186
|
+
: formatLine(child, depth, role, name, layout));
|
|
187
|
+
walk(child, depth + 1, ctx, announce);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
walk(child, depth, ctx, announce);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
137
193
|
function walk(parent, depth, ctx, inLive = false) {
|
|
138
194
|
if (depth > ctx.maxDepth)
|
|
139
195
|
return;
|
|
140
|
-
|
|
196
|
+
const children = pierceChildren(parent);
|
|
197
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
141
198
|
if (ctx.nodes >= ctx.maxNodes) {
|
|
142
199
|
ctx.truncated = true;
|
|
200
|
+
// Everything from here on is unread, not absent. Naming the frontier is what makes the cut
|
|
201
|
+
// recoverable — see `unread` on SnapshotResult.
|
|
202
|
+
recordUnread(children.slice(index), ctx);
|
|
143
203
|
return;
|
|
144
204
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// display-none skip and the layout signature — was two forced style resolutions per node.
|
|
149
|
-
const view = child.ownerDocument.defaultView;
|
|
150
|
-
const style = view !== null ? view.getComputedStyle(child) : null;
|
|
151
|
-
if (style !== null && 'none' === style.display)
|
|
152
|
-
continue;
|
|
153
|
-
const role = getRole(child);
|
|
154
|
-
const name = getAccessibleName(child);
|
|
155
|
-
const interactive = INTERACTIVE.has(role);
|
|
156
|
-
// Announcements are exempt from leanness, and so is everything inside one: a live region whose
|
|
157
|
-
// message sits in a child element would otherwise be included as a contentless `- generic`.
|
|
158
|
-
const announce = inLive || announces(child, role);
|
|
159
|
-
const lean = ctx.mode === SnapshotMode.INTERACTIVE && !announce;
|
|
160
|
-
// A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
|
|
161
|
-
// so the actionable-only view stays lean while FULL/meaningful views see content regressions.
|
|
162
|
-
const text = !lean && 'generic' === role && 0 === name.length ? directText(child) : '';
|
|
163
|
-
// Layout signature for grid/flex containers — makes CLS/layout regressions visible.
|
|
164
|
-
const layout = lean ? '' : layoutSignature(style);
|
|
165
|
-
const meaningful = interactive || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
166
|
-
const include = lean ? interactive : meaningful;
|
|
167
|
-
if (include) {
|
|
168
|
-
ctx.nodes += 1;
|
|
169
|
-
ctx.lines.push(text.length > 0 && 0 === name.length && 0 === layout.length
|
|
170
|
-
? formatTextLine(depth, text)
|
|
171
|
-
: formatLine(child, depth, role, name, layout));
|
|
172
|
-
walk(child, depth + 1, ctx, announce);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
walk(child, depth, ctx, announce);
|
|
176
|
-
}
|
|
205
|
+
const child = children[index];
|
|
206
|
+
if (child !== undefined)
|
|
207
|
+
visit(child, depth, ctx, inLive);
|
|
177
208
|
}
|
|
178
209
|
}
|
|
179
210
|
function collectDialogs(root) {
|
|
@@ -259,12 +290,19 @@ export function buildSnapshot(options = {}) {
|
|
|
259
290
|
mode,
|
|
260
291
|
maxNodes: options.maxNodes ?? 400,
|
|
261
292
|
maxDepth: options.maxDepth ?? 20,
|
|
293
|
+
unread: [],
|
|
294
|
+
unreadOverflow: false,
|
|
262
295
|
};
|
|
263
|
-
|
|
296
|
+
if (true === options.includeRoot)
|
|
297
|
+
visit(root, 0, ctx, false);
|
|
298
|
+
else
|
|
299
|
+
walk(root, 0, ctx);
|
|
264
300
|
return {
|
|
265
301
|
tree: ctx.lines.join('\n'),
|
|
266
302
|
status,
|
|
267
303
|
nodes: ctx.nodes,
|
|
268
304
|
truncated: ctx.truncated,
|
|
305
|
+
...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
|
|
306
|
+
...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
|
|
269
307
|
};
|
|
270
308
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RefRegistry } from './dom/refs.js';
|
|
2
|
+
export declare class EditEpoch {
|
|
3
|
+
#private;
|
|
4
|
+
/** The registry is injected for the same reason the clock is: the interesting property is the
|
|
5
|
+
* RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
|
|
6
|
+
constructor(registry: RefRegistry);
|
|
7
|
+
/**
|
|
8
|
+
* How many hot updates this document has applied.
|
|
9
|
+
*
|
|
10
|
+
* `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
|
|
11
|
+
* have no channel that could report one, so absence here is UNKNOWN.
|
|
12
|
+
*/
|
|
13
|
+
get current(): number;
|
|
14
|
+
/** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
|
|
15
|
+
* no channel is the normal case, not an error. */
|
|
16
|
+
observe(hot: unknown): void;
|
|
17
|
+
/** Record that an update was applied, naming the files it changed (which may be none). */
|
|
18
|
+
applied(files: readonly string[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
|
|
21
|
+
*
|
|
22
|
+
* The generic wording is untouched and stays at the front: it is already tuned, and the server's
|
|
23
|
+
* recovery table matches on it. When the ref predates the last hot update the diagnosis is
|
|
24
|
+
* appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
|
|
25
|
+
* stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
|
|
26
|
+
* blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
|
|
27
|
+
* worse than the vague right one.
|
|
28
|
+
*/
|
|
29
|
+
staleRefMessage(ref: string): string;
|
|
30
|
+
}
|
|
31
|
+
/** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
|
|
32
|
+
export declare const editEpoch: EditEpoch;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NO_EDITS_OBSERVED } from '@reticlehq/core';
|
|
2
|
+
import { echoRef, refs } from './dom/refs.js';
|
|
3
|
+
/**
|
|
4
|
+
* What the page can tell us about its own source being edited underneath it.
|
|
5
|
+
*
|
|
6
|
+
* The agent driving Reticle edits source and re-verifies in a loop. When an edit lands, the dev
|
|
7
|
+
* server hot-updates the module and the framework re-renders: DOM nodes are REPLACED, every ref the
|
|
8
|
+
* agent holds points at a detached node — and there was no navigation, so the document is the same
|
|
9
|
+
* one and `documentId` never moves. The agent got a generic "that ref is stale" and could not tell
|
|
10
|
+
* its own edit landing from a bug in the app it was verifying.
|
|
11
|
+
*
|
|
12
|
+
* DELIBERATELY NOT A BUILD INTEGRATION. Nothing here watches files, reads the user's repo or knows
|
|
13
|
+
* what a module graph is. The page is told by its own dev server that it hot-updated; this records
|
|
14
|
+
* that it happened and what it named, and that is the whole feature.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Vite's "the update has been applied" event, on a hot context.
|
|
18
|
+
*
|
|
19
|
+
* `vite:afterUpdate` rather than `vite:beforeUpdate`: the point of the epoch is that the DOM has
|
|
20
|
+
* been replaced, and before the update it has not been yet.
|
|
21
|
+
*
|
|
22
|
+
* The SDK does NOT depend on Vite — it ships to Next, Electron, Tauri and plain pages, and it cannot
|
|
23
|
+
* read `import.meta.hot` of its own module in any of them (a dependency is pre-bundled by Vite's
|
|
24
|
+
* optimizer and gets no hot context at all). The channel is handed IN by whoever has one, and this
|
|
25
|
+
* name is the only Vite-shaped thing in the SDK.
|
|
26
|
+
*/
|
|
27
|
+
const HOT_UPDATE_APPLIED = 'vite:afterUpdate';
|
|
28
|
+
/** How many changed files a refusal names before it stops being a diagnosis and starts being a list. */
|
|
29
|
+
const MAX_NAMED_FILES = 3;
|
|
30
|
+
/** The exact substring the server's recovery table keys off. Changing it silently unclassifies. */
|
|
31
|
+
const STALE_REF_REFUSAL = 'no longer resolves to an element';
|
|
32
|
+
/** The clause that turns a dead end into a diagnosis, and the one the server's table matches on. */
|
|
33
|
+
const CODE_CHANGED = 'the code changed underneath it';
|
|
34
|
+
/** Anything object-shaped, as a bag of unknowns. The payload comes from the page, so nothing about
|
|
35
|
+
* its shape is guaranteed and every field has to be checked before it is read. */
|
|
36
|
+
function fields(value) {
|
|
37
|
+
return null !== value && 'object' === typeof value
|
|
38
|
+
? value
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
function isHotLike(value) {
|
|
42
|
+
return 'function' === typeof fields(value)?.['on'];
|
|
43
|
+
}
|
|
44
|
+
/** The file paths a hot-update payload named, narrowed from `unknown`. */
|
|
45
|
+
function updatedPaths(payload) {
|
|
46
|
+
const updates = fields(payload)?.['updates'];
|
|
47
|
+
if (!Array.isArray(updates))
|
|
48
|
+
return [];
|
|
49
|
+
const paths = [];
|
|
50
|
+
for (const update of updates) {
|
|
51
|
+
const path = fields(update)?.['path'];
|
|
52
|
+
if ('string' === typeof path && path.length > 0 && !paths.includes(path))
|
|
53
|
+
paths.push(path);
|
|
54
|
+
}
|
|
55
|
+
return paths;
|
|
56
|
+
}
|
|
57
|
+
export class EditEpoch {
|
|
58
|
+
#epoch = NO_EDITS_OBSERVED;
|
|
59
|
+
#files = [];
|
|
60
|
+
#refs;
|
|
61
|
+
/** The registry is injected for the same reason the clock is: the interesting property is the
|
|
62
|
+
* RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
|
|
63
|
+
constructor(registry) {
|
|
64
|
+
this.#refs = registry;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* How many hot updates this document has applied.
|
|
68
|
+
*
|
|
69
|
+
* `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
|
|
70
|
+
* have no channel that could report one, so absence here is UNKNOWN.
|
|
71
|
+
*/
|
|
72
|
+
get current() {
|
|
73
|
+
return this.#epoch;
|
|
74
|
+
}
|
|
75
|
+
/** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
|
|
76
|
+
* no channel is the normal case, not an error. */
|
|
77
|
+
observe(hot) {
|
|
78
|
+
if (!isHotLike(hot))
|
|
79
|
+
return;
|
|
80
|
+
hot.on(HOT_UPDATE_APPLIED, (payload) => this.applied(updatedPaths(payload)));
|
|
81
|
+
}
|
|
82
|
+
/** Record that an update was applied, naming the files it changed (which may be none). */
|
|
83
|
+
applied(files) {
|
|
84
|
+
this.#epoch += 1;
|
|
85
|
+
this.#files = files.slice(0, MAX_NAMED_FILES);
|
|
86
|
+
this.#refs.markEdited();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
|
|
90
|
+
*
|
|
91
|
+
* The generic wording is untouched and stays at the front: it is already tuned, and the server's
|
|
92
|
+
* recovery table matches on it. When the ref predates the last hot update the diagnosis is
|
|
93
|
+
* appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
|
|
94
|
+
* stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
|
|
95
|
+
* blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
|
|
96
|
+
* worse than the vague right one.
|
|
97
|
+
*/
|
|
98
|
+
staleRefMessage(ref) {
|
|
99
|
+
const generic = `ref '${echoRef(ref)}' ${STALE_REF_REFUSAL}`;
|
|
100
|
+
if (!this.#refs.mintedBeforeLastEdit(ref))
|
|
101
|
+
return generic;
|
|
102
|
+
const named = 0 === this.#files.length ? 'the page hot-updated' : `${this.#files.join(', ')} hot-updated`;
|
|
103
|
+
return (`${generic} — ${named} after this ref was taken, so ${CODE_CHANGED} and the framework ` +
|
|
104
|
+
'replaced the node. Query again for a fresh ref: this is your own edit landing, not the app failing.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
|
|
108
|
+
export const editEpoch = new EditEpoch(refs);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The page asked for ANOTHER browsing context — `window.open` — so the consequence of whatever was
|
|
4
|
+
* just clicked may live where this document's SDK cannot follow. An OAuth sign-in is the archetype:
|
|
5
|
+
* the POST succeeds, the popup carries the whole flow, and the original tab legitimately never
|
|
6
|
+
* changes (#508). Without this event the contradiction layer reads that shape as response-ignored,
|
|
7
|
+
* accusing the client of ignoring a response it handed to a window nobody can see.
|
|
8
|
+
*
|
|
9
|
+
* The wrap preserves the call exactly: same arguments, same return value, and a null/undefined
|
|
10
|
+
* first argument (the "open a blank tab" form) still emits, since a blank tab can navigate anywhere.
|
|
11
|
+
*/
|
|
12
|
+
export declare function installContextOpen(emit: Emit): Teardown;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EventType } from '@reticlehq/core';
|
|
2
|
+
import { captureMethod } from '../patching/capture-method.js';
|
|
3
|
+
/**
|
|
4
|
+
* The page asked for ANOTHER browsing context — `window.open` — so the consequence of whatever was
|
|
5
|
+
* just clicked may live where this document's SDK cannot follow. An OAuth sign-in is the archetype:
|
|
6
|
+
* the POST succeeds, the popup carries the whole flow, and the original tab legitimately never
|
|
7
|
+
* changes (#508). Without this event the contradiction layer reads that shape as response-ignored,
|
|
8
|
+
* accusing the client of ignoring a response it handed to a window nobody can see.
|
|
9
|
+
*
|
|
10
|
+
* The wrap preserves the call exactly: same arguments, same return value, and a null/undefined
|
|
11
|
+
* first argument (the "open a blank tab" form) still emits, since a blank tab can navigate anywhere.
|
|
12
|
+
*/
|
|
13
|
+
export function installContextOpen(emit) {
|
|
14
|
+
const originalOpen = captureMethod(window, 'open');
|
|
15
|
+
if (originalOpen === undefined)
|
|
16
|
+
return () => { };
|
|
17
|
+
const openPatch = function (href, target, features) {
|
|
18
|
+
emit(EventType.CONTEXT_OPENED, { ...(href === undefined ? {} : { href }) });
|
|
19
|
+
return originalOpen.call(this, href, target, features);
|
|
20
|
+
};
|
|
21
|
+
window.open = openPatch;
|
|
22
|
+
return () => {
|
|
23
|
+
window.open = originalOpen;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -17,6 +17,7 @@ import { installStoreState } from './state.js';
|
|
|
17
17
|
import { installFocus } from './focus.js';
|
|
18
18
|
import { installBlindSpots } from './blind-spots.js';
|
|
19
19
|
import { installDownload } from './download.js';
|
|
20
|
+
import { installContextOpen } from './context-open.js';
|
|
20
21
|
import { installNetwork } from './network.js';
|
|
21
22
|
import { installIpc, ipcNetOverrides, isReticleOwnIpc } from './ipc.js';
|
|
22
23
|
import { installPerf } from './perf.js';
|
|
@@ -62,5 +63,6 @@ export function installAllObservers(emit, options) {
|
|
|
62
63
|
guard(emit, SdkSite.NETWORK_OBSERVER, () => installDownload(emit, { capturePreview: options.captureBodies })),
|
|
63
64
|
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)), // cross-origin iframes the SDK can't see → BLIND_SPOT (coverage: partial)
|
|
64
65
|
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)), // page visibility/focus health + heartbeat
|
|
66
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installContextOpen(emit)), // window.open → CONTEXT_OPENED (the consequence may live in another context)
|
|
65
67
|
];
|
|
66
68
|
}
|
|
@@ -256,6 +256,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
256
256
|
const contentType = res.headers.get('content-type');
|
|
257
257
|
// The request is done; the BODY may not be. Watch it so settle cannot pass mid-stream.
|
|
258
258
|
watchStreamedBody(emit, res, id, url, contentType, res.headers.get('content-length'));
|
|
259
|
+
reportedNetUrls.add(rawUrl);
|
|
259
260
|
const emitRequest = (responseBodyFields) => {
|
|
260
261
|
emit(EventType.NET_REQUEST, {
|
|
261
262
|
id,
|
|
@@ -385,6 +386,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
385
386
|
const cur = meta.get(this);
|
|
386
387
|
if (cur === undefined)
|
|
387
388
|
return;
|
|
389
|
+
reportedNetUrls.add(cur.rawUrl);
|
|
388
390
|
const xhrContentType = this.getResponseHeader('content-type');
|
|
389
391
|
let responseBodyFields = {};
|
|
390
392
|
// responseText throws unless responseType is '' or 'text' — guard before reading.
|
|
@@ -523,7 +525,64 @@ export function installNetwork(emit, opts = {}) {
|
|
|
523
525
|
};
|
|
524
526
|
navProto.sendBeacon = patchedBeacon;
|
|
525
527
|
}
|
|
528
|
+
// Document-initiated subresources (link/css/img/script/manifest): fetch and XHR patches never see
|
|
529
|
+
// these, so a `{net}` predicate over a favicon or manifest used to read `assertion_failed` — "your
|
|
530
|
+
// change is broken" — when the truthful answer is "not observable". A PerformanceObserver over
|
|
531
|
+
// `resource` entries reports them with no CDP. Status is the known gap: entries carry one only on
|
|
532
|
+
// newer Chromium, so the field is emitted ONLY when readable and the evaluation seam downgrades
|
|
533
|
+
// status assertions it cannot verify instead of guessing.
|
|
534
|
+
//
|
|
535
|
+
// Dedup: any URL the patched transports already reported (or later report) is skipped — resource
|
|
536
|
+
// timing also records fetch/XHR loads. The seen-set is filled eagerly here AND at every
|
|
537
|
+
// NET_REQUEST emit below, so both orders of arrival stay single-reported.
|
|
538
|
+
const reportedNetUrls = new Set();
|
|
539
|
+
let subresourceEvents = 0;
|
|
540
|
+
const SUBRESOURCE_CAP = 200;
|
|
541
|
+
let subresourceObserver;
|
|
542
|
+
if (typeof PerformanceObserver !== 'undefined') {
|
|
543
|
+
try {
|
|
544
|
+
const observer = new PerformanceObserver((list) => {
|
|
545
|
+
for (const raw of list.getEntries()) {
|
|
546
|
+
const entry = raw;
|
|
547
|
+
if (subresourceEvents >= SUBRESOURCE_CAP)
|
|
548
|
+
return;
|
|
549
|
+
const type = entry.initiatorType || 'other';
|
|
550
|
+
// Patched transports own their records; only document-initiated types are new here.
|
|
551
|
+
if ('fetch' === type || 'xmlhttprequest' === type)
|
|
552
|
+
continue;
|
|
553
|
+
const rawUrl = entry.name;
|
|
554
|
+
if (reportedNetUrls.has(rawUrl))
|
|
555
|
+
continue;
|
|
556
|
+
reportedNetUrls.add(rawUrl);
|
|
557
|
+
subresourceEvents += 1;
|
|
558
|
+
emit(EventType.NET_REQUEST, {
|
|
559
|
+
id: nextId(),
|
|
560
|
+
method: 'GET',
|
|
561
|
+
url: redactUrl(rawUrl),
|
|
562
|
+
durationMs: Math.round(entry.duration),
|
|
563
|
+
initiator: type,
|
|
564
|
+
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
565
|
+
// responseStatus is Chromium-only; omitted entirely when unreadable so the wire never
|
|
566
|
+
// carries a guessed status. Its absence is what the server-side seam reads as unknown.
|
|
567
|
+
...((entry.responseStatus ?? 0) > 0
|
|
568
|
+
? {
|
|
569
|
+
status: entry.responseStatus,
|
|
570
|
+
ok: (entry.responseStatus ?? 0) >= 200 && (entry.responseStatus ?? 0) < 400,
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
subresourceObserver = observer;
|
|
577
|
+
observer.observe({ type: 'resource', buffered: true });
|
|
578
|
+
// Disconnected in the disposer below.
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// No observer support: document-initiated loads stay unobserved, stated rather than guessed.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
526
584
|
return () => {
|
|
585
|
+
subresourceObserver?.disconnect();
|
|
527
586
|
// Restore each slot ONLY if it still holds OUR wrapper. Between connect() and disconnect() the app
|
|
528
587
|
// (or Sentry/analytics/a router) may have wrapped fetch/XHR/EventSource/WebSocket/sendBeacon ON TOP
|
|
529
588
|
// of ours; blindly writing the original back would silently uninstall their instrumentation — the
|
|
@@ -65,7 +65,7 @@ export function readStorage(area) {
|
|
|
65
65
|
return readArea(safeArea(() => window.localStorage));
|
|
66
66
|
if (area === StorageArea.SESSION)
|
|
67
67
|
return readArea(safeArea(() => window.sessionStorage));
|
|
68
|
-
if (
|
|
68
|
+
if (StorageArea.COOKIE === area)
|
|
69
69
|
return readCookies();
|
|
70
70
|
return {
|
|
71
71
|
local: readArea(safeArea(() => window.localStorage)),
|
|
@@ -9,28 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
|
|
12
|
-
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the mark
|
|
13
|
+
* lives here as a string (same precedent as SEND_ICON in presenter-controls.ts).
|
|
14
|
+
* Minified - editor metadata, clip paths, masks and sub-0.01 precision stripped - and painted
|
|
15
15
|
* with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
|
|
16
16
|
*/
|
|
17
|
-
/** Accessible name of the HUD
|
|
17
|
+
/** Accessible name of the HUD. */
|
|
18
18
|
export declare const BRAND_NAME = "Reticle";
|
|
19
19
|
/** Mark alone (shown in the FAB and compact toolbar). */
|
|
20
20
|
export declare const MARK_SVG: string;
|
|
21
|
-
/** FAB-sized mark for the collapsed circular control. */
|
|
22
|
-
export declare const FAB_MARK_SVG: string;
|
|
23
|
-
/** Mark + "reticle" lettering (legacy wordmark artwork). */
|
|
24
|
-
export declare const WORDMARK_SVG: string;
|
|
25
|
-
/** Compact text label beside the mark in the expanded toolbar. */
|
|
26
|
-
export declare const BRAND_MINI_CLASS = "reticle-brand-mini";
|
|
27
|
-
/** Toolbar brand: mark + Reticle word (always shown together when expanded). */
|
|
28
|
-
export declare const TOOLBAR_BRAND_HTML: string;
|
|
29
|
-
/**
|
|
30
|
-
* Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
|
|
31
|
-
* attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
|
|
32
|
-
* the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
|
|
33
|
-
*/
|
|
34
|
-
export declare const BRAND_HTML: string;
|
|
35
21
|
/** Collapsed FAB control - Reticle mark only. */
|
|
36
22
|
export declare const FAB_TOGGLE_HTML: string;
|
|
@@ -9,30 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
|
|
12
|
-
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the mark
|
|
13
|
+
* lives here as a string (same precedent as SEND_ICON in presenter-controls.ts).
|
|
14
|
+
* Minified - editor metadata, clip paths, masks and sub-0.01 precision stripped - and painted
|
|
15
15
|
* with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
|
|
16
16
|
*/
|
|
17
|
-
/** Accessible name of the HUD
|
|
17
|
+
/** Accessible name of the HUD. */
|
|
18
18
|
export const BRAND_NAME = 'Reticle';
|
|
19
|
-
/** The two glyph paths of the mark
|
|
19
|
+
/** The two glyph paths of the mark. */
|
|
20
20
|
const MARK_PATHS = `<path d="M92.82 92.16C93.07 93.4 91.77 94.4 90.63 93.83C89.62 93.33 88.62 92.82 87.63 92.3C71.14 83.78 54.79 72.27 39.61 57.96C39.09 57.49 38.57 56.98 38.06 56.49C22.45 41.52 9.78 25.14 .33 8.52C0.29 8.45 .25 8.38 .2 8.31C-0.44 7.2 .51 5.84 1.79 6.05L24.07 9.71C43.06 12.83 60.63 11.75 75.55 6.53L93.95 .09C95.15-0.33 96.31 .78 95.93 1.98L90.1 20.49C85.37 35.48 84.9 52.97 88.69 71.73L92.83 92.16H92.82Z"/><path d="M58.72 89.23L6.15 92.8C5.24 92.86 4.48 92.13 4.51 91.22L6.56 30.39C6.61 28.91 8.51 28.36 9.35 29.57C16.23 39.43 24.98 52.35 33.01 61.26C41.05 70.17 50.6 78.72 59.62 86.56C60.65 87.45 60.08 89.13 58.72 89.23Z"/>`;
|
|
21
21
|
/** Mark alone (shown in the FAB and compact toolbar). */
|
|
22
22
|
export const MARK_SVG = `<svg class="reticle-mark" viewBox="0 0 96 94" fill="currentColor" aria-hidden="true">${MARK_PATHS}</svg>`;
|
|
23
23
|
/** FAB-sized mark for the collapsed circular control. */
|
|
24
|
-
|
|
25
|
-
/** Mark + "reticle" lettering (legacy wordmark artwork). */
|
|
26
|
-
export const WORDMARK_SVG = `<svg class="reticle-wordmark" viewBox="0 0 347 96" fill="currentColor" aria-hidden="true"><g transform="translate(1 1)">${MARK_PATHS}</g><path d="M116.43 78V27.09H136.51C140.35 27.09 143.64 27.78 146.35 29.15C149.09 30.51 151.17 32.44 152.59 34.95C154.04 37.43 154.76 40.36 154.76 43.72C154.76 47.1 154.03 50.01 152.57 52.45C151.11 54.87 149 56.72 146.23 58.01C143.48 59.31 140.15 59.95 136.24 59.95H122.79V51.3H134.5C136.55 51.3 138.26 51.02 139.62 50.46C140.98 49.89 141.99 49.05 142.65 47.92C143.33 46.8 143.67 45.39 143.67 43.72C143.67 42.03 143.33 40.61 142.65 39.45C141.99 38.29 140.97 37.41 139.59 36.81C138.23 36.2 136.52 35.89 134.45 35.89H127.19V78H116.43ZM143.92 54.83L156.57 78H144.69L132.31 54.83H143.92ZM178.95 78.75C175.02 78.75 171.64 77.95 168.81 76.36C165.99 74.75 163.82 72.48 162.29 69.55C160.77 66.6 160.01 63.11 160.01 59.08C160.01 55.16 160.77 51.71 162.29 48.74C163.82 45.78 165.97 43.46 168.73 41.81C171.52 40.15 174.78 39.32 178.53 39.32C181.05 39.32 183.39 39.73 185.56 40.54C187.75 41.33 189.65 42.54 191.28 44.14C192.92 45.75 194.2 47.77 195.11 50.21C196.02 52.63 196.47 55.46 196.47 58.71V61.62H164.23V55.06H186.51C186.51 53.53 186.17 52.18 185.51 51C184.85 49.83 183.93 48.91 182.75 48.25C181.59 47.57 180.24 47.23 178.7 47.23C177.09 47.23 175.67 47.6 174.43 48.34C173.2 49.07 172.24 50.06 171.54 51.3C170.85 52.53 170.49 53.9 170.47 55.4V61.64C170.47 63.53 170.82 65.17 171.52 66.54C172.23 67.92 173.23 68.98 174.52 69.72C175.82 70.47 177.35 70.84 179.12 70.84C180.3 70.84 181.38 70.68 182.35 70.34C183.33 70.01 184.17 69.52 184.87 68.85C185.56 68.19 186.09 67.38 186.46 66.42L196.25 67.06C195.75 69.42 194.73 71.47 193.19 73.23C191.67 74.97 189.7 76.33 187.28 77.3C184.87 78.27 182.1 78.75 178.95 78.75ZM223.69 39.82V47.77H200.7V39.82H223.69ZM205.92 30.67H216.51V66.27C216.51 67.24 216.66 68.01 216.96 68.55C217.26 69.08 217.67 69.46 218.2 69.67C218.75 69.89 219.38 70 220.09 70C220.59 70 221.08 69.95 221.58 69.87C222.08 69.77 222.46 69.7 222.72 69.65L224.39 77.53C223.86 77.69 223.11 77.88 222.15 78.1C221.19 78.33 220.02 78.47 218.65 78.52C216.1 78.62 213.86 78.28 211.94 77.5C210.03 76.72 208.55 75.51 207.49 73.87C206.43 72.23 205.9 70.16 205.92 67.66V30.67ZM230.59 78V39.82H241.18V78H230.59ZM235.91 34.9C234.34 34.9 232.99 34.37 231.86 33.33C230.75 32.27 230.19 31 230.19 29.53C230.19 28.07 230.75 26.82 231.86 25.77C232.99 24.71 234.34 24.18 235.91 24.18C237.49 24.18 238.83 24.71 239.94 25.77C241.07 26.82 241.63 28.07 241.63 29.53C241.63 31 241.07 32.27 239.94 33.33C238.83 34.37 237.49 34.9 235.91 34.9ZM266.92 78.75C263 78.75 259.64 77.92 256.82 76.26C254.02 74.59 251.87 72.27 250.36 69.3C248.87 66.33 248.12 62.92 248.12 59.06C248.12 55.15 248.88 51.72 250.38 48.77C251.91 45.8 254.07 43.49 256.87 41.83C259.67 40.16 263 39.32 266.87 39.32C270.2 39.32 273.11 39.93 275.62 41.14C278.12 42.35 280.1 44.04 281.56 46.23C283.01 48.42 283.82 50.99 283.97 53.94H273.97C273.69 52.03 272.95 50.5 271.74 49.34C270.54 48.16 268.98 47.57 267.04 47.57C265.4 47.57 263.96 48.02 262.74 48.92C261.53 49.79 260.58 51.08 259.9 52.77C259.23 54.46 258.89 56.51 258.89 58.91C258.89 61.35 259.22 63.42 259.88 65.12C260.56 66.83 261.51 68.13 262.74 69.03C263.96 69.92 265.4 70.37 267.04 70.37C268.25 70.37 269.33 70.12 270.3 69.62C271.27 69.13 272.08 68.4 272.71 67.46C273.35 66.5 273.78 65.35 273.97 64.01H283.97C283.8 66.92 283.01 69.49 281.58 71.71C280.17 73.92 278.23 75.64 275.74 76.88C273.25 78.12 270.31 78.75 266.92 78.75ZM301.34 27.09V78H290.75V27.09H301.34ZM327.22 78.75C323.29 78.75 319.91 77.95 317.08 76.36C314.26 74.75 312.09 72.48 310.57 69.55C309.04 66.6 308.28 63.11 308.28 59.08C308.28 55.16 309.04 51.71 310.57 48.74C312.09 45.78 314.24 43.46 317 41.81C319.79 40.15 323.05 39.32 326.8 39.32C329.32 39.32 331.66 39.73 333.83 40.54C336.02 41.33 337.93 42.54 339.55 44.14C341.19 45.75 342.47 47.77 343.38 50.21C344.29 52.63 344.75 55.46 344.75 58.71V61.62H312.5V55.06H334.78C334.78 53.53 334.45 52.18 333.78 51C333.12 49.83 332.2 48.91 331.02 48.25C329.86 47.57 328.51 47.23 326.97 47.23C325.36 47.23 323.94 47.6 322.7 48.34C321.47 49.07 320.51 50.06 319.81 51.3C319.12 52.53 318.76 53.9 318.74 55.4V61.64C318.74 63.53 319.09 65.17 319.79 66.54C320.5 67.92 321.5 68.98 322.8 69.72C324.09 70.47 325.62 70.84 327.39 70.84C328.57 70.84 329.65 70.68 330.63 70.34C331.6 70.01 332.44 69.52 333.14 68.85C333.83 68.19 334.36 67.38 334.73 66.42L344.52 67.06C344.02 69.42 343.01 71.47 341.46 73.23C339.94 74.97 337.97 76.33 335.55 77.3C333.15 78.27 330.37 78.75 327.22 78.75Z"/></svg>`;
|
|
27
|
-
/** Compact text label beside the mark in the expanded toolbar. */
|
|
28
|
-
export const BRAND_MINI_CLASS = 'reticle-brand-mini';
|
|
29
|
-
/** Toolbar brand: mark + Reticle word (always shown together when expanded). */
|
|
30
|
-
export const TOOLBAR_BRAND_HTML = `<span class="reticle-brand reticle-brand--toolbar">${MARK_SVG}<span class="${BRAND_MINI_CLASS}">${BRAND_NAME}</span><span class="reticle-brand-sr">${BRAND_NAME}</span></span>`;
|
|
31
|
-
/**
|
|
32
|
-
* Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
|
|
33
|
-
* attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
|
|
34
|
-
* the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
|
|
35
|
-
*/
|
|
36
|
-
export const BRAND_HTML = `<span class="reticle-brand">${MARK_SVG}${WORDMARK_SVG}<span class="${BRAND_MINI_CLASS}">${BRAND_NAME}</span><span class="reticle-brand-sr">${BRAND_NAME}</span></span>`;
|
|
24
|
+
const FAB_MARK_SVG = `<svg class="reticle-fab-mark" viewBox="0 0 96 94" fill="currentColor" aria-hidden="true">${MARK_PATHS}</svg>`;
|
|
37
25
|
/** Collapsed FAB control - Reticle mark only. */
|
|
38
26
|
export const FAB_TOGGLE_HTML = `<button type="button" class="reticle-fab" data-reticle-fab aria-label="Start ${BRAND_NAME}" aria-expanded="false">${FAB_MARK_SVG}<span class="reticle-fab-pulse" aria-hidden="true"></span><span data-reticle-mark-count class="reticle-fab-badge" hidden>0</span></button>`;
|
|
@@ -126,10 +126,6 @@ export declare const Placement: {
|
|
|
126
126
|
readonly ABOVE: "above";
|
|
127
127
|
readonly BELOW: "below";
|
|
128
128
|
};
|
|
129
|
-
export declare const DockAlign: {
|
|
130
|
-
readonly START: "start";
|
|
131
|
-
readonly END: "end";
|
|
132
|
-
};
|
|
133
129
|
/** Show/hide annotation markers. */
|
|
134
130
|
export declare const MARKERS_BTN_ATTR = "data-reticle-markers-btn";
|
|
135
131
|
/** Clear every annotation on the page. */
|
|
@@ -177,8 +173,6 @@ export declare const LOG_TIMESTAMPS_ATTR = "data-reticle-log-ts";
|
|
|
177
173
|
/** When 1, the chat panel uses a narrower width. */
|
|
178
174
|
/** When 1, HUD motion is reduced for accessibility. */
|
|
179
175
|
export declare const REDUCE_MOTION_ATTR = "data-reticle-reduce-motion";
|
|
180
|
-
/** Presenter settings card version label (browser package). */
|
|
181
|
-
export declare const PRESENTER_UI_VERSION = "2.7.0";
|
|
182
176
|
/** Docs opened from the settings footer. */
|
|
183
177
|
export declare const MCP_DOCS_URL = "https://github.com/reticlehq/reticle/blob/main/docs/getting-started.md";
|
|
184
178
|
/** HUD has been dragged off the default dock - positioned with explicit left/top. */
|