@fixback/sdk 0.5.0 → 0.6.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 (41) hide show
  1. package/README.md +12 -2
  2. package/dist/capture/extract-dom.d.ts +33 -0
  3. package/dist/connect.d.ts +47 -0
  4. package/dist/dom.d.ts +7 -19
  5. package/dist/element-picker.d.ts +1 -1
  6. package/dist/error-capture.d.ts +21 -64
  7. package/dist/fixback.umd.js +36 -202
  8. package/dist/fixback.umd.js.map +1 -1
  9. package/dist/identity.d.ts +6 -1
  10. package/dist/index.d.ts +17 -15
  11. package/dist/index.mjs +3577 -3683
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/init.d.ts +62 -96
  14. package/dist/mount.d.ts +46 -0
  15. package/dist/overlay/dom.d.ts +16 -0
  16. package/dist/overlay/icons.d.ts +23 -0
  17. package/dist/overlay/marking.d.ts +64 -0
  18. package/dist/overlay/send.d.ts +65 -0
  19. package/dist/overlay/view.d.ts +78 -0
  20. package/dist/overlay-styles.d.ts +1 -1
  21. package/dist/overlay.d.ts +40 -41
  22. package/dist/package.json +3 -0
  23. package/dist/report.d.ts +9 -13
  24. package/dist/reporter-session-store.d.ts +17 -0
  25. package/dist/session.d.ts +27 -0
  26. package/dist/submit.d.ts +51 -8
  27. package/dist/trace/instrument/beacon.d.ts +16 -0
  28. package/dist/trace/instrument/console.d.ts +11 -0
  29. package/dist/trace/instrument/fetch.d.ts +16 -0
  30. package/dist/trace/instrument/index.d.ts +64 -0
  31. package/dist/trace/instrument/navigation.d.ts +12 -0
  32. package/dist/trace/instrument/ui.d.ts +23 -0
  33. package/dist/trace/instrument/window.d.ts +66 -0
  34. package/dist/trace/instrument/xhr.d.ts +15 -0
  35. package/dist/version.d.ts +1 -1
  36. package/package.json +8 -5
  37. package/dist/boot.d.ts +0 -74
  38. package/dist/breadcrumbs.d.ts +0 -327
  39. package/dist/invite.d.ts +0 -104
  40. package/dist/onboarding-styles.d.ts +0 -8
  41. package/dist/onboarding.d.ts +0 -44
package/dist/init.d.ts CHANGED
@@ -1,36 +1,31 @@
1
- import type { BeforeSend } from "@fixback/sdk-core";
2
- import { type IdentityInputs } from "./boot";
3
- import { type BeforeBreadcrumb, type BreadcrumbLevel, type TraceStream } from "./breadcrumbs";
4
- import { type ReplayOptions } from "./replay";
5
1
  /**
6
- * The hosted Fixback API origin the SDK talks to by default. A self-hosted or
7
- * local deployment overrides it with `apiUrl` (the dashboard's install snippet
8
- * pre-fills the right value for the Project).
9
- */
10
- export declare const DEFAULT_API_URL = "https://api.fixback.dev";
11
- /**
12
- * Does `url` address one of the SDK's **own** API endpoints? Every request the SDK
13
- * issues lives under two namespaces beneath the API origin: `/api/ingest/*` boot
14
- * (`bootEndpoint`, boot.ts) and feedback (`feedbackEndpoint`, submit.ts) and
15
- * `/api/invites/*` (`inviteStatusEndpoint` / `redeemEndpoint`, invite.ts).
2
+ * The SDK's **entry point** and the options it takes.
3
+ *
4
+ * `init` boots the SDK and mounts the launcher when — and only when — a submission
5
+ * would be accepted for this key, origin, and Gate. It recognises the Reporter through
6
+ * the **Reporter session** token it holds after a Connect (ADR-0032), refreshes it on
7
+ * boot, and detects a one-time code arriving on the `?fixback=` query parameter. The
8
+ * running instance and the module-level {@link Fixback} expose `signIn` / `signOut`
9
+ * / `identity`, so a host app can start a Connect from its own UI (the way a gated
10
+ * Project, which shows nothing to a signed-out visitor, is entered).
16
11
  *
17
- * The trace's network capture ignores **only** these, never the whole origin. That
18
- * distinction matters when the instrumented app and the Fixback API share an origin a
19
- * self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
20
- * ignoring the origin wholesale would swallow all of the app's own requests and leave the
21
- * Network tab empty. When the app and the API sit on different origins (the common case)
22
- * this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
23
- * request URL — absolute for every call the SDK makes — so a prefix test over the
24
- * normalised origin is exact.
12
+ * The runtime-agnostic half the trace buffer and its crumb builders, scrubbing,
13
+ * fingerprinting, the ingest boot + Connect contracts, and the shared init vocabulary
14
+ * is `@fixback/sdk-core` (ADR-0028) and re-exported here, so a host page imports
15
+ * everything from one package.
25
16
  */
26
- export declare function isFixbackApiRequest(apiUrl: string, url: string): boolean;
17
+ import { type BreadcrumbLevel, type CommonCaptureInitOptions, DEFAULT_API_URL, type IdentityInputs, type ReporterIdentity, type TraceStream } from "@fixback/sdk-core";
18
+ import type { ReplayOptions } from "./replay";
19
+ export { DEFAULT_API_URL };
27
20
  /**
28
- * Trace buffer tuning (spec #122 §A). These are configurable **starting points**
21
+ * Trace buffer **tuning** (spec #122 §A). These are configurable starting points
29
22
  * from the grill — three per-stream ring budgets (network ≈ 100 / console ≈ 80 /
30
23
  * breadcrumbs ≈ 40), a shared age cap (~3 min, on), and **all** console levels
31
24
  * captured (eviction priority, not capture, keeps warn/error above the chatter) —
32
- * never frozen magic numbers. Pass `false` for {@link InitOptions.trace} to disable
33
- * capture entirely.
25
+ * never frozen magic numbers.
26
+ *
27
+ * Tuning only: whether a stream is captured at all is {@link InitOptions.capture}'s
28
+ * job, and whether the SDK runs at all is {@link InitOptions.enabled}'s.
34
29
  */
