@vitrinka/web 0.1.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +93 -0
  3. package/README.md +177 -0
  4. package/build/index.d.ts +11 -0
  5. package/build/index.js +1 -0
  6. package/build/next.d.ts +23 -0
  7. package/build/next.js +27 -0
  8. package/build/protocol/index.d.ts +74 -0
  9. package/build/protocol/index.js +22 -0
  10. package/build/recorder/RecorderProvider.d.ts +17 -0
  11. package/build/recorder/RecorderProvider.js +126 -0
  12. package/build/recorder/api-status.d.ts +6 -0
  13. package/build/recorder/api-status.js +6 -0
  14. package/build/recorder/api.d.ts +40 -0
  15. package/build/recorder/api.js +84 -0
  16. package/build/recorder/capture/click.d.ts +17 -0
  17. package/build/recorder/capture/click.js +77 -0
  18. package/build/recorder/capture/console.d.ts +3 -0
  19. package/build/recorder/capture/console.js +89 -0
  20. package/build/recorder/capture/nav.d.ts +8 -0
  21. package/build/recorder/capture/nav.js +54 -0
  22. package/build/recorder/capture/net.d.ts +22 -0
  23. package/build/recorder/capture/net.js +506 -0
  24. package/build/recorder/capture/redact.d.ts +38 -0
  25. package/build/recorder/capture/redact.js +54 -0
  26. package/build/recorder/capture/rrweb.d.ts +10 -0
  27. package/build/recorder/capture/rrweb.js +76 -0
  28. package/build/recorder/config.d.ts +50 -0
  29. package/build/recorder/config.js +100 -0
  30. package/build/recorder/control.d.ts +29 -0
  31. package/build/recorder/control.js +63 -0
  32. package/build/recorder/hud/AnnotateOverlay.d.ts +25 -0
  33. package/build/recorder/hud/AnnotateOverlay.js +122 -0
  34. package/build/recorder/hud/Hud.d.ts +12 -0
  35. package/build/recorder/hud/Hud.js +190 -0
  36. package/build/recorder/hud/LinkSheet.d.ts +26 -0
  37. package/build/recorder/hud/LinkSheet.js +15 -0
  38. package/build/recorder/hud/RecorderPill.d.ts +36 -0
  39. package/build/recorder/hud/RecorderPill.js +73 -0
  40. package/build/recorder/hud/Sheet.d.ts +20 -0
  41. package/build/recorder/hud/Sheet.js +36 -0
  42. package/build/recorder/hud/host.d.ts +27 -0
  43. package/build/recorder/hud/host.js +170 -0
  44. package/build/recorder/hud/icons.d.ts +15 -0
  45. package/build/recorder/hud/icons.js +40 -0
  46. package/build/recorder/hud/styles.d.ts +13 -0
  47. package/build/recorder/hud/styles.js +111 -0
  48. package/build/recorder/index.d.ts +46 -0
  49. package/build/recorder/index.js +61 -0
  50. package/build/recorder/link.d.ts +18 -0
  51. package/build/recorder/link.js +37 -0
  52. package/build/recorder/queue.d.ts +163 -0
  53. package/build/recorder/queue.js +642 -0
  54. package/build/recorder/session.d.ts +73 -0
  55. package/build/recorder/session.js +246 -0
  56. package/build/recorder/state.d.ts +26 -0
  57. package/build/recorder/state.js +42 -0
  58. package/build/recorder/storage/index.d.ts +35 -0
  59. package/build/recorder/storage/index.js +69 -0
  60. package/build/recorder/storage/memory.d.ts +2 -0
  61. package/build/recorder/storage/memory.js +2 -0
  62. package/package.json +77 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The pill — the extension's HUD: rec dot · timer · name · sync glyph ·
