@usedocent/sdk-core 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.
@@ -0,0 +1,392 @@
1
+ import { MascotState, AnchorRef, ToolResult, TurnRecord, VoiceGrant, Snapshot, MascotPaletteInput, SnapshotNode, ClientFrame, ServerFrame } from '@usedocent/shared';
2
+ import { DotHandle, DotOptions, PresetName } from '@usedocent/mascot';
3
+
4
+ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
5
+ type Edge = 'left' | 'right';
6
+ type Placement = {
7
+ kind: 'corner';
8
+ corner: Corner;
9
+ } | {
10
+ kind: 'edge';
11
+ edge: Edge;
12
+ y: number;
13
+ };
14
+ interface Rect {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+ declare const INSET = 10;
21
+ declare const DEFAULT_PLACEMENT: Placement;
22
+ declare const nearestCorner: (cx: number, cy: number, vw: number, vh: number) => Corner;
23
+ declare const cornerOrder: (from: Corner, vw: number, vh: number, size: number) => Corner[];
24
+ declare const placementToPoint: (p: Placement, vw: number, vh: number, size: number) => {
25
+ x: number;
26
+ y: number;
27
+ };
28
+ declare const rectsOverlap: (a: Rect, b: Rect) => boolean;
29
+ declare const rectNear: (a: Rect, b: Rect, gap: number) => boolean;
30
+ declare const resolveKeepOut: (p: Placement, blockers: Rect[], vw: number, vh: number, size: number) => Placement;
31
+ declare const snapPlacement: (cx: number, cy: number, vw: number, vh: number, size: number, mobile: boolean) => Placement;
32
+
33
+ interface PresenceStorage {
34
+ loadPlacement(): Placement | null;
35
+ savePlacement(p: Placement): void;
36
+ hasSeen(): boolean;
37
+ markSeen(): void;
38
+ dismissals(): number;
39
+ addDismissal(): number;
40
+ }
41
+ declare const createStorage: (appId: string, userId: string, win?: Window) => PresenceStorage;
42
+
43
+ declare const STATE_ANNOUNCEMENTS: Record<MascotState, string>;
44
+ interface LiveRegion {
45
+ el: HTMLElement;
46
+ announce(text: string): void;
47
+ }
48
+ declare const createLiveRegion: (root: ShadowRoot | HTMLElement) => LiveRegion;
49
+
50
+ declare const FIRST_RUN_CAPTION = "I'm Docent. Tap if you want a hand.";
51
+ interface CaptionPanelOptions {
52
+ reducedMotion: boolean;
53
+ onClose(): void;
54
+ onToggleMute(muted: boolean): void;
55
+ returnFocusTo: HTMLElement;
56
+ /** Retained for compatibility; captions now stream by character, not by word. */
57
+ wordMs?: number;
58
+ }
59
+ interface CaptionPanel {
60
+ el: HTMLElement;
61
+ readonly isOpen: boolean;
62
+ readonly isMuted: boolean;
63
+ readonly composer: HTMLInputElement | null;
64
+ open(focus?: boolean): void;
65
+ close(returnFocus?: boolean): void;
66
+ setMuted(muted: boolean): void;
67
+ addCaption(role: 'docent' | 'user', text: string): HTMLElement;
68
+ setComposer(onSubmit: (text: string) => void): void;
69
+ streamCaption(role: 'docent', el: HTMLElement, delta: string): void;
70
+ /** Finishes a caption at once: shows all of it and drops the caret. */
71
+ settleCaption(el: HTMLElement): void;
72
+ /**
73
+ * Asks the user to confirm an action the guard withheld, and returns whether it was shown.
74
+ *
75
+ * The guide proposes; the guard withholds; the user decides. Pressing the button here IS the
76
+ * explicit request PRD rule 1 requires, in the same turn, so nothing about the guarantee moves:
77
+ * it just stops the user having to phrase the request again to get what they already meant.
78
+ */
79
+ offer(label: string, onAccept: () => void): boolean;
80
+ /** Drops any pending offer. A stale offer belongs to a turn that is over. */
81
+ clearOffer(): void;
82
+ showFirstRun(): void;
83
+ destroy(): void;
84
+ }
85
+ declare const createCaptionPanel: (root: ShadowRoot, options: CaptionPanelOptions) => CaptionPanel;
86
+
87
+ declare const overlayStyles = "\n:host {\n /* Read the host's value, fall back to ours. Declaring --docent-accent here instead would set\n the property ON the host element, which beats the value inherited from the page: the\n documented theming hook silently did nothing, and every overlay stayed purple inside every\n customer's app no matter what they set. The rules below use the internal names, so the\n public --docent-* names stay purely inputs. */\n --dk-accent: var(--docent-accent, #7f77dd);\n --dk-radius: var(--docent-radius, 16px);\n --dk-font: var(--docent-font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);\n --dk-ink: var(--docent-ink, #130e30);\n --dk-surface: var(--docent-surface, #ffffff);\n --docent-ease: cubic-bezier(0.2, 0, 0, 1);\n /* A drop is not a state change. --docent-ease decelerates hard, which is right for a face\n changing expression and wrong for an object being put down: it spends its last 200ms\n crawling the final 20px, and a mascot with visible weight reads that crawl as lag rather\n than as easing. The snap curve keeps speed most of the way and settles quickly. */\n --docent-snap: cubic-bezier(0.32, 0.72, 0, 1);\n --docent-snap-dur: 260ms;\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 2147483000;\n font-family: var(--dk-font);\n color: var(--dk-ink);\n}\n.docent-dot {\n position: absolute;\n left: 0;\n top: 0;\n translate: var(--docent-x, 0px) var(--docent-y, 0px);\n transition-property: translate;\n transition-duration: var(--docent-snap-dur);\n transition-timing-function: var(--docent-snap);\n pointer-events: auto;\n touch-action: none;\n appearance: none;\n border: 0;\n padding: 0;\n margin: 0;\n background: transparent;\n line-height: 0;\n cursor: grab;\n border-radius: 9999px;\n}\n.docent-dot[data-dragging='true'] { transition-duration: 0ms; cursor: grabbing; }\n.docent-dot:active { scale: 0.96; transition-property: translate, scale; }\n.docent-dot:focus-visible { outline: 3px solid var(--dk-accent); outline-offset: 2px; }\n.docent-panel {\n position: absolute;\n width: min(360px, calc(100vw - 20px));\n max-height: min(60vh, 480px);\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n /* Glass, so the panel reads as sitting on the host's page rather than as a white card\n dropped over it. The surface colour still comes from the host token, just at low alpha,\n so a dark app gets dark glass. */\n background: color-mix(in oklab, var(--dk-surface) 55%, transparent);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n backdrop-filter: blur(20px) saturate(180%);\n color: var(--dk-ink);\n border-radius: var(--dk-radius);\n /* A hairline rather than a border: enough to hold an edge against a busy page. */\n border: 1px solid color-mix(in oklab, var(--dk-ink) 8%, transparent);\n box-shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px rgb(0 0 0 / 0.08);\n font-size: 14px;\n line-height: 1.4;\n}\n/* Without backdrop-filter there is no glass, only a see-through panel over live text, so this\n falls back to the opaque surface instead. */\n@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {\n .docent-panel { background: var(--dk-surface); }\n}\n.docent-panel[hidden] { display: none; }\n.docent-panel header { display: flex; align-items: center; gap: 4px; padding: 8px 8px 8px 16px; }\n.docent-panel h2 { flex: 1; margin: 0; font-size: 14px; font-weight: 600; }\n.docent-panel h2:focus { outline: none; }\n.docent-iconbtn {\n appearance: none; border: 0; background: transparent; color: inherit; cursor: pointer;\n width: 40px; height: 40px; border-radius: 9999px; display: inline-grid; place-items: center; font: inherit;\n}\n.docent-iconbtn:hover { background: rgb(0 0 0 / 0.06); }\n.docent-iconbtn:focus-visible { outline: 2px solid var(--dk-accent); outline-offset: -2px; }\n.docent-iconbtn[aria-pressed='true'] { color: var(--dk-accent); }\n@media (pointer: coarse) { .docent-iconbtn { width: 44px; height: 44px; } }\n.docent-transcript { list-style: none; margin: 0; padding: 0 16px 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }\n.docent-transcript li { padding: 6px 2px; border-radius: 12px; max-width: 92%; }\n/* Docent's own words sit directly on the glass. A filled bubble here would put a card inside\n a card, which is the thing the glass is meant to get rid of. */\n.docent-transcript li[data-role='docent'] { align-self: flex-start; }\n.docent-transcript li[data-role='user'] {\n padding: 8px 12px;\n background: color-mix(in oklab, var(--dk-accent) 88%, transparent);\n color: #fff;\n align-self: flex-end;\n}\n/* The caret marks text still arriving, and goes when the sentence is complete. */\n.docent-caret {\n display: inline-block; width: 2px; height: 1em; margin-left: 2px; vertical-align: -0.15em;\n background: currentColor; opacity: 0.7; animation: docent-caret-blink 1s steps(1) infinite;\n}\n@keyframes docent-caret-blink { 0%, 49% { opacity: 0.7; } 50%, 100% { opacity: 0; } }\n.docent-sr-only {\n position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;\n clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;\n}\n.docent-overlay { position: absolute; inset: 0; pointer-events: none; }\n.docent-overlay[hidden] { display: none; }\n.docent-dim { position: absolute; inset: 0; width: 100%; height: 100%; }\n.docent-dim[hidden] { display: none; }\n.docent-marker {\n position: absolute; left: 0; top: 0; width: 24px; height: 24px;\n translate: 0 0; transition-property: translate; transition-duration: 700ms; transition-timing-function: var(--docent-ease);\n}\n.docent-cursor {\n position: absolute; inset: 0; width: 24px; height: 24px;\n fill: var(--dk-accent);\n /* Derived, never configured: the edge contrasts with the host's accent, so a pale brand does\n not lose its silhouette on a pale page. Computed in tools/contrast.ts. */\n stroke: var(--docent-pointer-edge, #fff); stroke-width: 2.25; stroke-linejoin: round;\n paint-order: stroke fill;\n /* Separation from the page behind is the shadow's job; the edge separates fill from outline.\n Two jobs, two devices. */\n filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.28));\n}\n/* Mirrored about the tip, so the body swings away from the control rather than across it. */\n.docent-marker[data-facing='left'] .docent-cursor { transform: scaleX(-1); transform-origin: 4px 2px; }\n/* The click: two rings leaving the tip. */\n.docent-pulse, .docent-pulse::after {\n position: absolute; left: 4px; top: 2px; width: 14px; height: 14px; margin: -7px 0 0 -7px;\n border-radius: 9999px; border: 2px solid var(--dk-accent); opacity: 0;\n}\n.docent-pulse { content: ''; }\n.docent-pulse::after { content: ''; left: 7px; top: 7px; animation-delay: 260ms; }\n.docent-marker[data-arrived] .docent-pulse,\n.docent-marker[data-arrived] .docent-pulse::after {\n animation: docent-click 1200ms var(--docent-ease) 3;\n}\n@keyframes docent-click {\n from { opacity: 0.75; scale: 0.4; }\n to { opacity: 0; scale: 2.2; }\n}\n.docent-marker[hidden] { display: none; }\n.docent-composer { display: flex; gap: 6px; padding: 8px 12px 12px; }\n.docent-offer {\n display: flex; align-items: center; gap: 8px; padding: 8px 12px 0; font-size: 13px;\n}\n.docent-offer span { flex: 1; min-width: 0; }\n.docent-offer button {\n font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 9999px; cursor: pointer;\n transition-property: scale; transition-duration: 120ms;\n}\n.docent-offer button:active { scale: 0.96; }\n.docent-offer-yes { border: 0; background: var(--dk-accent); color: #fff; font-weight: 600; }\n.docent-offer-no {\n border: 1px solid color-mix(in oklab, var(--dk-ink) 20%, transparent);\n background: transparent; color: inherit;\n}\n.docent-offer[hidden] { display: none; }\n.docent-composer input { flex: 1; min-width: 0; height: 40px; padding: 0 12px; border-radius: 9999px; border: 1px solid color-mix(in oklab, var(--dk-ink) 20%, transparent); background: transparent; color: inherit; font: inherit; }\n.docent-composer input:focus-visible { outline: 2px solid var(--dk-accent); outline-offset: 1px; }\n.docent-send { height: 40px; padding: 0 14px; border-radius: 9999px; border: 0; background: var(--dk-accent); color: #fff; font: inherit; font-weight: 600; cursor: pointer; transition-property: scale; transition-duration: 120ms; }\n.docent-send:active { scale: 0.96; }\n.docent-send:focus-visible { outline: 2px solid var(--dk-accent); outline-offset: 1px; }\n@media (pointer: coarse) { .docent-composer input, .docent-send { height: 44px; } }\n@media (prefers-reduced-motion: reduce) {\n .docent-dot, .docent-panel { transition-duration: 0ms !important; }\n .docent-marker { transition-duration: 0ms !important; }\n /* The caret still marks unfinished text, it just stops blinking (PRD rule 6). */\n .docent-caret { animation: none !important; }\n /* The cursor still arrives and still points; it just does not pulse. */\n .docent-pulse, .docent-pulse::after { animation: none !important; }\n}\n";
88
+
89
+ interface RouterAdapter {
90
+ navigate(path: string): void | Promise<void>;
91
+ currentPath(): string;
92
+ }
93
+ /** Fallback for hosts without a router adapter: pushState and let listeners react to popstate. */
94
+ declare const locationAdapter: (win: Window) => RouterAdapter;
95
+
96
+ interface ToolsOptions {
97
+ presence: PresenceHandle;
98
+ doc?: Document;
99
+ router?: RouterAdapter;
100
+ threshold?: number;
101
+ }
102
+ interface Tools {
103
+ spotlight(anchor: AnchorRef): ToolResult;
104
+ point(anchor: AnchorRef): ToolResult;
105
+ scroll_to(anchor: AnchorRef): ToolResult;
106
+ navigate(route: string): Promise<ToolResult>;
107
+ act(anchor: AnchorRef, action: 'click' | 'fill', opts: {
108
+ userRequested: boolean;
109
+ value?: string;
110
+ }): ToolResult;
111
+ clear(): void;
112
+ destroy(): void;
113
+ }
114
+ declare const createTools: (options: ToolsOptions) => Tools;
115
+
116
+ interface VoiceInput {
117
+ grant: VoiceGrant;
118
+ tools: Tools;
119
+ presence: PresenceHandle;
120
+ onEnd(): void;
121
+ /** Origin of the Docent API, for vendors whose signalling goes through our Worker. */
122
+ apiUrl: string;
123
+ /** Proves this browser owns the session. Never sent to the vendor, only to our own Worker. */
124
+ clientSecret: string;
125
+ /**
126
+ * Hand a user turn to the Durable Object and get back what it decided to say.
127
+ *
128
+ * This is the delegation bridge. A vendor that delegates reasoning (GPT-Live) calls this when
129
+ * it wants an answer, so the knowledge pack, the goals, the tools and the action guard all run
130
+ * exactly as they do in text mode. The audio chunk never reasons and never picks a tool.
131
+ */
132
+ delegate(text: string): Promise<string>;
133
+ }
134
+ interface VoiceHandle {
135
+ end(): Promise<void>;
136
+ setMuted(muted: boolean): void;
137
+ }
138
+ interface AudioModule {
139
+ startVoice(input: VoiceInput): Promise<VoiceHandle>;
140
+ }
141
+ type DocentEvent = {
142
+ type: 'handoff';
143
+ reason: string;
144
+ route: string;
145
+ transcript: TurnRecord[];
146
+ } | {
147
+ type: 'open_docs';
148
+ url: string;
149
+ title?: string;
150
+ } | {
151
+ type: 'goal_done';
152
+ goalKey: string;
153
+ } | {
154
+ type: 'error';
155
+ code: string;
156
+ message: string;
157
+ };
158
+ interface DocentOptions {
159
+ appId: string;
160
+ publishableKey: string;
161
+ apiUrl: string;
162
+ user: {
163
+ id: string;
164
+ traits?: Record<string, string | number | boolean>;
165
+ };
166
+ userToken?: string;
167
+ router?: RouterAdapter;
168
+ presence?: Partial<Omit<PresenceOptions, 'appId' | 'user'>>;
169
+ /**
170
+ * Loads the audio chunk for the vendor this session was granted. Takes the vendor so the host
171
+ * can code-split per vendor and ship only the one a given session needs; the grant is resolved
172
+ * before this is called, so the choice is never a guess.
173
+ */
174
+ loadAudio?: (vendor: VoiceGrant['vendor']) => Promise<AudioModule>;
175
+ /**
176
+ * Loads the presence the owner chose in the Console, by library slug.
177
+ *
178
+ * Injected for the same reason `loadAudio` is: naming the presences here would make `sdk-core`
179
+ * import all fourteen, and an app showing one would ship the bytes of every other. The host
180
+ * passes `(slug) => PRESENCE_LOADERS[slug]?.()` from `@usedocent/mascot/library/loaders`, which is
181
+ * a chunk per presence and none fetched until one is chosen. Omitting it keeps Dot.
182
+ */
183
+ loadPresence?: (slug: string) => Promise<MascotFactory>;
184
+ debug?: boolean;
185
+ fetchImpl?: typeof fetch;
186
+ WebSocketImpl?: typeof WebSocket;
187
+ }
188
+ interface DocentHandle {
189
+ readonly presence: PresenceHandle;
190
+ readonly tools: Tools;
191
+ readonly sessionId: string | null;
192
+ readonly ready: Promise<void>;
193
+ ask(text: string): Promise<void>;
194
+ track(event: string, payload?: Record<string, unknown>): void;
195
+ on(cb: (e: DocentEvent) => void): () => void;
196
+ startVoice(): Promise<void>;
197
+ endVoice(): Promise<void>;
198
+ destroy(): void;
199
+ }
200
+ declare const createDocent: (options: DocentOptions) => DocentHandle;
201
+
202
+ /**
203
+ * A mascot constructor. Every presence exposes the same handle, so swapping one is a factory
204
+ * swap and nothing else changes.
205
+ *
206
+ * Injected rather than selected by name for the same reason `loadAudio` is: naming it here would
207
+ * mean `sdk-core` statically imports every presence, and an app running Dot would pay for Lumen's
208
+ * bytes. The host imports the one it wants.
209
+ */
210
+ type MascotFactory = (host: HTMLElement, options?: DotOptions) => DotHandle;
211
+ interface PresenceOptions {
212
+ appId: string;
213
+ user?: {
214
+ id: string;
215
+ };
216
+ palette?: PresetName | MascotPaletteInput;
217
+ /** Defaults to Dot. Pass `createLumen` from `@usedocent/mascot/lumen` for the signature presence. */
218
+ mascot?: MascotFactory;
219
+ mount?: HTMLElement;
220
+ arrivalMs?: {
221
+ first: number;
222
+ later: number;
223
+ };
224
+ debug?: boolean;
225
+ }
226
+ interface PresenceHandle {
227
+ readonly host: HTMLElement;
228
+ readonly root: ShadowRoot;
229
+ readonly dot: DotHandle;
230
+ readonly trigger: HTMLButtonElement;
231
+ readonly panel: CaptionPanel;
232
+ readonly placement: Placement;
233
+ readonly isOpen: boolean;
234
+ readonly isMuted: boolean;
235
+ readonly proactiveMuted: boolean;
236
+ open(): void;
237
+ close(): void;
238
+ toggle(): void;
239
+ setMuted(muted: boolean): void;
240
+ /** Fires whenever mute changes, however it changed. Returns an unsubscribe. */
241
+ onMuteChange(cb: (muted: boolean) => void): () => void;
242
+ /**
243
+ * Fires when the user actively dismisses the guide, by the close control or by tapping Dot
244
+ * shut. Not fired by the arrival beat settling on its own, which the user did not ask for.
245
+ * Returns an unsubscribe.
246
+ */
247
+ onDismiss(cb: () => void): () => void;
248
+ setPlacement(p: Placement, animate?: boolean): void;
249
+ setState(state: MascotState): void;
250
+ /**
251
+ * Replaces the mascot in place, keeping the current state, size and placement.
252
+ *
253
+ * Exists because the owner's choice of presence arrives with the session mint, which is one
254
+ * network round trip after the presence is on screen. Constructing nothing until the mint
255
+ * returns would cost the arrival beat its timing and leave the corner empty on a slow network.
256
+ */
257
+ setMascot(factory: MascotFactory): void;
258
+ addCaption(role: 'docent' | 'user', text: string): HTMLElement;
259
+ relayout(): void;
260
+ destroy(): void;
261
+ }
262
+ declare global {
263
+ interface Window {
264
+ __docent?: {
265
+ presence: PresenceHandle;
266
+ tools?: Tools;
267
+ snapshot?: () => Snapshot | null;
268
+ docent?: DocentHandle;
269
+ };
270
+ }
271
+ }
272
+ declare const createPresence: (options: PresenceOptions) => PresenceHandle;
273
+
274
+ declare const NAME_FROM_CONTENT: ReadonlySet<string>;
275
+ declare const isInteractiveRole: (role: string) => boolean;
276
+ declare const implicitRole: (el: Element) => string | null;
277
+ /** the label element(s) accessibleName reads text from for this element, explicit for- label first */
278
+ declare const labelElements: (el: Element) => Element[];
279
+ /** the elements aria-labelledby points at, in order, skipping ids that don't resolve */
280
+ declare const labelledByTargets: (el: Element) => Element[];
281
+ declare const accessibleName: (el: Element) => string;
282
+
283
+ declare const MASK = "\u2022\u2022\u2022";
284
+ declare const redactPII: (text: string) => string;
285
+ declare const isMasked: (el: Element) => boolean;
286
+ /**
287
+ * true when el is inside a mask, contains one, or draws its accessible name from an
288
+ * element that is inside a mask or contains one (aria-labelledby target, explicit
289
+ * label[for], or wrapping label) — either way its text must not leave the page
290
+ */
291
+ declare const containsMask: (el: Element) => boolean;
292
+ declare const safeMask: <T>(fn: () => T) => T | null;
293
+
294
+ interface CaptureOptions {
295
+ maxNodes?: number;
296
+ includeLandmarks?: boolean;
297
+ }
298
+ declare const nodeKey: (el: Element, role: string, name: string, ordinal?: number) => string;
299
+ declare const captureSnapshot: (doc: Document, opts?: CaptureOptions) => Snapshot | null;
300
+ declare const elementFor: (node: SnapshotNode, doc: Document) => Element | null;
301
+
302
+ interface SnapshotDelta {
303
+ added: SnapshotNode[];
304
+ removed: string[];
305
+ changed: SnapshotNode[];
306
+ route: string;
307
+ scrollY: number;
308
+ }
309
+ declare const DELTA_BUDGET_BYTES: number;
310
+ declare const diffSnapshots: (prev: Snapshot | null, next: Snapshot) => SnapshotDelta;
311
+ declare const deltaBytes: (d: SnapshotDelta) => number;
312
+
313
+ interface WatchOptions {
314
+ debounceMs?: number;
315
+ }
316
+ declare const watchDOM: (doc: Document, onChange: () => void, opts?: WatchOptions) => (() => void);
317
+
318
+ type AnchorMethod = 'guide' | 'role-name' | 'text' | 'position';
319
+ interface Resolution {
320
+ el: Element;
321
+ confidence: number;
322
+ method: AnchorMethod;
323
+ describe: string;
324
+ }
325
+ declare const CONFIDENCE: {
326
+ readonly guide: 1;
327
+ readonly roleNameExact: 0.9;
328
+ readonly roleNameContains: 0.7;
329
+ readonly text: 0.6;
330
+ readonly position: 0.4;
331
+ };
332
+ declare const DEFAULT_THRESHOLD = 0.6;
333
+ declare const describeElement: (el: Element) => string;
334
+ declare const resolveAnchor: (ref: AnchorRef, doc: Document) => Resolution | null;
335
+
336
+ type GuardAction = 'click' | 'fill';
337
+ interface GuardContext {
338
+ userRequested: boolean;
339
+ }
340
+ type GuardDecision = {
341
+ allowed: true;
342
+ } | {
343
+ allowed: false;
344
+ reason: 'forbidden' | 'not_allowlisted' | 'not_requested' | 'unsupported';
345
+ };
346
+ declare const DESTRUCTIVE_PATTERN: RegExp;
347
+ declare const evaluateAction: (el: Element, action: GuardAction, ctx: GuardContext) => GuardDecision;
348
+
349
+ /** Docent's deep ink, used when the accent is too light for a white edge to register. */
350
+ declare const EDGE_DARK = "#130e30";
351
+ declare const EDGE_LIGHT = "#ffffff";
352
+ /** WCAG 2.2 relative luminance. Channels are 0..1. */
353
+ declare const relativeLuminance: (r: number, g: number, b: number) => number;
354
+ /**
355
+ * The outline for a given accent, from a computed `rgb()` / `rgba()` string. Anything
356
+ * unparseable falls back to the white edge, which is the right default for the purple the
357
+ * overlay ships with.
358
+ */
359
+ declare const edgeForAccent: (computedColor: string) => string;
360
+
361
+ interface Overlay {
362
+ spotlight(target: DOMRect): void;
363
+ point(from: {
364
+ x: number;
365
+ y: number;
366
+ }, target: DOMRect): void;
367
+ clear(): void;
368
+ destroy(): void;
369
+ }
370
+ declare const createOverlay: (root: ShadowRoot, opts: {
371
+ reducedMotion: boolean;
372
+ }) => Overlay;
373
+
374
+ interface Transport {
375
+ send(frame: ClientFrame): void;
376
+ onFrame(cb: (f: ServerFrame) => void): () => void;
377
+ readonly connected: boolean;
378
+ close(): void;
379
+ }
380
+ interface TransportOptions {
381
+ WebSocketImpl?: typeof WebSocket;
382
+ backoffMs?: number[];
383
+ queueLimit?: number;
384
+ }
385
+ declare const createTransport: (url: string, opts?: TransportOptions) => Transport;
386
+
387
+ interface SyncOptions {
388
+ debounceMs?: number;
389
+ }
390
+ declare const startSync: (doc: Document, transport: Transport, opts?: SyncOptions) => (() => void);
391
+
392
+ export { type AnchorMethod, type AudioModule, CONFIDENCE, type CaptionPanel, type CaptionPanelOptions, type CaptureOptions, type Corner, DEFAULT_PLACEMENT, DEFAULT_THRESHOLD, DELTA_BUDGET_BYTES, DESTRUCTIVE_PATTERN, type DocentEvent, type DocentHandle, type DocentOptions, EDGE_DARK, EDGE_LIGHT, type Edge, FIRST_RUN_CAPTION, type GuardAction, type GuardContext, type GuardDecision, INSET, type LiveRegion, MASK, type MascotFactory, NAME_FROM_CONTENT, type Overlay, type Placement, type PresenceHandle, type PresenceOptions, type PresenceStorage, type Rect, type Resolution, type RouterAdapter, STATE_ANNOUNCEMENTS, type SnapshotDelta, type SyncOptions, type Tools, type ToolsOptions, type Transport, type TransportOptions, type VoiceHandle, type VoiceInput, type WatchOptions, accessibleName, captureSnapshot, containsMask, cornerOrder, createCaptionPanel, createDocent, createLiveRegion, createOverlay, createPresence, createStorage, createTools, createTransport, deltaBytes, describeElement, diffSnapshots, edgeForAccent, elementFor, evaluateAction, implicitRole, isInteractiveRole, isMasked, labelElements, labelledByTargets, locationAdapter, nearestCorner, nodeKey, overlayStyles, placementToPoint, rectNear, rectsOverlap, redactPII, relativeLuminance, resolveAnchor, resolveKeepOut, safeMask, snapPlacement, startSync, watchDOM };