35
30
  export interface TraceOptions {
36
31
  /**
@@ -43,15 +38,19 @@ export interface TraceOptions {
43
38
  readonly maxAgeMs?: number;
44
39
  /** Console levels captured. Defaults to **all** levels (log/info/warn/error/assert/debug). */
45
40
  readonly consoleLevels?: readonly BreadcrumbLevel[];
46
- /** Per-crumb filter: mute a category, edit a crumb, or drop it (`null`). */
47
- readonly beforeBreadcrumb?: BeforeBreadcrumb;
48
41
  }
49
- /** Options for {@link init}. Only `key` is required. */
50
- export interface InitOptions extends IdentityInputs {
42
+ /**
43
+ * Options for {@link init}. Only `key` is required.
44
+ *
45
+ * The shared half (`apiUrl`, `release`, `environment`, `enabled`, `scrub`,
46
+ * `beforeSend`, `beforeBreadcrumb`, `capture`) comes from `CommonInitOptions`, so
47
+ * it means exactly the same here as in `@fixback/expo` and `@fixback/node`
48
+ * (ADR-0028); the `hostIdentity` escape hatch and an optional `anonymousId` come
49
+ * from `IdentityInputs`; what follows is what only a browser has.
50
+ */
51
+ export interface InitOptions extends CommonCaptureInitOptions, IdentityInputs {
51
52
  /** The Project's **publishable** key — an identifier that ships in the page. */
52
53
  readonly key: string;
53
- /** The Fixback API origin. Defaults to {@link DEFAULT_API_URL}. */
54
- readonly apiUrl?: string;
55
54
  /** Where to mount the launcher. Defaults to `document.body`. */
56
55
  readonly target?: HTMLElement;
57
56
  /**
@@ -60,36 +59,14 @@ export interface InitOptions extends IdentityInputs {
60
59
  * honours on its own.
61
60
  */
62
61
  readonly reduceMotion?: boolean;
63
- /**
64
- * The synchronous, network-free client scrub hook every report passes through
65
- * before transport (spec §C). Mutate the draft to scrub further, or return
66
- * `null` to drop the report. Runs after the default scrubbers.
67
- */
68
- readonly beforeSend?: BeforeSend;
69
- /** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
70
- readonly scrub?: boolean;
71
- /** Trace buffer tuning, or `false` to turn the buffer off entirely. */
72
- readonly trace?: TraceOptions | false;
62
+ /** Trace buffer tuning (sizes, age cap, console levels) — see {@link TraceOptions}. */
63
+ readonly trace?: TraceOptions;
73
64
  /**
74
65
  * Buffered session-replay tuning (issue #189, ADR-0024) — masking defaults on:
75
- * all inputs, all text, SDK chrome blocked or `false` to turn the recorder
76
- * off entirely regardless of the served config.
77
- */
78
- readonly replay?: ReplayOptions | false;
79
- /**
80
- * The dev-side override of the Project's server-served capture config (spec #122
81
- * §L; ticket #138). Capture is **default-on** and normally governed per-project
82
- * from the dashboard, surfaced on the boot answer; set a stream here to override
83
- * what the server serves for it — `{ network: false }` turns network capture off
84
- * even where the Project leaves it on, and a stream left unset follows the served
85
- * config. Independent of {@link trace} `false`, which turns the whole buffer off,
86
- * and of {@link replay} `false`, which turns the recorder off.
66
+ * all inputs, all text, SDK chrome blocked. To turn the recorder off, set
67
+ * `capture: { replay: false }`.
87
68
  */
88
- readonly capture?: {
89
- readonly console?: boolean;
90
- readonly network?: boolean;
91
- readonly replay?: boolean;
92
- };
69
+ readonly replay?: ReplayOptions;
93
70
  /**
94
71
  * Automatic error capture — the SDK files uncaught exceptions / unhandled
95
72
  * rejections as `source: error` Feedback with no prompt (spec §E, ADR-0011).
@@ -98,50 +75,39 @@ export interface InitOptions extends IdentityInputs {
98
75
  * never filed where a manual report would be refused.
99
76
  */
100
77
  readonly autoCapture?: boolean;
101
- /**
102
- * The host app's **Release** — a build identifier (a git SHA, a semver, a
103
- * `name@version`) stamped into every submission's environment (#117,
104
- * ADR-0024). Set it to the same value the build uploaded sourcemaps under
105
- * (`npx fixback sourcemaps upload --release <v>`), and auto-captured errors
106
- * gain a symbolicated **code-area pointer** on their Issues. Optional: without
107
- * it (or without uploaded sourcemaps) everything behaves exactly as before.
108
- * An invalid value (whitespace, slashes, over 100 chars) is ignored with a
109
- * console warning.
110
- */
111
- readonly release?: string;
112
- }
113
- /**
114
- * Options for {@link redeem} — the explicit form of the `?fixback_invite=` URL
115
- * detection {@link init} performs. Identical to {@link InitOptions} plus the
116
- * `token` to redeem (the value the invite link carried).
117
- */
118
- export interface RedeemOptions extends InitOptions {
119
- /** The invite token to redeem — the `?fixback_invite=` value from the link. */
120
- readonly token: string;
121
78
  }
122
79
  /** A running SDK instance. */
123
80
  export interface FixbackInstance {
124
81
  /** Remove the launcher and release its DOM. Safe to call more than once. */
125
82
  destroy(): void;
83
+ /**
84
+ * Start a **Connect** (ADR-0032): open the platform's connect page, sign the person
85
+ * in, and bind their Account to this site as a Reporter session. Resolves to the
86
+ * resulting identity. The way to enter a gated Project (which shows no launcher to a
87
+ * signed-out visitor) from the host app's own UI.
88
+ */
89
+ signIn(): Promise<ReporterIdentity>;
90
+ /** Clear the Reporter session — revoke it server-side and forget the token locally. */
91
+ signOut(): Promise<void>;
92
+ /** The current Reporter identity — anonymous, or a connected Account with its tier. */
93
+ identity(): ReporterIdentity;
126
94
  }
127
95
  /**
128
- * Boot the SDK and mount the launcher when and only when — a submission would
129
- * be accepted for this key, origin, and Gate.
130
- *
131
- * On load the SDK first checks the page URL for an invite token
132
- * (`?fixback_invite=`, spec §F); when one is present and live it renders the
133
- * onboarding modal and defers the launcher until the tester redeems. Otherwise it
134
- * calls the ingest boot endpoint and mounts the launcher solely when the answer's
135
- * `canSubmit` is true — so a Reporter is never shown a launcher a submission would
136
- * be refused, and on an Invited / Internal Gate the launcher stays absent until
137
- * redemption. Everything is wrapped so a Fixback problem — unreachable, refused,
138
- * or an unexpected error — resolves to a no-op instance.
96
+ * The module-level Connect surface (ADR-0032). `Fixback.signIn()` lets a host app start
97
+ * a Connect from its own UI — the entry point for a gated Project that shows nothing to
98
+ * a signed-out visitor. It delegates to the most recent {@link init} instance.
139
99
  */
140
- export declare function init(options: InitOptions): Promise<FixbackInstance>;
100
+ export declare const Fixback: {
101
+ signIn: () => Promise<ReporterIdentity>;
102
+ signOut: () => Promise<void>;
103
+ identity: () => ReporterIdentity;
104
+ };
141
105
  /**
142
- * Redeem an invite token explicitly, the programmatic equivalent of landing on a
143
- * page with `?fixback_invite=<token>`. Renders the onboarding modal, and on
144
- * confirm redeems, persists the `reporterId`, and mounts the launcher (spec §F).
145
- * Use it when the token reaches the page some way other than the URL.
106
+ * Boot the SDK and mount the launcher when and only when — a submission would be
107
+ * accepted for this key, origin, and Gate. On an Open Gate the launcher (and its
108
+ * identity chip) mount for an anonymous visitor; on a gated Project the launcher stays
109
+ * absent until the Account is eligible, entered through `Fixback.signIn()` or a
110
+ * Fixback-issued link's `?fixback=` code. Everything is wrapped so a Fixback problem —
111
+ * unreachable, refused, or an unexpected error — resolves to a no-op instance.
146
112
  */
147
- export declare function redeem(options: RedeemOptions): Promise<FixbackInstance>;
113
+ export declare function init(options: InitOptions): Promise<FixbackInstance>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Mounting the running SDK once boot has said yes.
3
+ *
4
+ * `mountReporter` is the one place the four live pieces are created and wired
5
+ * together — the launcher, the report overlay it opens, the trace buffer and its
6
+ * instrumentation (spec §C), and automatic error capture (spec §E) — and the one
7
+ * place they are all torn down. It runs **only** with a boot answer whose
8
+ * `canSubmit` is true, so every decision here can assume the Gate is already
9
+ * satisfied: the overlay and the auto-capture handlers carry the resolved identity,
10
+ * and both a filed report and an auto-captured error are attributed to the very
11
+ * Reporter boot recognised.
12
+ *
13
+ * The boot answer's `gate` + `tier` ride into the overlay with no second call:
14
+ * `tier` drives the display-only header chip (§G), `gate` is reserved for
15
+ * Gate-aware behaviour.
16
+ */
17
+ import { type BootAnswer, type IdentityInputs, type ReporterIdentity, type Teardown } from "@fixback/sdk-core";
18
+ import type { InitOptions } from "./init";
19
+ /** Context for mounting the launcher + overlay + trace once boot says yes. */
20
+ export interface MountContext {
21
+ readonly options: InitOptions;
22
+ readonly apiUrl: string;
23
+ readonly key: string;
24
+ /**
25
+ * The identity evidence to forward on each report / auto-capture, read **live** at
26
+ * send time — so a report filed after an in-overlay Connect carries the new Reporter
27
+ * session token, not the stale one the overlay mounted with (ADR-0032).
28
+ */
29
+ readonly getIdentity: () => IdentityInputs;
30
+ /** The current Reporter identity for the chip (ADR-0032) — anonymous or connected. */
31
+ readonly reporterIdentity: ReporterIdentity;
32
+ /** The chip's **Sign in** — starts a Connect (ADR-0032). */
33
+ readonly onSignIn: () => void;
34
+ /** The chip's **Sign out** — clears the Reporter session (ADR-0032). */
35
+ readonly onSignOut: () => void;
36
+ readonly target: HTMLElement;
37
+ readonly answer: BootAnswer;
38
+ }
39
+ /** A mounted reporter: how to tear it down, and how to update its identity chip. */
40
+ export interface MountHandle {
41
+ readonly teardown: Teardown;
42
+ /** Update the identity chip in place after a Connect or sign-out (ADR-0032). */
43
+ setIdentity(identity: ReporterIdentity): void;
44
+ }
45
+ /** Mount the launcher, overlay, trace, and auto-capture. Returns a teardown + chip updater. */
46
+ export declare function mountReporter(ctx: MountContext): MountHandle;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The two element constructors the overlay's view is built from.
3
+ *
4
+ * Both take the `Document` explicitly rather than closing over a global, so the
5
+ * whole overlay can be built into any document (a test's, an iframe's) — and both
6
+ * set attributes rather than assigning properties, which keeps the built markup a
7
+ * faithful match for the frozen Signal prototype.
8
+ */
9
+ /** The SVG namespace, for the draw surface's mark layer and the toolbar icons. */
10
+ export declare const SVG_NS = "http://www.w3.org/2000/svg";
11
+ /** Marks the overlay's host element in the light DOM (skipped by capture/picker). */
12
+ export declare const OVERLAY_ATTRIBUTE = "data-fixback-overlay";
13
+ /** Create an HTML element with attributes and children (text or nodes). */
14
+ export declare function h<K extends keyof HTMLElementTagNameMap>(doc: Document, tag: K, props?: Partial<Record<string, string>>, children?: ReadonlyArray<Node | string>): HTMLElementTagNameMap[K];
15
+ /** Create a namespaced SVG element with attributes and optional SVG children. */
16
+ export declare function svgNode(doc: Document, tag: string, attrs?: Record<string, string>, children?: ReadonlyArray<SVGElement>): SVGElement;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The overlay's inline **icons** — the glyphs on the element-pick, region-capture,
3
+ * undo, and draw-tool controls, matching the frozen Signal Reporter prototype
4
+ * (`docs/design/Fixback Reporter.dc.html`).
5
+ *
6
+ * They are built as DOM rather than shipped as an icon font or sprite sheet: the
7
+ * SDK stays dependency-free and self-isolating, so every pixel it draws is created
8
+ * inside its own Shadow DOM with no external asset to load or fail.
9
+ */
10
+ import type { DrawTool } from "../annotation";
11
+ /** The draw tools, in toolbar order (spec §B, the Reporter prototype). */
12
+ export declare const DRAW_TOOLS: ReadonlyArray<{
13
+ tool: DrawTool;
14
+ label: string;
15
+ }>;
16
+ /** The crosshair glyph on the element-pick control (matches the `⌖` in the prototype). */
17
+ export declare function pickGlyph(doc: Document): HTMLElement;
18
+ /** The frame glyph on the region-capture control (matches the prototype's crop icon). */
19
+ export declare function captureGlyph(doc: Document): SVGElement;
20
+ /** The undo glyph on the draw toolbar. */
21
+ export declare function undoGlyph(doc: Document): SVGElement;
22
+ /** The per-tool glyph on the draw toolbar (matches the prototype's icons). */
23
+ export declare function drawToolGlyph(doc: Document, tool: DrawTool): Node;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The overlay's **marking state machine** (spec §B): the three composable,
3
+ * optional layers a Reporter can put over the current view — **element-pick**,
4
+ * **region-capture** (drag), and **draw** (arrow / box / pen / text) — and the
5
+ * mutually-exclusive full-screen modes that collect them.
6
+ *
7
+ * Exactly one marking layer runs at a time, so starting one closes the others; the
8
+ * panel hides while any of them is up. What survives is pure data — a picked
9
+ * element, a viewport-space region, and a list of vector marks — which is what the
10
+ * send path assembles into the structured Annotation. Marks are **never** baked
11
+ * into an image: they are viewport-space vectors the dashboard composites at view
12
+ * time (spec §D).
13
+ *
14
+ * The controller owns no DOM of its own: it positions the layers the view built
15
+ * and reports every change through `onChange`, so the panel's summary line and the
16
+ * send path read one state.
17
+ */
18
+ import type { SelectedElement } from "@fixback/sdk-core";
19
+ import type { DrawTool, Mark, Rect } from "../annotation";
20
+ import { type ElementPicker, type ElementPickerOptions } from "../element-picker";
21
+ import type { Refs } from "./view";
22
+ /** Which full-screen marking mode is active, if any. */
23
+ export type MarkMode = "compose" | "picking" | "capturing" | "drawing";
24
+ /** Everything the Reporter has marked so far — the send path's input. */
25
+ export interface MarkingState {
26
+ readonly selectedElement: SelectedElement | null;
27
+ readonly region: Rect | null;
28
+ readonly marks: readonly Mark[];
29
+ }
30
+ /** The element-picker factory, injectable so the overlay is testable without pointer events. */
31
+ export type StartPickerFn = (options: ElementPickerOptions) => ElementPicker;
32
+ /** Configuration for {@link createMarkingController}. */
33
+ export interface MarkingConfig {
34
+ readonly doc: Document;
35
+ readonly refs: Refs;
36
+ readonly startElementPicker: StartPickerFn;
37
+ /** Called whenever the marked state or the active mode changes. */
38
+ readonly onChange: () => void;
39
+ }
40
+ /** The running marking state machine the overlay drives. */
41
+ export interface MarkingController {
42
+ /** The current mode — `compose` when no marking layer is up. */
43
+ mode(): MarkMode;
44
+ /** What has been marked so far. */
45
+ state(): MarkingState;
46
+ /** Toggle the element picker (start it, or end it if it is already running). */
47
+ togglePick(): void;
48
+ /** Start the region-capture drag, which flows into the draw layer on completion. */
49
+ beginCapture(): void;
50
+ /** Select a draw tool (arrow / box / pen / text). */
51
+ setTool(tool: DrawTool): void;
52
+ /** Undo the last mark on the draw surface. */
53
+ undo(): void;
54
+ /** Attach the in-flight capture: its marks and region become part of the report. */
55
+ attachDraw(): void;
56
+ /** Discard the in-flight capture; previously attached marking is untouched. */
57
+ cancelDraw(): void;
58
+ /** Tear down any active marking layer (before starting another, or on close). */
59
+ closeAll(): void;
60
+ /** Forget every mark — called when the overlay opens a fresh report. */
61
+ reset(): void;
62
+ }
63
+ /** Create the marking state machine over an already-built overlay view. */
64
+ export declare function createMarkingController(config: MarkingConfig): MarkingController;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The overlay's **Send** path: assemble what the Reporter composed, run it through
3
+ * the single client-side scrub choke point, and hand it to the transport.
4
+ *
5
+ * Kept free of any DOM so the whole decision sequence — what is assembled, what the
6
+ * hook may drop, when the replay window is read — is testable without an overlay.
7
+ * The order matters and is asserted by its tests: the draft is assembled, then
8
+ * scrubbed (spec §C), and only a draft the hook *accepted* causes the buffered
9
+ * replay window to be read. A dropped report sends nothing at all.
10
+ */
11
+ import { type Breadcrumb, type BeforeSend, type IdentityInputs } from "@fixback/sdk-core";
12
+ import type { ReplaySnapshot, ReplaySource } from "../replay";
13
+ import type { SubmitDeps, SubmitInput, SubmitResult } from "../submit";
14
+ import type { MarkingState } from "./marking";
15
+ /** A read-only view of the trace buffer the overlay attaches to a report. */
16
+ export interface BreadcrumbSource {
17
+ snapshot(): readonly Breadcrumb[];
18
+ }
19
+ /** The transport call the send path makes — injectable for tests. */
20
+ export type SubmitReportFn = (apiUrl: string, input: SubmitInput, deps?: SubmitDeps) => Promise<SubmitResult>;
21
+ /** Everything the send path needs that does not change between sends. */
22
+ export interface SendContext {
23
+ readonly apiUrl: string;
24
+ readonly key: string;
25
+ readonly identity?: IdentityInputs;
26
+ readonly sdkVersion: string;
27
+ /** The host app's Release (#117, ADR-0024), validated by `init`. */
28
+ readonly release?: string;
29
+ /** The deploy environment (`production` / `staging` / …), stamped on every report. */
30
+ readonly deployEnvironment?: string;
31
+ /** The window the page URL and viewport are read from. */
32
+ readonly win: Window;
33
+ /** The trace buffer whose snapshot rides on the report (spec §C). */
34
+ readonly buffer?: BreadcrumbSource | null;
35
+ /** The replay recorder, read only once the scrub hook has accepted the report. */
36
+ readonly replay?: ReplaySource | null;
37
+ /** Per-project client scrub hook, run at the `beforeSend` choke point. */
38
+ readonly beforeSend?: BeforeSend;
39
+ /** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
40
+ readonly scrub?: boolean;
41
+ readonly submitReport: SubmitReportFn;
42
+ }
43
+ /** What one Send carries beyond the context. */
44
+ export interface SendDraft {
45
+ readonly comment: string;
46
+ readonly marking: MarkingState;
47
+ /**
48
+ * The replay window frozen when the overlay **opened** (ADR-0024) — the lead-up
49
+ * to the bug, not to Send. `null` falls back to a snapshot taken now.
50
+ */
51
+ readonly openedReplay: ReplaySnapshot | null;
52
+ }
53
+ /**
54
+ * The outcome of a Send. `dropped` is the project's own `beforeSend` returning
55
+ * `null`: an intentional non-send, which the overlay confirms exactly like a
56
+ * delivered report — the Reporter did nothing wrong and must not see an error.
57
+ */
58
+ export type SendOutcome = {
59
+ readonly kind: "dropped";
60
+ } | {
61
+ readonly kind: "submitted";
62
+ readonly result: SubmitResult;
63
+ };
64
+ /** Assemble, scrub, and submit one report. See the module doc for the ordering. */
65
+ export declare function sendReport(context: SendContext, draft: SendDraft): Promise<SendOutcome>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The overlay's **view**: one function that builds the whole Shadow DOM tree and
3
+ * hands back typed references to the nodes the controller drives.
4
+ *
5
+ * It is deliberately behaviour-free. Every control is wired to a callback from
6
+ * {@link OverlayHandlers}, so this module never decides anything — it renders the
7
+ * frozen Signal Reporter prototype (`docs/design/Fixback Reporter.dc.html`) and
8
+ * nothing more, which is what keeps the markup reviewable against the design and
9
+ * the state machine reviewable on its own (`./marking`, `./send`).
10
+ */
11
+ import type { DrawTool } from "../annotation";
12
+ import type { ReporterIdentity } from "@fixback/sdk-core";
13
+ /** The nodes the overlay controller reads and mutates after building. */
14
+ export interface Refs {
15
+ readonly panel: HTMLElement;
16
+ /** The identity chip container — "Anonymous · Sign in" / "<name> · <tier> · Sign out". */
17
+ readonly identity: HTMLElement;
18
+ readonly mark: HTMLElement;
19
+ readonly selector: HTMLElement;
20
+ readonly caption: HTMLElement;
21
+ readonly comment: HTMLTextAreaElement;
22
+ readonly status: HTMLElement;
23
+ readonly pickBtn: HTMLButtonElement;
24
+ readonly captureBtn: HTMLButtonElement;
25
+ readonly sendBtn: HTMLButtonElement;
26
+ readonly highlight: HTMLElement;
27
+ readonly pickLayer: HTMLElement;
28
+ readonly captureLayer: HTMLElement;
29
+ readonly selBox: HTMLElement;
30
+ readonly drawLayer: HTMLElement;
31
+ readonly drawSvg: SVGSVGElement;
32
+ readonly drawText: HTMLInputElement;
33
+ readonly drawFrame: HTMLElement;
34
+ readonly drawTools: Map<DrawTool, HTMLButtonElement>;
35
+ readonly undoBtn: HTMLButtonElement;
36
+ }
37
+ /** What each control does — supplied by the controller, never decided here. */
38
+ export interface OverlayHandlers {
39
+ readonly onClose: () => void;
40
+ readonly onTogglePick: () => void;
41
+ readonly onBeginCapture: () => void;
42
+ readonly onSend: () => void;
43
+ readonly onSelectTool: (tool: DrawTool) => void;
44
+ readonly onUndo: () => void;
45
+ readonly onCancelDraw: () => void;
46
+ readonly onAttachDraw: () => void;
47
+ /** The identity chip's **Sign in** — starts a Connect (ADR-0032). */
48
+ readonly onSignIn: () => void;
49
+ /** The identity chip's **Sign out** — clears the Reporter session (ADR-0032). */
50
+ readonly onSignOut: () => void;
51
+ }
52
+ /** What {@link buildOverlay} needs beyond its handlers. */
53
+ export interface OverlayViewConfig {
54
+ readonly doc: Document;
55
+ /** Where to mount the host element. Defaults to the document's `body`. */
56
+ readonly target?: HTMLElement;
57
+ /** The initial identity to render in the chip (ADR-0032). */
58
+ readonly identity: ReporterIdentity;
59
+ }
60
+ /**
61
+ * Render the identity chip's contents for the given identity (ADR-0032). Anonymous
62
+ * shows a **Sign in** button; a connected Account shows its name, its server-derived
63
+ * tier, and a **Sign out** button. Called on build and again whenever the identity
64
+ * changes, so the chip stays live without re-mounting the overlay. The tier is
65
+ * display-only — never a trust signal, never read for a decision.
66
+ */
67
+ export declare function renderIdentityChip(doc: Document, container: HTMLElement, identity: ReporterIdentity, handlers: Pick<OverlayHandlers, "onSignIn" | "onSignOut">): void;
68
+ /** A built overlay: its light-DOM host and the refs its controller drives. */
69
+ export interface OverlayView {
70
+ readonly host: HTMLElement;
71
+ readonly refs: Refs;
72
+ }
73
+ /**
74
+ * Build the overlay into a fresh Shadow DOM host, append it to the target, and
75
+ * return the host plus its refs. The host starts hidden — the controller shows it
76
+ * on `open()`.
77
+ */
78
+ export declare function buildOverlay(config: OverlayViewConfig, handlers: OverlayHandlers): OverlayView;
@@ -6,4 +6,4 @@
6
6
  * vendored Signal tokens (copied from `packages/ui/src/tokens.css`, not imported —
7
7
  * the SDK must not depend on `@fixback/ui` at runtime). Keep them in sync by value.
8
8
  */
9
- export declare const OVERLAY_STYLES = "\n:host {\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-ink: #0f1720;\n --fb-color-text: #1a2530;\n --fb-color-muted: #5a6875;\n --fb-color-faint: #9aa7b2;\n --fb-color-border: #e0e6ec;\n --fb-color-border-soft: #e6ebf0;\n --fb-color-surface: #ffffff;\n --fb-color-bug: #e5484d;\n --fb-color-bug-bg: #fdecec;\n --fb-color-impr: #2f6fed;\n --fb-color-impr-bg: #eaf1fe;\n --fb-color-success: #2f9e5b;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n --fb-font-mono: \"IBM Plex Mono\", ui-monospace, \"SFMono-Regular\", Menlo, Consolas,\n monospace;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.45;\n -webkit-font-smoothing: antialiased;\n}\n\n* { box-sizing: border-box; }\n\n.fb-ov-panel {\n width: 300px;\n max-width: calc(100vw - 40px);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 13px;\n box-shadow: 0 18px 44px rgba(20, 40, 70, 0.2);\n overflow: hidden;\n}\n\n.fb-ov-head {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--fb-color-border-soft);\n}\n.fb-ov-mark-sq {\n width: 13px;\n height: 13px;\n border-radius: 4px;\n background: var(--fb-color-accent);\n flex: none;\n}\n.fb-ov-brand { font-size: 14px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-tier {\n flex: none;\n font-family: var(--fb-font-mono);\n font-size: 9.5px;\n color: var(--fb-color-faint);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 5px;\n padding: 1px 6px;\n}\n.fb-ov-close {\n margin-left: auto;\n border: 0;\n background: none;\n color: var(--fb-color-faint);\n font-size: 16px;\n line-height: 1;\n padding: 2px 4px;\n cursor: pointer;\n border-radius: 6px;\n}\n.fb-ov-close:hover { color: var(--fb-color-muted); background: #f4f7fa; }\n\n.fb-ov-mark {\n margin: 12px 14px 10px;\n min-height: 52px;\n border-radius: 9px;\n border: 1px solid var(--fb-color-border-soft);\n background: repeating-linear-gradient(135deg, #f4f7fa, #f4f7fa 7px, #eaeff4 7px, #eaeff4 14px);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n gap: 6px;\n padding: 10px 12px;\n}\n.fb-ov-selector {\n display: none;\n max-width: 100%;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n padding: 3px 8px;\n border-radius: 5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.fb-ov-mark.has-element .fb-ov-selector { display: inline-block; }\n.fb-ov-mark-caption { font-size: 10px; color: var(--fb-color-faint); font-family: var(--fb-font-mono); }\n\n.fb-ov-comment {\n display: block;\n width: calc(100% - 28px);\n margin: 0 14px 10px;\n min-height: 62px;\n resize: vertical;\n font-family: inherit;\n font-size: 13px;\n color: var(--fb-color-text);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 9px;\n padding: 10px 11px;\n}\n.fb-ov-comment::placeholder { color: var(--fb-color-faint); }\n.fb-ov-comment:focus-visible { outline: 2px solid var(--fb-color-accent); outline-offset: 1px; }\n\n.fb-ov-status { padding: 0 14px; font-size: 11px; min-height: 0; }\n.fb-ov-status.is-error { color: var(--fb-color-bug); }\n\n.fb-ov-tools { display: flex; align-items: center; gap: 7px; padding: 8px 14px 14px; }\n.fb-ov-pickbtn,\n.fb-ov-capturebtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--fb-color-muted);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 8px;\n padding: 8px 11px;\n cursor: pointer;\n}\n.fb-ov-capturebtn { padding: 8px 9px; }\n.fb-ov-pickbtn:hover,\n.fb-ov-capturebtn:hover { border-color: #cfd8e2; }\n.fb-ov-pickbtn.is-active,\n.fb-ov-capturebtn.is-active,\n.fb-ov-capturebtn.is-attached {\n color: var(--fb-color-accent);\n border-color: var(--fb-color-accent);\n background: var(--fb-color-impr-bg);\n}\n.fb-ov-pickbtn__glyph { font-size: 14px; line-height: 1; }\n.fb-ov-icon { display: block; }\n\n.fb-ov-send {\n margin-left: auto;\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n border: 0;\n border-radius: 8px;\n padding: 9px 18px;\n cursor: pointer;\n}\n.fb-ov-send:hover { background: var(--fb-color-accent-hover); }\n.fb-ov-send:disabled { opacity: 0.6; cursor: default; }\n\n.fb-ov-done { display: none; padding: 24px 18px; text-align: center; }\n.fb-ov-panel.is-sent .fb-ov-form { display: none; }\n.fb-ov-panel.is-sent .fb-ov-done { display: block; }\n.fb-ov-done__check {\n width: 40px; height: 40px; margin: 0 auto 12px;\n border-radius: 50%;\n background: #e7f6ee; color: var(--fb-color-success);\n display: flex; align-items: center; justify-content: center;\n font-size: 20px; font-weight: 700;\n}\n.fb-ov-done__title { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-done__sub { font-size: 12px; color: var(--fb-color-muted); margin-top: 4px; }\n\n/* The panel is hidden while any full-screen marking layer is active. */\n.fb-ov-panel.is-marking { visibility: hidden; }\n\n.fb-ov-pick,\n.fb-ov-capture,\n.fb-ov-draw { position: fixed; inset: 0; z-index: 2147483002; display: none; }\n.fb-ov-pick.is-visible,\n.fb-ov-capture.is-visible,\n.fb-ov-draw.is-visible { display: block; }\n\n/* Element-pick highlight layer \u2014 visual only, never intercepts host events. */\n.fb-ov-pick { pointer-events: none; }\n.fb-ov-highlight {\n position: absolute;\n border: 2px dashed var(--fb-color-accent);\n border-radius: 6px;\n box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);\n transition: all 60ms ease;\n}\n.fb-ov-hint {\n position: absolute;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n font-family: var(--fb-font-mono);\n font-size: 11px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-ink);\n padding: 6px 12px;\n border-radius: 7px;\n box-shadow: 0 8px 20px rgba(20, 40, 70, 0.25);\n}\n\n/* Region-capture layer \u2014 a dim wash with a live selection box. */\n.fb-ov-capture { cursor: crosshair; }\n.fb-ov-capture-dim { position: absolute; inset: 0; background: rgba(15, 23, 32, 0.28); }\n.fb-ov-capture-sel {\n position: absolute;\n border: 2px solid var(--fb-color-accent);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.32);\n}\n\n/* Draw layer \u2014 the mark SVG, the region frame, the label input, and the toolbar. */\n.fb-ov-draw-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n.fb-ov-draw-frame {\n position: absolute;\n border: 1px solid rgba(47, 111, 237, 0.5);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.18);\n pointer-events: none;\n}\n.fb-ov-draw-text {\n position: absolute;\n display: none;\n font-family: var(--fb-font-sans);\n font-size: 15px;\n font-weight: 600;\n color: var(--fb-color-bug);\n background: rgba(255, 255, 255, 0.92);\n border: 1px dashed var(--fb-color-bug);\n border-radius: 4px;\n padding: 2px 6px;\n outline: none;\n z-index: 3;\n}\n.fb-ov-draw-toolbar {\n position: fixed;\n left: 50%;\n bottom: 26px;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 5px;\n background: var(--fb-color-ink);\n border-radius: 12px;\n padding: 7px;\n box-shadow: 0 16px 40px rgba(15, 40, 70, 0.4);\n}\n.fb-ov-drawtool {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-drawtool:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-drawtool.is-active { background: var(--fb-color-accent); color: var(--fb-color-on-emphasis); }\n.fb-ov-drawtool__t { font-weight: 700; font-size: 14px; line-height: 1; }\n.fb-ov-draw-divider { width: 1px; height: 22px; background: #2a343e; margin: 0 3px; }\n.fb-ov-draw-undo {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n}\n.fb-ov-draw-undo:hover:not(:disabled) { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-draw-undo:disabled { opacity: 0.4; cursor: default; }\n.fb-ov-draw-cancel {\n height: 34px;\n padding: 0 12px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fb-color-faint);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n.fb-ov-draw-cancel:hover { color: #fff; }\n.fb-ov-draw-attach {\n height: 34px;\n padding: 0 15px;\n border: 0;\n border-radius: 8px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n.fb-ov-draw-attach:hover { background: var(--fb-color-accent-hover); }\n\n@media (prefers-reduced-motion: reduce) {\n .fb-ov-highlight { transition: none; }\n}\n";
9
+ export declare const OVERLAY_STYLES = "\n:host {\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-ink: #0f1720;\n --fb-color-text: #1a2530;\n --fb-color-muted: #5a6875;\n --fb-color-faint: #9aa7b2;\n --fb-color-border: #e0e6ec;\n --fb-color-border-soft: #e6ebf0;\n --fb-color-surface: #ffffff;\n --fb-color-bug: #e5484d;\n --fb-color-bug-bg: #fdecec;\n --fb-color-impr: #2f6fed;\n --fb-color-impr-bg: #eaf1fe;\n --fb-color-success: #2f9e5b;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n --fb-font-mono: \"IBM Plex Mono\", ui-monospace, \"SFMono-Regular\", Menlo, Consolas,\n monospace;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.45;\n -webkit-font-smoothing: antialiased;\n}\n\n* { box-sizing: border-box; }\n\n.fb-ov-panel {\n width: 300px;\n max-width: calc(100vw - 40px);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 13px;\n box-shadow: 0 18px 44px rgba(20, 40, 70, 0.2);\n overflow: hidden;\n}\n\n.fb-ov-head {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--fb-color-border-soft);\n}\n.fb-ov-mark-sq {\n width: 13px;\n height: 13px;\n border-radius: 4px;\n background: var(--fb-color-accent);\n flex: none;\n}\n.fb-ov-brand { font-size: 14px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-tier {\n flex: none;\n font-family: var(--fb-font-mono);\n font-size: 9.5px;\n color: var(--fb-color-faint);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 5px;\n padding: 1px 6px;\n}\n/* The identity chip (ADR-0032): \"Anonymous \u00B7 Sign in\", or \"<name> \u00B7 <tier> \u00B7 Sign out\". */\n.fb-ov-identity {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n.fb-ov-ident-name {\n font-size: 11.5px;\n color: var(--fb-color-faint);\n max-width: 140px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.fb-ov-signin,\n.fb-ov-signout {\n flex: none;\n border: 1px solid var(--fb-color-border-soft);\n background: none;\n border-radius: 5px;\n padding: 1px 7px;\n font-size: 10.5px;\n color: var(--fb-color-accent);\n cursor: pointer;\n}\n.fb-ov-signin:hover,\n.fb-ov-signout:hover { border-color: var(--fb-color-accent); }\n.fb-ov-signout { color: var(--fb-color-faint); }\n.fb-ov-close {\n margin-left: auto;\n border: 0;\n background: none;\n color: var(--fb-color-faint);\n font-size: 16px;\n line-height: 1;\n padding: 2px 4px;\n cursor: pointer;\n border-radius: 6px;\n}\n.fb-ov-close:hover { color: var(--fb-color-muted); background: #f4f7fa; }\n\n.fb-ov-mark {\n margin: 12px 14px 10px;\n min-height: 52px;\n border-radius: 9px;\n border: 1px solid var(--fb-color-border-soft);\n background: repeating-linear-gradient(135deg, #f4f7fa, #f4f7fa 7px, #eaeff4 7px, #eaeff4 14px);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n gap: 6px;\n padding: 10px 12px;\n}\n.fb-ov-selector {\n display: none;\n max-width: 100%;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n padding: 3px 8px;\n border-radius: 5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.fb-ov-mark.has-element .fb-ov-selector { display: inline-block; }\n.fb-ov-mark-caption { font-size: 10px; color: var(--fb-color-faint); font-family: var(--fb-font-mono); }\n\n.fb-ov-comment {\n display: block;\n width: calc(100% - 28px);\n margin: 0 14px 10px;\n min-height: 62px;\n resize: vertical;\n font-family: inherit;\n font-size: 13px;\n color: var(--fb-color-text);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 9px;\n padding: 10px 11px;\n}\n.fb-ov-comment::placeholder { color: var(--fb-color-faint); }\n.fb-ov-comment:focus-visible { outline: 2px solid var(--fb-color-accent); outline-offset: 1px; }\n\n.fb-ov-status { padding: 0 14px; font-size: 11px; min-height: 0; }\n.fb-ov-status.is-error { color: var(--fb-color-bug); }\n\n.fb-ov-tools { display: flex; align-items: center; gap: 7px; padding: 8px 14px 14px; }\n.fb-ov-pickbtn,\n.fb-ov-capturebtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--fb-color-muted);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 8px;\n padding: 8px 11px;\n cursor: pointer;\n}\n.fb-ov-capturebtn { padding: 8px 9px; }\n.fb-ov-pickbtn:hover,\n.fb-ov-capturebtn:hover { border-color: #cfd8e2; }\n.fb-ov-pickbtn.is-active,\n.fb-ov-capturebtn.is-active,\n.fb-ov-capturebtn.is-attached {\n color: var(--fb-color-accent);\n border-color: var(--fb-color-accent);\n background: var(--fb-color-impr-bg);\n}\n.fb-ov-pickbtn__glyph { font-size: 14px; line-height: 1; }\n.fb-ov-icon { display: block; }\n\n.fb-ov-send {\n margin-left: auto;\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n border: 0;\n border-radius: 8px;\n padding: 9px 18px;\n cursor: pointer;\n}\n.fb-ov-send:hover { background: var(--fb-color-accent-hover); }\n.fb-ov-send:disabled { opacity: 0.6; cursor: default; }\n\n.fb-ov-done { display: none; padding: 24px 18px; text-align: center; }\n.fb-ov-panel.is-sent .fb-ov-form { display: none; }\n.fb-ov-panel.is-sent .fb-ov-done { display: block; }\n.fb-ov-done__check {\n width: 40px; height: 40px; margin: 0 auto 12px;\n border-radius: 50%;\n background: #e7f6ee; color: var(--fb-color-success);\n display: flex; align-items: center; justify-content: center;\n font-size: 20px; font-weight: 700;\n}\n.fb-ov-done__title { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-done__sub { font-size: 12px; color: var(--fb-color-muted); margin-top: 4px; }\n\n/* The panel is hidden while any full-screen marking layer is active. */\n.fb-ov-panel.is-marking { visibility: hidden; }\n\n.fb-ov-pick,\n.fb-ov-capture,\n.fb-ov-draw { position: fixed; inset: 0; z-index: 2147483002; display: none; }\n.fb-ov-pick.is-visible,\n.fb-ov-capture.is-visible,\n.fb-ov-draw.is-visible { display: block; }\n\n/* Element-pick highlight layer \u2014 visual only, never intercepts host events. */\n.fb-ov-pick { pointer-events: none; }\n.fb-ov-highlight {\n position: absolute;\n border: 2px dashed var(--fb-color-accent);\n border-radius: 6px;\n box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);\n transition: all 60ms ease;\n}\n.fb-ov-hint {\n position: absolute;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n font-family: var(--fb-font-mono);\n font-size: 11px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-ink);\n padding: 6px 12px;\n border-radius: 7px;\n box-shadow: 0 8px 20px rgba(20, 40, 70, 0.25);\n}\n\n/* Region-capture layer \u2014 a dim wash with a live selection box. */\n.fb-ov-capture { cursor: crosshair; }\n.fb-ov-capture-dim { position: absolute; inset: 0; background: rgba(15, 23, 32, 0.28); }\n.fb-ov-capture-sel {\n position: absolute;\n border: 2px solid var(--fb-color-accent);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.32);\n}\n\n/* Draw layer \u2014 the mark SVG, the region frame, the label input, and the toolbar. */\n.fb-ov-draw-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n.fb-ov-draw-frame {\n position: absolute;\n border: 1px solid rgba(47, 111, 237, 0.5);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.18);\n pointer-events: none;\n}\n.fb-ov-draw-text {\n position: absolute;\n display: none;\n font-family: var(--fb-font-sans);\n font-size: 15px;\n font-weight: 600;\n color: var(--fb-color-bug);\n background: rgba(255, 255, 255, 0.92);\n border: 1px dashed var(--fb-color-bug);\n border-radius: 4px;\n padding: 2px 6px;\n outline: none;\n z-index: 3;\n}\n.fb-ov-draw-toolbar {\n position: fixed;\n left: 50%;\n bottom: 26px;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 5px;\n background: var(--fb-color-ink);\n border-radius: 12px;\n padding: 7px;\n box-shadow: 0 16px 40px rgba(15, 40, 70, 0.4);\n}\n.fb-ov-drawtool {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-drawtool:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-drawtool.is-active { background: var(--fb-color-accent); color: var(--fb-color-on-emphasis); }\n.fb-ov-drawtool__t { font-weight: 700; font-size: 14px; line-height: 1; }\n.fb-ov-draw-divider { width: 1px; height: 22px; background: #2a343e; margin: 0 3px; }\n.fb-ov-draw-undo {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n}\n.fb-ov-draw-undo:hover:not(:disabled) { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-draw-undo:disabled { opacity: 0.4; cursor: default; }\n.fb-ov-draw-cancel {\n height: 34px;\n padding: 0 12px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fb-color-faint);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n.fb-ov-draw-cancel:hover { color: #fff; }\n.fb-ov-draw-attach {\n height: 34px;\n padding: 0 15px;\n border: 0;\n border-radius: 8px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n.fb-ov-draw-attach:hover { background: var(--fb-color-accent-hover); }\n\n@media (prefers-reduced-motion: reduce) {\n .fb-ov-highlight { transition: none; }\n}\n";