3
+ * ⏸ ✎ ⌖ with keycaps on hover (⌥⇧P / ⌥⇧N / ⌥⇧A on Apple platforms, Alt⇧
4
+ * elsewhere), a ⋯ menu (Open board · Stop), and a one-line health detail
5
+ * that unfolds only when something is wrong. Idle (no session): a quiet grip
6
+ * that starts a recording.
7
+ */
8
+ import { type ReactElement } from 'react';
9
+ import { type RecorderHealth, type SessionState } from '../queue';
10
+ export declare const MOD: string;
11
+ /** The extension's renderHealth: glyph + class + optional detail line. */
12
+ export declare function healthLine(h: RecorderHealth, stopping: boolean): {
13
+ glyph: ReactElement | string;
14
+ cls: string;
15
+ line: string;
16
+ bad: boolean;
17
+ };
18
+ export interface RecorderPillProps {
19
+ rec: SessionState | null;
20
+ composing: boolean;
21
+ annotating: boolean;
22
+ stopping: boolean;
23
+ starting: boolean;
24
+ /** Something to authenticate with (an explicit key or a stored link). */
25
+ linked: boolean;
26
+ /** A stored link can be forgotten; an explicit key cannot. */
27
+ canUnlink: boolean;
28
+ onLink: () => void;
29
+ onUnlink: () => void;
30
+ onStart: () => void;
31
+ onPause: () => void;
32
+ onNote: () => void;
33
+ onAnnotate: () => void;
34
+ onStop: () => void;
35
+ }
36
+ export declare function RecorderPill(p: RecorderPillProps): ReactElement;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The pill — the extension's HUD: rec dot · timer · name · sync glyph ·
4
+ * ⏸ ✎ ⌖ with keycaps on hover (⌥⇧P / ⌥⇧N / ⌥⇧A on Apple platforms, Alt⇧
5
+ * elsewhere), a ⋯ menu (Open board · Stop), and a one-line health detail
6
+ * that unfolds only when something is wrong. Idle (no session): a quiet grip
7
+ * that starts a recording.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { health } from '../queue';
11
+ import { elapsedOf } from '../session';
12
+ import { AnnotateIcon, CheckIcon, CloseIcon, MoreIcon, NewTabIcon, PauseIcon, PencilIcon, PlayIcon, StopIcon } from './icons';
13
+ export const MOD = /Mac|iPhone|iPad/.test(globalThis.navigator?.platform ?? '') ? '⌥⇧' : 'Alt⇧';
14
+ function fmtAge(ms) {
15
+ return ms < 1000 ? 'just now' : ms < 60000 ? `${Math.round(ms / 1000)}s` : `${Math.round(ms / 60000)}m`;
16
+ }
17
+ function fmtClock(ms) {
18
+ const s = Math.max(0, Math.floor(ms / 1000));
19
+ return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
20
+ }
21
+ /** The extension's renderHealth: glyph + class + optional detail line. */
22
+ export function healthLine(h, stopping) {
23
+ if (stopping)
24
+ return { glyph: '⟳', cls: 'busy', line: `wrapping up · ${h.queued} left`, bad: false };
25
+ switch (h.state) {
26
+ case 'offline':
27
+ return {
28
+ glyph: '⚠',
29
+ cls: 'bad',
30
+ line: `offline${h.sinceSyncMs ? ' ' + fmtAge(h.sinceSyncMs) : ''} · ${h.queued} held · retrying`,
31
+ bad: true,
32
+ };
33
+ case 'dead':
34
+ return { glyph: '⛔', cls: 'bad', line: h.deadReason || 'this session ended on the server', bad: true };
35
+ case 'backlog':
36
+ return { glyph: '⟳', cls: 'busy', line: `syncing · ${h.queued} queued`, bad: false };
37
+ default:
38
+ return { glyph: h.synced ? _jsx(CheckIcon, {}) : '·', cls: '', line: '', bad: false };
39
+ }
40
+ }
41
+ export function RecorderPill(p) {
42
+ const [, tick] = useState(0);
43
+ const [menu, setMenu] = useState(false);
44
+ useEffect(() => {
45
+ const t = setInterval(() => tick((n) => n + 1), 1000);
46
+ return () => clearInterval(t);
47
+ }, []);
48
+ useEffect(() => {
49
+ if (!p.rec)
50
+ setMenu(false);
51
+ }, [p.rec]);
52
+ if (!p.rec && !p.linked) {
53
+ return (_jsx("div", { className: "stack", children: _jsxs("button", { type: "button", className: "grip link", "aria-label": "Link recorder", onClick: p.onLink, children: [_jsx("span", { className: "dot" }), _jsx("span", { children: "Link recorder" })] }) }));
54
+ }
55
+ if (!p.rec) {
56
+ return (_jsx("div", { className: "stack", children: _jsx("button", { type: "button", className: p.starting ? 'grip busy' : 'grip', "aria-label": "Start recording", title: "Start a vitrinka recording", onClick: p.onStart, disabled: p.starting, children: _jsx("span", { className: "dot" }) }) }));
57
+ }
58
+ const rec = p.rec;
59
+ const h = health();
60
+ const hl = healthLine(h, p.stopping);
61
+ const paused = rec.paused || rec.dead || p.stopping;
62
+ const cls = ['pill', paused ? 'paused' : '', p.composing ? 'composing' : ''].filter(Boolean).join(' ');
63
+ return (_jsxs("div", { className: "stack", children: [_jsxs("div", { className: cls, "data-e2e": "recorder-pill", children: [_jsx("span", { className: "dot" }), _jsx("span", { className: "time", children: fmtClock(elapsedOf(rec)) }), _jsx("span", { className: "name", children: rec.title || `${rec.project} · ${rec.environment}` }), _jsx("span", { className: `sync${hl.cls ? ' ' + hl.cls : ''}`, title: "everything captured has reached vitrinka", children: hl.glyph }), _jsxs("button", { type: "button", className: "b-pause", "aria-label": rec.paused ? 'Resume' : 'Pause', onClick: p.onPause, children: [rec.paused ? _jsx(PlayIcon, {}) : _jsx(PauseIcon, {}), _jsxs("kbd", { children: [MOD, "P ", rec.paused ? 'resume' : 'pause'] })] }), _jsxs("button", { type: "button", className: "b-note", "aria-label": "Note", onClick: p.onNote, children: [_jsx(PencilIcon, {}), _jsxs("kbd", { children: [MOD, "N note"] })] }), _jsxs("button", { type: "button", className: p.annotating ? 'snap b-snap on' : 'snap b-snap', "aria-label": "Annotate", "aria-pressed": p.annotating, onClick: p.onAnnotate, children: [_jsx(AnnotateIcon, {}), _jsxs("kbd", { children: [MOD, "A annotate"] })] }), _jsxs("span", { className: "menuwrap", children: [_jsx("button", { type: "button", className: "b-more", "aria-label": "More", "aria-expanded": menu, onClick: () => setMenu((m) => !m), children: _jsx(MoreIcon, {}) }), menu ? (_jsxs("div", { className: "menu", role: "menu", children: [rec.boardUrl ? (_jsxs("button", { type: "button", role: "menuitem", onClick: () => {
64
+ setMenu(false);
65
+ window.open(rec.boardUrl, '_blank', 'noopener');
66
+ }, children: [_jsx(NewTabIcon, {}), " Open board"] })) : null, _jsxs("button", { type: "button", role: "menuitem", className: "danger", onClick: () => {
67
+ setMenu(false);
68
+ p.onStop();
69
+ }, children: [_jsx(StopIcon, {}), " Stop recording"] }), p.canUnlink ? (_jsxs("button", { type: "button", role: "menuitem", onClick: () => {
70
+ setMenu(false);
71
+ p.onUnlink();
72
+ }, children: [_jsx(CloseIcon, {}), " Unlink"] })) : null] })) : null] })] }), _jsx("div", { className: ['detail', hl.line ? 'show' : '', hl.bad ? 'bad' : ''].filter(Boolean).join(' '), children: hl.line })] }));
73
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The composer sheet — the extension's hud.html, as a component: 360px, a
3
+ * 16px textarea, ✕ in the kicker, a three-column footer (board|task ·
4
+ * context · ↑ Send) and the hint line. Enter sends, ⇧Enter newlines, Esc
5
+ * cancels. The DRAFT is owned by the caller so a cancel keeps it until the
6
+ * next send (recorder-hud-polish D3); the board|task choice resets on every
7
+ * open — a destination is per observation, not a mode.
8
+ */
9
+ import { type ReactElement } from 'react';
10
+ export interface SheetProps {
11
+ title: string;
12
+ ctx: string;
13
+ /** True when the sheet composes an annotation (shows board|task). */
14
+ pick: boolean;
15
+ draft: string;
16
+ onDraft: (text: string) => void;
17
+ onSend: (text: string, task: boolean) => void;
18
+ onClose: () => void;
19
+ }
20
+ export declare function Sheet({ title, ctx, pick, draft, onDraft, onSend, onClose }: SheetProps): ReactElement;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The composer sheet — the extension's hud.html, as a component: 360px, a
4
+ * 16px textarea, ✕ in the kicker, a three-column footer (board|task ·
5
+ * context · ↑ Send) and the hint line. Enter sends, ⇧Enter newlines, Esc
6
+ * cancels. The DRAFT is owned by the caller so a cancel keeps it until the
7
+ * next send (recorder-hud-polish D3); the board|task choice resets on every
8
+ * open — a destination is per observation, not a mode.
9
+ */
10
+ import { useEffect, useRef, useState } from 'react';
11
+ import { ArrowUpIcon, CloseIcon } from './icons';
12
+ export function Sheet({ title, ctx, pick, draft, onDraft, onSend, onClose }) {
13
+ const ta = useRef(null);
14
+ const [task, setTask] = useState(false);
15
+ useEffect(() => {
16
+ const el = ta.current;
17
+ if (!el)
18
+ return;
19
+ el.focus();
20
+ el.setSelectionRange(el.value.length, el.value.length);
21
+ }, []);
22
+ const send = () => onSend(draft.trim(), task);
23
+ const onKey = (e) => {
24
+ if (e.key === 'Escape') {
25
+ e.preventDefault();
26
+ onClose();
27
+ }
28
+ };
29
+ const onTaKey = (e) => {
30
+ if (e.key === 'Enter' && !e.shiftKey) {
31
+ e.preventDefault();
32
+ send();
33
+ }
34
+ };
35
+ return (_jsxs("div", { className: "pop", role: "dialog", "aria-labelledby": "vt-pop-title", onKeyDown: onKey, children: [_jsxs("div", { className: "pop-head", children: [_jsxs("label", { children: [_jsx("i", {}), _jsx("span", { id: "vt-pop-title", children: title })] }), _jsx("button", { type: "button", className: "closeb", "aria-label": "Close", onClick: onClose, children: _jsx(CloseIcon, {}) })] }), _jsx("textarea", { ref: ta, placeholder: "what's wrong / what to refine\u2026", value: draft, onChange: (e) => onDraft(e.target.value), onKeyDown: onTaKey }), _jsxs("div", { className: pick ? 'row' : 'row nodest', children: [pick ? (_jsxs("span", { className: "dest", role: "group", "aria-label": "Where this snap lands", children: [_jsx("button", { type: "button", "aria-pressed": !task, onClick: () => setTask(false), children: "board" }), _jsx("button", { type: "button", "aria-pressed": task, onClick: () => setTask(true), children: "task" })] })) : null, _jsx("span", { className: "ctx", children: ctx }), _jsxs("button", { type: "button", className: "sendb", onClick: send, children: [_jsx(ArrowUpIcon, {}), _jsx("span", { children: "Send" })] })] }), _jsx("div", { className: "hints", children: "\u21A9 send \u00B7 \u21E7\u21A9 newline \u00B7 esc cancel" })] }));
36
+ }
@@ -0,0 +1,27 @@
1
+ export declare function shield(el: HTMLElement): void;
2
+ export interface HudHost {
3
+ host: HTMLElement;
4
+ /** The React mount point inside the shadow root. */
5
+ mount: HTMLElement;
6
+ /** True while a modal/dialog is open — the sheet portals into it. */
7
+ destroy: () => void;
8
+ }
9
+ export declare function createHudHost(): HudHost;
10
+ /**
11
+ * Where the composer sheet portals (D4): the topmost open `<dialog>` (last in
12
+ * document order), else the last open ARIA dialog (`[role="dialog"]
13
+ * [data-state="open"]` — Radix and friends), else null = the HUD host itself.
14
+ * Inside the dialog's subtree a focus trap and a modal's inert both leave
15
+ * the sheet alone.
16
+ */
17
+ export declare function sheetTarget(): Element | null;
18
+ /**
19
+ * A sheet host: its own open shadow root (so the HUD stylesheet applies
20
+ * inside a foreign dialog too), shielded, appended to `target`.
21
+ */
22
+ export declare function createSheetHost(target: Element): {
23
+ mount: HTMLElement;
24
+ destroy: () => void;
25
+ };
26
+ /** Is `t` inside any HUD surface (host or a portaled sheet)? */
27
+ export declare function insideHud(t: EventTarget | null): boolean;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The HUD host (D4): one element appended to `document.documentElement`,
3
+ * carrying an OPEN shadow root the React HUD renders into. `popover="manual"`
4
+ * where supported so it paints in the top layer above every page z-index;
5
+ * a MutationObserver re-raises it whenever a dialog opens or closes, so it
6
+ * stays above a later top-layer entrant.
7
+ *
8
+ * The propagation SHIELD: nothing the tester does on the HUD reaches the host
9
+ * page. Shadow retargeting makes every event look like it happened on the
10
+ * host element, and a page's "close on outside pointerdown / focus" logic
11
+ * would close the very dialog being reported. Bubble-phase stops on the host
12
+ * — no preventDefault, so buttons and the textarea still focus.
13
+ */
14
+ import { RRWEB_BLOCK_ATTR } from '../capture/rrweb';
15
+ import { HOST_STYLE, SHEET_HOST_STYLE } from './styles';
16
+ const SHIELDED = [
17
+ 'pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointercancel',
18
+ 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'click', 'dblclick', 'auxclick',
19
+ 'contextmenu', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'wheel',
20
+ 'focusin', 'focusout', 'keydown', 'keyup', 'keypress',
21
+ ];
22
+ export function shield(el) {
23
+ for (const t of SHIELDED)
24
+ el.addEventListener(t, (e) => e.stopPropagation());
25
+ }
26
+ export function createHudHost() {
27
+ const host = document.createElement('div');
28
+ host.setAttribute(RRWEB_BLOCK_ATTR, '');
29
+ host.setAttribute('data-vitrinka-hud', '');
30
+ host.style.cssText = HOST_STYLE;
31
+ const root = host.attachShadow({ mode: 'open' });
32
+ const mount = document.createElement('div');
33
+ root.append(mount);
34
+ shield(host);
35
+ document.documentElement.append(host);
36
+ const canPop = typeof host.showPopover === 'function';
37
+ const raise = () => {
38
+ if (!canPop || !host.isConnected)
39
+ return;
40
+ try {
41
+ host.hidePopover?.();
42
+ }
43
+ catch {
44
+ // not shown
45
+ }
46
+ try {
47
+ host.showPopover?.();
48
+ }
49
+ catch {
50
+ // detached
51
+ }
52
+ };
53
+ if (canPop)
54
+ host.popover = 'manual';
55
+ raise();
56
+ let scheduled = false;
57
+ const schedule = () => {
58
+ if (scheduled)
59
+ return;
60
+ scheduled = true;
61
+ queueMicrotask(() => {
62
+ scheduled = false;
63
+ raise();
64
+ });
65
+ };
66
+ // A dialog can mount anywhere in the tree, so the whole document is watched
67
+ // — but the WORK is kept off the mutation hot path: records are collected
68
+ // and inspected at most once per animation frame, text nodes and childless
69
+ // elements are dismissed without a query, and the first hit ends the scan.
70
+ // Attribute records are already narrowed by attributeFilter.
71
+ const touchesDialog = (m) => {
72
+ if (m.type === 'attributes')
73
+ return m.target instanceof Element && isDialog(m.target);
74
+ for (const list of [m.addedNodes, m.removedNodes]) {
75
+ for (const n of list) {
76
+ if (!(n instanceof Element))
77
+ continue;
78
+ if (isDialog(n))
79
+ return true;
80
+ if (n.childElementCount > 0 && n.querySelector(DIALOG_SEL))
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ };
86
+ let pending = [];
87
+ let scanRaf = 0;
88
+ const scan = () => {
89
+ scanRaf = 0;
90
+ const batch = pending;
91
+ pending = [];
92
+ if (batch.some(touchesDialog))
93
+ schedule();
94
+ };
95
+ const mo = new MutationObserver((muts) => {
96
+ for (const m of muts)
97
+ pending.push(m);
98
+ if (scanRaf === 0) {
99
+ scanRaf =
100
+ typeof requestAnimationFrame === 'function'
101
+ ? requestAnimationFrame(scan)
102
+ : setTimeout(scan, 16);
103
+ }
104
+ });
105
+ mo.observe(document.documentElement, {
106
+ subtree: true,
107
+ childList: true,
108
+ attributes: true,
109
+ attributeFilter: ['open', 'data-state'],
110
+ });
111
+ return {
112
+ host,
113
+ mount,
114
+ destroy: () => {
115
+ mo.disconnect();
116
+ if (scanRaf !== 0 && typeof cancelAnimationFrame === 'function')
117
+ cancelAnimationFrame(scanRaf);
118
+ pending = [];
119
+ try {
120
+ host.hidePopover?.();
121
+ }
122
+ catch {
123
+ // not shown
124
+ }
125
+ host.remove();
126
+ },
127
+ };
128
+ }
129
+ const DIALOG_SEL = 'dialog,[role="dialog"]';
130
+ function isDialog(el) {
131
+ return el.matches(DIALOG_SEL);
132
+ }
133
+ /**
134
+ * Where the composer sheet portals (D4): the topmost open `<dialog>` (last in
135
+ * document order), else the last open ARIA dialog (`[role="dialog"]
136
+ * [data-state="open"]` — Radix and friends), else null = the HUD host itself.
137
+ * Inside the dialog's subtree a focus trap and a modal's inert both leave
138
+ * the sheet alone.
139
+ */
140
+ export function sheetTarget() {
141
+ const native = document.querySelectorAll('dialog[open]');
142
+ const lastNative = native[native.length - 1];
143
+ if (lastNative)
144
+ return lastNative;
145
+ const aria = document.querySelectorAll('[role="dialog"][data-state="open"]');
146
+ return aria[aria.length - 1] ?? null;
147
+ }
148
+ /**
149
+ * A sheet host: its own open shadow root (so the HUD stylesheet applies
150
+ * inside a foreign dialog too), shielded, appended to `target`.
151
+ */
152
+ export function createSheetHost(target) {
153
+ const el = document.createElement('div');
154
+ el.setAttribute(RRWEB_BLOCK_ATTR, '');
155
+ el.setAttribute('data-vitrinka-sheet', '');
156
+ el.style.cssText = SHEET_HOST_STYLE;
157
+ const root = el.attachShadow({ mode: 'open' });
158
+ const mount = document.createElement('div');
159
+ root.append(mount);
160
+ shield(el);
161
+ target.append(el);
162
+ return { mount, destroy: () => el.remove() };
163
+ }
164
+ /** Is `t` inside any HUD surface (host or a portaled sheet)? */
165
+ export function insideHud(t) {
166
+ if (!(t instanceof Node))
167
+ return false;
168
+ const el = t instanceof Element ? t : t.parentElement;
169
+ return Boolean(el?.closest('[data-vitrinka-hud],[data-vitrinka-sheet]'));
170
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The manifest glyphs the extension draws (vendor/vitrinka-icons.js), as
3
+ * React elements: 1em, currentColor, sized by the host's font.
4
+ */
5
+ import type { ReactElement } from 'react';
6
+ export declare function CheckIcon(): ReactElement;
7
+ export declare function PauseIcon(): ReactElement;
8
+ export declare function PlayIcon(): ReactElement;
9
+ export declare function PencilIcon(): ReactElement;
10
+ export declare function AnnotateIcon(): ReactElement;
11
+ export declare function CloseIcon(): ReactElement;
12
+ export declare function ArrowUpIcon(): ReactElement;
13
+ export declare function MoreIcon(): ReactElement;
14
+ export declare function NewTabIcon(): ReactElement;
15
+ export declare function StopIcon(): ReactElement;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const base = {
3
+ viewBox: '0 0 24 24',
4
+ fill: 'none',
5
+ stroke: 'currentColor',
6
+ strokeWidth: 2,
7
+ strokeLinecap: 'round',
8
+ strokeLinejoin: 'round',
9
+ 'aria-hidden': true,
10
+ };
11
+ export function CheckIcon() {
12
+ return (_jsx("svg", { ...base, children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }));
13
+ }
14
+ export function PauseIcon() {
15
+ return (_jsxs("svg", { ...base, children: [_jsx("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" }), _jsx("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" })] }));
16
+ }
17
+ export function PlayIcon() {
18
+ return (_jsx("svg", { ...base, children: _jsx("path", { d: "M6 4l14 8-14 8z" }) }));
19
+ }
20
+ export function PencilIcon() {
21
+ return (_jsxs("svg", { ...base, children: [_jsx("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" }), _jsx("path", { d: "m15 5 4 4" })] }));
22
+ }
23
+ export function AnnotateIcon() {
24
+ return (_jsxs("svg", { ...base, children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "22", x2: "18", y1: "12", y2: "12" }), _jsx("line", { x1: "6", x2: "2", y1: "12", y2: "12" }), _jsx("line", { x1: "12", x2: "12", y1: "6", y2: "2" }), _jsx("line", { x1: "12", x2: "12", y1: "22", y2: "18" })] }));
25
+ }
26
+ export function CloseIcon() {
27
+ return (_jsxs("svg", { ...base, children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }));
28
+ }
29
+ export function ArrowUpIcon() {
30
+ return (_jsxs("svg", { ...base, children: [_jsx("path", { d: "M12 19V5" }), _jsx("path", { d: "m5 12 7-7 7 7" })] }));
31
+ }
32
+ export function MoreIcon() {
33
+ return (_jsxs("svg", { ...base, children: [_jsx("circle", { cx: "12", cy: "12", r: "1" }), _jsx("circle", { cx: "19", cy: "12", r: "1" }), _jsx("circle", { cx: "5", cy: "12", r: "1" })] }));
34
+ }
35
+ export function NewTabIcon() {
36
+ return (_jsxs("svg", { ...base, children: [_jsx("path", { d: "M15 3h6v6" }), _jsx("path", { d: "M10 14 21 3" }), _jsx("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })] }));
37
+ }
38
+ export function StopIcon() {
39
+ return (_jsx("svg", { ...base, children: _jsx("rect", { x: "5", y: "5", width: "14", height: "14", rx: "2" }) }));
40
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The HUD's stylesheet — the extension's pill/sheet/pick CSS, verbatim where
3
+ * it applies. Lives inside the shadow root (a `<style>` element), so it never
4
+ * touches host CSS and host CSS never touches it.
5
+ */
6
+ export declare const HUD_CSS_BASE = "\n* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; }\nsvg { width:1em; height:1em; vertical-align:-.125em; }\n.stack { position:relative; z-index:4; display:flex; flex-direction:column; align-items:flex-end; gap:6px; }\n.pill { display:flex; align-items:center; gap:10px; padding:8px 10px 8px 14px;\n background:#1d1a1b; border:1px solid #292526; border-radius:999px;\n box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }\n.dot { width:10px; height:10px; border-radius:50%; background:#ff3b57; animation:p 1.4s ease infinite; }\n.paused .dot { animation:none; background:#756e68; }\n@keyframes p { 50% { opacity:.35; } }\n.sync { font:600 11px/1 ui-monospace, Menlo, monospace; color:#5f7a5f; }\n.sync.warn { color:#e8a33d; }\n.sync.bad { color:#ff3b57; }\n.sync.busy { color:#a8a099; }\n.detail { max-width:320px; padding:6px 12px; border-radius:999px;\n background:#1d1a1b; border:1px solid #292526; color:#a8a099;\n font:500 10px/1.4 ui-monospace, Menlo, monospace;\n opacity:0; transform:translateY(-3px); transition:opacity .24s ease, transform .24s ease;\n pointer-events:none; }\n.detail.show { opacity:1; transform:none; }\n.detail.bad { border-color:#ff3b57; color:#f0eae4; }\n.time { font:600 11px/1 ui-monospace, Menlo, monospace; }\n.name { font:500 10px/1 ui-monospace, Menlo, monospace; color:#756e68;\n border-left:1px solid #292526; padding-left:10px; max-width:140px;\n overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\nbutton { all:unset; cursor:pointer; position:relative; width:28px; height:28px; border-radius:50%;\n border:1px solid #363132; color:#a8a099; font-size:12px; text-align:center; line-height:28px; }\nbutton:hover { color:#f0eae4; border-color:#756e68; }\nbutton:focus-visible, .sendb:focus-visible, .closeb:focus-visible { outline:2px solid #ff3b57; outline-offset:2px; }\nbutton.snap { background:#ff3b57; border-color:#ff3b57; color:#fff; }\nbutton.snap.on { background:#f0eae4; border-color:#f0eae4; color:#1d1a1b; }\nkbd { position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%);\n padding:4px 6px; border-radius:5px; background:#252122; border:1px solid #363132;\n color:#a8a099; font:600 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em;\n white-space:nowrap; opacity:0; transition:opacity .16s ease; pointer-events:none; }\n.b-snap kbd, .b-more kbd { left:auto; right:0; transform:none; }\nbutton:hover kbd, button:focus-visible kbd { opacity:1; }\n.pill.composing kbd { opacity:0; }\n/* idle grip: a quiet dot until a recording starts */\n.grip { display:flex; align-items:center; justify-content:center; width:34px; height:34px; border-radius:50%;\n background:#1d1a1b; border:1px solid #292526; box-shadow:0 8px 30px rgba(0,0,0,.35); }\n.grip .dot { animation:none; opacity:.9; }\n.grip:hover .dot { animation:p 1.4s ease infinite; }\n.grip.busy .dot { background:#756e68; }\n/* menu (Open board \u00B7 Stop) */\n.menu { position:absolute; right:0; bottom:44px; min-width:160px; padding:6px;\n background:#1d1a1b; border:1px solid #292526; border-radius:10px;\n box-shadow:0 8px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; gap:2px; }\n.menu button { all:unset; cursor:pointer; padding:7px 10px; border-radius:6px; color:#d8d2cc;\n font:500 12px/1 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; display:flex; gap:8px; align-items:center; }\n.menu button:hover { background:#252122; color:#f0eae4; }\n.menu button.danger:hover { color:#ff3b57; }\n.menuwrap { position:relative; }\n/* pick chrome (annotate mode): dim, outline, hint \u2014 shadow-root children */\n.dim { position:fixed; inset:0; z-index:1; pointer-events:none; background:rgba(0,0,0,.22); }\n.outline { position:fixed; z-index:2; pointer-events:none; border:2px solid #ff3b57; border-radius:6px;\n box-shadow:0 0 0 4px rgba(255,59,87,.15); }\n.hint { position:fixed; top:16px; left:50%; transform:translateX(-50%); z-index:3; padding:8px 16px;\n border-radius:999px; background:#1d1a1b; border:1px solid #ff3b57; box-shadow:0 8px 30px rgba(0,0,0,.4);\n font:600 12px/1 -apple-system,BlinkMacSystemFont,sans-serif; color:#f0eae4; white-space:nowrap; }\n/* the composer sheet (360px) */\n.pop { width:360px; padding:14px; background:#1d1a1b; border:1px solid #292526; border-radius:14px;\n box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }\n.pop-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }\nlabel { display:flex; align-items:center; gap:7px; font:700 9.5px/1 ui-monospace, Menlo, monospace;\n letter-spacing:.18em; text-transform:uppercase; color:#756e68; }\nlabel i { width:3px; height:10px; background:#ff3b57; }\n.closeb { all:unset; cursor:pointer; width:24px; height:24px; border-radius:6px; color:#756e68; font-size:13px; text-align:center; line-height:24px; }\n.closeb:hover { color:#f0eae4; background:#252122; }\ntextarea { width:100%; margin-top:10px; padding:10px 12px; min-height:72px; resize:vertical;\n background:#141213; border:1px solid #363132; border-radius:8px; color:#f0eae4;\n font:400 16px/1.4 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; }\ntextarea:focus { outline:none; border-color:#756e68; }\n.row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; margin-top:10px; }\n.row.nodest { grid-template-columns:minmax(0,1fr) auto; }\n.ctx { font:500 10px/1.35 ui-monospace, Menlo, monospace; color:#756e68; text-align:center;\n overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n.row.nodest .ctx { text-align:left; }\n.dest { display:flex; align-items:center; gap:9px; }\n.dest button { all:unset; cursor:pointer; padding:2px 0; color:#756e68; font:600 9px/1 ui-monospace, Menlo, monospace;\n letter-spacing:.12em; text-transform:uppercase; border-bottom:1px solid transparent; }\n.dest button[aria-pressed=\"true\"] { color:#f0eae4; border-bottom-color:#ff3b57; }\n.sendb { all:unset; cursor:pointer; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; border-radius:8px;\n background:#ff3b57; color:#fff; font-size:13px; font-weight:600; line-height:1; }\n.sendb:hover { background:#ff5670; }\n.hints { margin-top:8px; font:500 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em; color:#756e68; }\n";
7
+ /** Inline style of the HUD host — fixed bottom-right, above everything; overrides the [popover] UA sheet. */
8
+ export declare const HOST_STYLE: string;
9
+ /** Inline style of a sheet host portaled into a dialog: the same corner, above the dialog's own chrome. */
10
+ export declare const SHEET_HOST_STYLE: string;
11
+ /** Link-sheet additions (appended to HUD_CSS). */
12
+ export declare const LINK_CSS = "\n.pop.link .code { margin-top:12px; font:700 28px/1.2 ui-monospace, Menlo, monospace; letter-spacing:.14em; text-align:center; color:#f0eae4; }\n.linkrow { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-top:12px; }\n.linkrow .sendb { text-decoration:none; }\n.qr { width:96px; height:96px; border-radius:8px; background:#fff; padding:4px; }\n.linkline { margin-top:10px; font:500 10px/1.4 ui-monospace, Menlo, monospace; color:#756e68; display:flex; gap:10px; align-items:center; }\n.linkline.bad { color:#ff3b57; }\n.retry { all:unset; cursor:pointer; padding:3px 8px; border-radius:6px; border:1px solid #363132; color:#a8a099; font:600 10px/1 ui-monospace, Menlo, monospace; }\n.retry:hover { color:#f0eae4; border-color:#756e68; }\n.grip.link { width:auto; border-radius:999px; padding:0 14px; gap:8px; color:#f0eae4; font:600 11px/1 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; }\n";
13
+ export declare const HUD_CSS: string;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The HUD's stylesheet — the extension's pill/sheet/pick CSS, verbatim where
3
+ * it applies. Lives inside the shadow root (a `<style>` element), so it never
4
+ * touches host CSS and host CSS never touches it.
5
+ */
6
+ export const HUD_CSS_BASE = `
7
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
8
+ svg { width:1em; height:1em; vertical-align:-.125em; }
9
+ .stack { position:relative; z-index:4; display:flex; flex-direction:column; align-items:flex-end; gap:6px; }
10
+ .pill { display:flex; align-items:center; gap:10px; padding:8px 10px 8px 14px;
11
+ background:#1d1a1b; border:1px solid #292526; border-radius:999px;
12
+ box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }
13
+ .dot { width:10px; height:10px; border-radius:50%; background:#ff3b57; animation:p 1.4s ease infinite; }
14
+ .paused .dot { animation:none; background:#756e68; }
15
+ @keyframes p { 50% { opacity:.35; } }
16
+ .sync { font:600 11px/1 ui-monospace, Menlo, monospace; color:#5f7a5f; }
17
+ .sync.warn { color:#e8a33d; }
18
+ .sync.bad { color:#ff3b57; }
19
+ .sync.busy { color:#a8a099; }
20
+ .detail { max-width:320px; padding:6px 12px; border-radius:999px;
21
+ background:#1d1a1b; border:1px solid #292526; color:#a8a099;
22
+ font:500 10px/1.4 ui-monospace, Menlo, monospace;
23
+ opacity:0; transform:translateY(-3px); transition:opacity .24s ease, transform .24s ease;
24
+ pointer-events:none; }
25
+ .detail.show { opacity:1; transform:none; }
26
+ .detail.bad { border-color:#ff3b57; color:#f0eae4; }
27
+ .time { font:600 11px/1 ui-monospace, Menlo, monospace; }
28
+ .name { font:500 10px/1 ui-monospace, Menlo, monospace; color:#756e68;
29
+ border-left:1px solid #292526; padding-left:10px; max-width:140px;
30
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31
+ button { all:unset; cursor:pointer; position:relative; width:28px; height:28px; border-radius:50%;
32
+ border:1px solid #363132; color:#a8a099; font-size:12px; text-align:center; line-height:28px; }
33
+ button:hover { color:#f0eae4; border-color:#756e68; }
34
+ button:focus-visible, .sendb:focus-visible, .closeb:focus-visible { outline:2px solid #ff3b57; outline-offset:2px; }
35
+ button.snap { background:#ff3b57; border-color:#ff3b57; color:#fff; }
36
+ button.snap.on { background:#f0eae4; border-color:#f0eae4; color:#1d1a1b; }
37
+ kbd { position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%);
38
+ padding:4px 6px; border-radius:5px; background:#252122; border:1px solid #363132;
39
+ color:#a8a099; font:600 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em;
40
+ white-space:nowrap; opacity:0; transition:opacity .16s ease; pointer-events:none; }
41
+ .b-snap kbd, .b-more kbd { left:auto; right:0; transform:none; }
42
+ button:hover kbd, button:focus-visible kbd { opacity:1; }
43
+ .pill.composing kbd { opacity:0; }
44
+ /* idle grip: a quiet dot until a recording starts */
45
+ .grip { display:flex; align-items:center; justify-content:center; width:34px; height:34px; border-radius:50%;
46
+ background:#1d1a1b; border:1px solid #292526; box-shadow:0 8px 30px rgba(0,0,0,.35); }
47
+ .grip .dot { animation:none; opacity:.9; }
48
+ .grip:hover .dot { animation:p 1.4s ease infinite; }
49
+ .grip.busy .dot { background:#756e68; }
50
+ /* menu (Open board · Stop) */
51
+ .menu { position:absolute; right:0; bottom:44px; min-width:160px; padding:6px;
52
+ background:#1d1a1b; border:1px solid #292526; border-radius:10px;
53
+ box-shadow:0 8px 30px rgba(0,0,0,.35); display:flex; flex-direction:column; gap:2px; }
54
+ .menu button { all:unset; cursor:pointer; padding:7px 10px; border-radius:6px; color:#d8d2cc;
55
+ font:500 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display:flex; gap:8px; align-items:center; }
56
+ .menu button:hover { background:#252122; color:#f0eae4; }
57
+ .menu button.danger:hover { color:#ff3b57; }
58
+ .menuwrap { position:relative; }
59
+ /* pick chrome (annotate mode): dim, outline, hint — shadow-root children */
60
+ .dim { position:fixed; inset:0; z-index:1; pointer-events:none; background:rgba(0,0,0,.22); }
61
+ .outline { position:fixed; z-index:2; pointer-events:none; border:2px solid #ff3b57; border-radius:6px;
62
+ box-shadow:0 0 0 4px rgba(255,59,87,.15); }
63
+ .hint { position:fixed; top:16px; left:50%; transform:translateX(-50%); z-index:3; padding:8px 16px;
64
+ border-radius:999px; background:#1d1a1b; border:1px solid #ff3b57; box-shadow:0 8px 30px rgba(0,0,0,.4);
65
+ font:600 12px/1 -apple-system,BlinkMacSystemFont,sans-serif; color:#f0eae4; white-space:nowrap; }
66
+ /* the composer sheet (360px) */
67
+ .pop { width:360px; padding:14px; background:#1d1a1b; border:1px solid #292526; border-radius:14px;
68
+ box-shadow:0 8px 30px rgba(0,0,0,.35); color:#f0eae4; }
69
+ .pop-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
70
+ label { display:flex; align-items:center; gap:7px; font:700 9.5px/1 ui-monospace, Menlo, monospace;
71
+ letter-spacing:.18em; text-transform:uppercase; color:#756e68; }
72
+ label i { width:3px; height:10px; background:#ff3b57; }
73
+ .closeb { all:unset; cursor:pointer; width:24px; height:24px; border-radius:6px; color:#756e68; font-size:13px; text-align:center; line-height:24px; }
74
+ .closeb:hover { color:#f0eae4; background:#252122; }
75
+ textarea { width:100%; margin-top:10px; padding:10px 12px; min-height:72px; resize:vertical;
76
+ background:#141213; border:1px solid #363132; border-radius:8px; color:#f0eae4;
77
+ font:400 16px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
78
+ textarea:focus { outline:none; border-color:#756e68; }
79
+ .row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; margin-top:10px; }
80
+ .row.nodest { grid-template-columns:minmax(0,1fr) auto; }
81
+ .ctx { font:500 10px/1.35 ui-monospace, Menlo, monospace; color:#756e68; text-align:center;
82
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
83
+ .row.nodest .ctx { text-align:left; }
84
+ .dest { display:flex; align-items:center; gap:9px; }
85
+ .dest button { all:unset; cursor:pointer; padding:2px 0; color:#756e68; font:600 9px/1 ui-monospace, Menlo, monospace;
86
+ letter-spacing:.12em; text-transform:uppercase; border-bottom:1px solid transparent; }
87
+ .dest button[aria-pressed="true"] { color:#f0eae4; border-bottom-color:#ff3b57; }
88
+ .sendb { all:unset; cursor:pointer; display:inline-flex; align-items:center; gap:6px; padding:7px 14px; border-radius:8px;
89
+ background:#ff3b57; color:#fff; font-size:13px; font-weight:600; line-height:1; }
90
+ .sendb:hover { background:#ff5670; }
91
+ .hints { margin-top:8px; font:500 9.5px/1 ui-monospace, Menlo, monospace; letter-spacing:.06em; color:#756e68; }
92
+ `;
93
+ /** Inline style of the HUD host — fixed bottom-right, above everything; overrides the [popover] UA sheet. */
94
+ export const HOST_STYLE = 'all:initial;position:fixed;z-index:2147483647;inset:auto 20px 20px auto;margin:0;padding:0;border:0;' +
95
+ 'background:transparent;overflow:visible;width:auto;height:auto;color-scheme:normal;';
96
+ /** Inline style of a sheet host portaled into a dialog: the same corner, above the dialog's own chrome. */
97
+ export const SHEET_HOST_STYLE = 'all:initial;position:fixed;z-index:2147483647;right:20px;bottom:76px;margin:0;padding:0;border:0;' +
98
+ 'background:transparent;overflow:visible;width:auto;height:auto;color-scheme:normal;';
99
+ /** Link-sheet additions (appended to HUD_CSS). */
100
+ export const LINK_CSS = `
101
+ .pop.link .code { margin-top:12px; font:700 28px/1.2 ui-monospace, Menlo, monospace; letter-spacing:.14em; text-align:center; color:#f0eae4; }
102
+ .linkrow { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-top:12px; }
103
+ .linkrow .sendb { text-decoration:none; }
104
+ .qr { width:96px; height:96px; border-radius:8px; background:#fff; padding:4px; }
105
+ .linkline { margin-top:10px; font:500 10px/1.4 ui-monospace, Menlo, monospace; color:#756e68; display:flex; gap:10px; align-items:center; }
106
+ .linkline.bad { color:#ff3b57; }
107
+ .retry { all:unset; cursor:pointer; padding:3px 8px; border-radius:6px; border:1px solid #363132; color:#a8a099; font:600 10px/1 ui-monospace, Menlo, monospace; }
108
+ .retry:hover { color:#f0eae4; border-color:#756e68; }
109
+ .grip.link { width:auto; border-radius:999px; padding:0 14px; gap:8px; color:#f0eae4; font:600 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
110
+ `;
111
+ export const HUD_CSS = HUD_CSS_BASE + LINK_CSS;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Vitrinka journey recorder for React DOM apps — `@vitrinka/web/recorder`.
3
+ *
4
+ * The runtime strip: when `url` is empty, `VitrinkaRecorderRoot` renders its
5
+ * children and starts NOTHING, and `VitrinkaRecorderPill` renders null. Auth
6
+ * is a device link minted from the pill (`@vitrinka/link`) or, for CI and
7
+ * unattended builds, an explicit `recorderKey`; `withVitrinkaRecorder`
8
+ * (`@vitrinka/web/next`) refuses a production build that carries a baked key
9
+ * outside an allowed lane.
10
+ */
11
+ import { type ReactElement, type ReactNode } from 'react';
12
+ import { useRecorderRoute } from './RecorderProvider';
13
+ export { useRecorderRoute };
14
+ export type { RecorderControl, RecorderStatus } from './control';
15
+ export type { RecorderConfig } from './config';
16
+ export { configureRecorderStorage, type RecorderStorage } from './storage';
17
+ export interface VitrinkaRecorderRootProps {
18
+ /** vitrinka base URL; defaults to `NEXT_PUBLIC_VITRINKA_URL`. */
19
+ url?: string;
20
+ /**
21
+ * A `vkr_` recorder key; defaults to `NEXT_PUBLIC_VITRINKA_KEY`. Named
22
+ * `recorderKey` because React reserves `key` and never delivers it as a prop.
23
+ */
24
+ recorderKey?: string;
25
+ /** Device-link label shown in vitrinka; defaults to `<browser> on <os> · <host>`. */
26
+ label?: string;
27
+ /** Reported in session meta. */
28
+ appVersion?: string;
29
+ /** Explicit server lane; omitted = the key's project rule decides. */
30
+ environment?: string;
31
+ /** A router's pathname, when the History wrap is not enough. */
32
+ route?: string | null;
33
+ children?: ReactNode;
34
+ }
35
+ /** Wrap the app root. Starts the capture lanes only when `url` + `key` are present. */
36
+ export declare function VitrinkaRecorderRoot(props: VitrinkaRecorderRootProps): ReactElement;
37
+ export interface VitrinkaRecorderPillProps {
38
+ /** Session title when the tester starts from the pill; defaults to `document.title`. */
39
+ title?: string;
40
+ }
41
+ /**
42
+ * The HUD — mount it anywhere under the root (it renders into its own shadow
43
+ * host on `<html>`, never into the app's DOM). Renders null when the root is
44
+ * inert.
45
+ */
46
+ export declare function VitrinkaRecorderPill(props: VitrinkaRecorderPillProps): null;