@weatherboard/annotation 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { type AnnotationPayload, type BuildInfo } from "./protocol.cjs";
1
+ import { type AnnotationPayload, type BuildInfo, type TargetInfo } from "./protocol.cjs";
2
+ import { type AnnotationTransport, type EgressErrorHandler, type EgressOutcome } from "./transport.cjs";
2
3
  import { type ScreenshotOptions } from "./screenshot.cjs";
3
4
  /**
4
5
  * The annotation SDK (A-36).
@@ -11,13 +12,52 @@ export interface AnnotationState {
11
12
  failedRequests: AnnotationPayload["state"]["failedRequests"];
12
13
  featureFlags: Record<string, unknown>;
13
14
  }
15
+ /**
16
+ * What the reviewer clicked, handed to whoever collects the words (A-240).
17
+ *
18
+ * The target is the RESOLVED one, taken from the payload that was already
19
+ * assembled — not the element, and not something to re-read. See `onClick` for
20
+ * why that distinction is the whole of this feature's correctness.
21
+ */
22
+ export interface CommentRequest {
23
+ target: TargetInfo;
24
+ /** Viewport coordinates of the click, so a composer can sit near it. */
25
+ point: {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ }
30
+ /**
31
+ * Ask the human for their words (A-240).
32
+ *
33
+ * Resolves with the comment, or with `null` for cancel — and cancel means
34
+ * NOTHING IS SENT, which is why it is a distinct value rather than an empty
35
+ * string. An empty string is a reviewer who had nothing to add; `null` is a
36
+ * reviewer who changed their mind, and filing an issue for the second is the
37
+ * kind of thing that gets an annotation tool switched off.
38
+ *
39
+ * Absent, the payload keeps `comment: ""` and the frame path is unchanged: the
40
+ * parent window is still the thing that collects the words there.
41
+ */
42
+ export type CommentCollector = (request: CommentRequest) => Promise<string | null>;
14
43
  export interface AnnotationSdkOptions {
15
44
  /**
16
- * The management app's origin. Required and concrete — see
45
+ * The management app's origin, when there is one. Concrete — see
17
46
  * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we
18
47
  * never post to anywhere else.
48
+ *
49
+ * OPTIONAL SINCE A-240, and only because first-party there is no parent to
50
+ * listen to. It used to be required for a reason that survives: the inbound
51
+ * `message` listener needs a concrete origin to compare against, and a
52
+ * listener with nothing to compare against is one that accepts instructions
53
+ * from any page that can reach this one. So the resolution is not a default
54
+ * origin — it is NO LISTENER AT ALL. Omitting this makes the SDK deaf to
55
+ * `postMessage`, which is exactly right for a page with no parent, and it is
56
+ * the only construction in which that is true.
57
+ *
58
+ * Supply it and the frame path is bit-for-bit what it was.
19
59
  */
20
- parentOrigin: string;
60
+ parentOrigin?: string;
21
61
  build: BuildInfo;
22
62
  /**
23
63
  * The matched route pattern, e.g. `/pub/[slug]`.
@@ -40,6 +80,55 @@ export interface AnnotationSdkOptions {
40
80
  * else, so most clicks produce no screenshot at all, by design.
41
81
  */
42
82
  screenshot?: ScreenshotOptions | false;
83
+ /**
84
+ * How a capture leaves the page (A-239).
85
+ *
86
+ * Defaults to `postMessageTransport(parentOrigin)`, which is what this SDK
87
+ * has always done. Pass `httpTransport(...)` when the product is being
88
+ * annotated first-party, where `win.parent === win` and a posted message is
89
+ * received by nobody — see `transport.ts` and
90
+ * `docs/plans/first-party-annotation.md`.
91
+ */
92
+ transport?: AnnotationTransport;
93
+ /**
94
+ * Called when a message could not be delivered.
95
+ *
96
+ * The frame path effectively never fails, so this is new surface for the
97
+ * HTTP one, and it exists because the alternative is a dropped capture that
98
+ * looks like a successful one. With no handler the failure is logged rather
99
+ * than swallowed; A-240 is what puts it in front of the reviewer.
100
+ */
101
+ onEgressError?: EgressErrorHandler;
102
+ /**
103
+ * Collect the reviewer's words after a click (A-240).
104
+ *
105
+ * Omit it and the SDK behaves exactly as it did: `comment: ""`, and the
106
+ * parent frame merges the human's words in.
107
+ */
108
+ collectComment?: CommentCollector;
109
+ /**
110
+ * What became of the capture, plus a thunk that sends the SAME payload again.
111
+ *
112
+ * The retry re-sends what was captured rather than re-capturing: the reviewer
113
+ * has moved on by then — a dialog is open, a row is selected, the page has
114
+ * scrolled — and re-reading it would file an issue about a screen nobody
115
+ * complained about.
116
+ */
117
+ onCaptureResult?: (outcome: EgressOutcome, retry: () => void) => void;
118
+ /**
119
+ * Elements annotation mode must pretend it cannot see.
120
+ *
121
+ * The toolbar renders INTO THE PAGE IT IS ANNOTATING, so with annotation on,
122
+ * a click on *Stop* is a click on the page and would be captured — filing an
123
+ * issue about our own button and swallowing the press that was meant to end
124
+ * the session. Shadow DOM makes it worse rather than better: retargeting
125
+ * reports the event's target as the shadow HOST, so the capture would not
126
+ * even name the button.
127
+ *
128
+ * Injected rather than hard-coded to an id, because a hard-coded id is a
129
+ * second definition of who owns that node.
130
+ */
131
+ ignoreElement?: (el: Element) => boolean;
43
132
  }
44
133
  export interface AnnotationSdk {
45
134
  enable(): void;
package/dist/sdk.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { type AnnotationPayload, type BuildInfo } from "./protocol.mjs";
1
+ import { type AnnotationPayload, type BuildInfo, type TargetInfo } from "./protocol.mjs";
2
+ import { type AnnotationTransport, type EgressErrorHandler, type EgressOutcome } from "./transport.mjs";
2
3
  import { type ScreenshotOptions } from "./screenshot.mjs";
3
4
  /**
4
5
  * The annotation SDK (A-36).
@@ -11,13 +12,52 @@ export interface AnnotationState {
11
12
  failedRequests: AnnotationPayload["state"]["failedRequests"];
12
13
  featureFlags: Record<string, unknown>;
13
14
  }
15
+ /**
16
+ * What the reviewer clicked, handed to whoever collects the words (A-240).
17
+ *
18
+ * The target is the RESOLVED one, taken from the payload that was already
19
+ * assembled — not the element, and not something to re-read. See `onClick` for
20
+ * why that distinction is the whole of this feature's correctness.
21
+ */
22
+ export interface CommentRequest {
23
+ target: TargetInfo;
24
+ /** Viewport coordinates of the click, so a composer can sit near it. */
25
+ point: {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ }
30
+ /**
31
+ * Ask the human for their words (A-240).
32
+ *
33
+ * Resolves with the comment, or with `null` for cancel — and cancel means
34
+ * NOTHING IS SENT, which is why it is a distinct value rather than an empty
35
+ * string. An empty string is a reviewer who had nothing to add; `null` is a
36
+ * reviewer who changed their mind, and filing an issue for the second is the
37
+ * kind of thing that gets an annotation tool switched off.
38
+ *
39
+ * Absent, the payload keeps `comment: ""` and the frame path is unchanged: the
40
+ * parent window is still the thing that collects the words there.
41
+ */
42
+ export type CommentCollector = (request: CommentRequest) => Promise<string | null>;
14
43
  export interface AnnotationSdkOptions {
15
44
  /**
16
- * The management app's origin. Required and concrete — see
45
+ * The management app's origin, when there is one. Concrete — see
17
46
  * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we
18
47
  * never post to anywhere else.
48
+ *
49
+ * OPTIONAL SINCE A-240, and only because first-party there is no parent to
50
+ * listen to. It used to be required for a reason that survives: the inbound
51
+ * `message` listener needs a concrete origin to compare against, and a
52
+ * listener with nothing to compare against is one that accepts instructions
53
+ * from any page that can reach this one. So the resolution is not a default
54
+ * origin — it is NO LISTENER AT ALL. Omitting this makes the SDK deaf to
55
+ * `postMessage`, which is exactly right for a page with no parent, and it is
56
+ * the only construction in which that is true.
57
+ *
58
+ * Supply it and the frame path is bit-for-bit what it was.
19
59
  */
20
- parentOrigin: string;
60
+ parentOrigin?: string;
21
61
  build: BuildInfo;
22
62
  /**
23
63
  * The matched route pattern, e.g. `/pub/[slug]`.
@@ -40,6 +80,55 @@ export interface AnnotationSdkOptions {
40
80
  * else, so most clicks produce no screenshot at all, by design.
41
81
  */
42
82
  screenshot?: ScreenshotOptions | false;
83
+ /**
84
+ * How a capture leaves the page (A-239).
85
+ *
86
+ * Defaults to `postMessageTransport(parentOrigin)`, which is what this SDK
87
+ * has always done. Pass `httpTransport(...)` when the product is being
88
+ * annotated first-party, where `win.parent === win` and a posted message is
89
+ * received by nobody — see `transport.ts` and
90
+ * `docs/plans/first-party-annotation.md`.
91
+ */
92
+ transport?: AnnotationTransport;
93
+ /**
94
+ * Called when a message could not be delivered.
95
+ *
96
+ * The frame path effectively never fails, so this is new surface for the
97
+ * HTTP one, and it exists because the alternative is a dropped capture that
98
+ * looks like a successful one. With no handler the failure is logged rather
99
+ * than swallowed; A-240 is what puts it in front of the reviewer.
100
+ */
101
+ onEgressError?: EgressErrorHandler;
102
+ /**
103
+ * Collect the reviewer's words after a click (A-240).
104
+ *
105
+ * Omit it and the SDK behaves exactly as it did: `comment: ""`, and the
106
+ * parent frame merges the human's words in.
107
+ */
108
+ collectComment?: CommentCollector;
109
+ /**
110
+ * What became of the capture, plus a thunk that sends the SAME payload again.
111
+ *
112
+ * The retry re-sends what was captured rather than re-capturing: the reviewer
113
+ * has moved on by then — a dialog is open, a row is selected, the page has
114
+ * scrolled — and re-reading it would file an issue about a screen nobody
115
+ * complained about.
116
+ */
117
+ onCaptureResult?: (outcome: EgressOutcome, retry: () => void) => void;
118
+ /**
119
+ * Elements annotation mode must pretend it cannot see.
120
+ *
121
+ * The toolbar renders INTO THE PAGE IT IS ANNOTATING, so with annotation on,
122
+ * a click on *Stop* is a click on the page and would be captured — filing an
123
+ * issue about our own button and swallowing the press that was meant to end
124
+ * the session. Shadow DOM makes it worse rather than better: retargeting
125
+ * reports the event's target as the shadow HOST, so the capture would not
126
+ * even name the button.
127
+ *
128
+ * Injected rather than hard-coded to an id, because a hard-coded id is a
129
+ * second definition of who owns that node.
130
+ */
131
+ ignoreElement?: (el: Element) => boolean;
43
132
  }
44
133
  export interface AnnotationSdk {
45
134
  enable(): void;
package/dist/sdk.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type AnnotationPayload, type BuildInfo } from "./protocol.js";
1
+ import { type AnnotationPayload, type BuildInfo, type TargetInfo } from "./protocol.js";
2
+ import { type AnnotationTransport, type EgressErrorHandler, type EgressOutcome } from "./transport.js";
2
3
  import { type ScreenshotOptions } from "./screenshot.js";
3
4
  /**
4
5
  * The annotation SDK (A-36).
@@ -11,13 +12,52 @@ export interface AnnotationState {
11
12
  failedRequests: AnnotationPayload["state"]["failedRequests"];
12
13
  featureFlags: Record<string, unknown>;
13
14
  }
15
+ /**
16
+ * What the reviewer clicked, handed to whoever collects the words (A-240).
17
+ *
18
+ * The target is the RESOLVED one, taken from the payload that was already
19
+ * assembled — not the element, and not something to re-read. See `onClick` for
20
+ * why that distinction is the whole of this feature's correctness.
21
+ */
22
+ export interface CommentRequest {
23
+ target: TargetInfo;
24
+ /** Viewport coordinates of the click, so a composer can sit near it. */
25
+ point: {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ }
30
+ /**
31
+ * Ask the human for their words (A-240).
32
+ *
33
+ * Resolves with the comment, or with `null` for cancel — and cancel means
34
+ * NOTHING IS SENT, which is why it is a distinct value rather than an empty
35
+ * string. An empty string is a reviewer who had nothing to add; `null` is a
36
+ * reviewer who changed their mind, and filing an issue for the second is the
37
+ * kind of thing that gets an annotation tool switched off.
38
+ *
39
+ * Absent, the payload keeps `comment: ""` and the frame path is unchanged: the
40
+ * parent window is still the thing that collects the words there.
41
+ */
42
+ export type CommentCollector = (request: CommentRequest) => Promise<string | null>;
14
43
  export interface AnnotationSdkOptions {
15
44
  /**
16
- * The management app's origin. Required and concrete — see
45
+ * The management app's origin, when there is one. Concrete — see
17
46
  * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we
18
47
  * never post to anywhere else.
48
+ *
49
+ * OPTIONAL SINCE A-240, and only because first-party there is no parent to
50
+ * listen to. It used to be required for a reason that survives: the inbound
51
+ * `message` listener needs a concrete origin to compare against, and a
52
+ * listener with nothing to compare against is one that accepts instructions
53
+ * from any page that can reach this one. So the resolution is not a default
54
+ * origin — it is NO LISTENER AT ALL. Omitting this makes the SDK deaf to
55
+ * `postMessage`, which is exactly right for a page with no parent, and it is
56
+ * the only construction in which that is true.
57
+ *
58
+ * Supply it and the frame path is bit-for-bit what it was.
19
59
  */
20
- parentOrigin: string;
60
+ parentOrigin?: string;
21
61
  build: BuildInfo;
22
62
  /**
23
63
  * The matched route pattern, e.g. `/pub/[slug]`.
@@ -40,6 +80,55 @@ export interface AnnotationSdkOptions {
40
80
  * else, so most clicks produce no screenshot at all, by design.
41
81
  */
42
82
  screenshot?: ScreenshotOptions | false;
83
+ /**
84
+ * How a capture leaves the page (A-239).
85
+ *
86
+ * Defaults to `postMessageTransport(parentOrigin)`, which is what this SDK
87
+ * has always done. Pass `httpTransport(...)` when the product is being
88
+ * annotated first-party, where `win.parent === win` and a posted message is
89
+ * received by nobody — see `transport.ts` and
90
+ * `docs/plans/first-party-annotation.md`.
91
+ */
92
+ transport?: AnnotationTransport;
93
+ /**
94
+ * Called when a message could not be delivered.
95
+ *
96
+ * The frame path effectively never fails, so this is new surface for the
97
+ * HTTP one, and it exists because the alternative is a dropped capture that
98
+ * looks like a successful one. With no handler the failure is logged rather
99
+ * than swallowed; A-240 is what puts it in front of the reviewer.
100
+ */
101
+ onEgressError?: EgressErrorHandler;
102
+ /**
103
+ * Collect the reviewer's words after a click (A-240).
104
+ *
105
+ * Omit it and the SDK behaves exactly as it did: `comment: ""`, and the
106
+ * parent frame merges the human's words in.
107
+ */
108
+ collectComment?: CommentCollector;
109
+ /**
110
+ * What became of the capture, plus a thunk that sends the SAME payload again.
111
+ *
112
+ * The retry re-sends what was captured rather than re-capturing: the reviewer
113
+ * has moved on by then — a dialog is open, a row is selected, the page has
114
+ * scrolled — and re-reading it would file an issue about a screen nobody
115
+ * complained about.
116
+ */
117
+ onCaptureResult?: (outcome: EgressOutcome, retry: () => void) => void;
118
+ /**
119
+ * Elements annotation mode must pretend it cannot see.
120
+ *
121
+ * The toolbar renders INTO THE PAGE IT IS ANNOTATING, so with annotation on,
122
+ * a click on *Stop* is a click on the page and would be captured — filing an
123
+ * issue about our own button and swallowing the press that was meant to end
124
+ * the session. Shadow DOM makes it worse rather than better: retargeting
125
+ * reports the event's target as the shadow HOST, so the capture would not
126
+ * even name the button.
127
+ *
128
+ * Injected rather than hard-coded to an id, because a hard-coded id is a
129
+ * second definition of who owns that node.
130
+ */
131
+ ignoreElement?: (el: Element) => boolean;
43
132
  }
44
133
  export interface AnnotationSdk {
45
134
  enable(): void;
@@ -0,0 +1,185 @@
1
+ import type { AnnotationSdk, AnnotationSdkOptions, CommentCollector } from "./sdk.cjs";
2
+ import { type Arming } from "./arming.cjs";
3
+ import type { EgressOutcome } from "./transport.cjs";
4
+ /**
5
+ * The toolbar and the post-it (A-240).
6
+ *
7
+ * ===========================================================================
8
+ * WHY THIS EXISTS AT ALL
9
+ * ===========================================================================
10
+ *
11
+ * `sdk.ts` built its payload with `comment: ""` and a comment saying why: *the
12
+ * parent frame collects the human's words and merges them in verbatim*. First
13
+ * party there is no parent — see `docs/plans/first-party-annotation.md` — so a
14
+ * capture would file an issue carrying a screenshot, a `file:line` and NOTHING
15
+ * SAYING WHAT IS WRONG WITH IT. This is the surface that collects the words,
16
+ * and the surface that turns annotation mode on in the first place.
17
+ *
18
+ * The toggle is the point rather than a convenience. The reviewer navigates the
19
+ * real app with annotation OFF — clicking rows, opening dialogs, finishing a
20
+ * wizard — and switches it ON when they arrive somewhere worth commenting on.
21
+ * That is what stops click-only screens being a wall, and it is why this is a
22
+ * toolbar and not a mode the page boots into.
23
+ *
24
+ * ===========================================================================
25
+ * IT RENDERS INSIDE SOMEBODY ELSE'S PRODUCT, AND THAT DECIDES ALMOST EVERYTHING
26
+ * ===========================================================================
27
+ *
28
+ * 1. **Shadow DOM, closed to the product's CSS in both directions.** The
29
+ * product cannot restyle this and this cannot restyle the product. A
30
+ * toolbar inheriting a host's `* { box-sizing: border-box }` — or its
31
+ * `button { text-transform: uppercase }` — is a bug report about our own UI,
32
+ * filed against their product, by a tool whose entire output is bug reports.
33
+ * `:host { all: initial }` is the half people forget: a shadow root stops
34
+ * SELECTORS crossing, not INHERITANCE. `e2e/toolbar.spec.ts` proves both
35
+ * halves against a deliberately hostile fixture, and proves them the only
36
+ * way that means anything — by deleting each and watching it go red.
37
+ *
38
+ * THE ONE THING IT CANNOT DEFEND, said out loud rather than left to be
39
+ * discovered: the HOST is in the light DOM, so the product's own selectors
40
+ * reach it. `#ai-annotation-toolbar { position: static }` in a customer's
41
+ * stylesheet beats `:host`, and puts this in the flow. Nothing shadow DOM
42
+ * offers prevents that; the id is documented so that a collision is at
43
+ * least legible when somebody hits it.
44
+ *
45
+ * 2. **It must not alter what is being reviewed.** Everything is `position:
46
+ * fixed` and nothing is ever appended to the flow, so there is no layout
47
+ * shift and no scrollbar; and NOTHING IS FOCUSED ON MOUNT. The reviewer is
48
+ * here to judge pixels, and a tool that moves one of them has answered the
49
+ * question wrongly before anybody clicks anything.
50
+ *
51
+ * `position: fixed` is doing ALL of that work, and the viewport clamp in
52
+ * `position()` is doing NONE of it — see the note there, which used to
53
+ * claim otherwise.
54
+ *
55
+ * 3. **No product may be named here (A-98).** Every string in `DEFAULT_LABELS`
56
+ * is generic, and every one of them is overridable by the mounting app. The
57
+ * only text this renders that it did not author is the target's own
58
+ * `file:line`, which is the customer's source path and belongs to them.
59
+ *
60
+ * ===========================================================================
61
+ * WHY THE CSS IS A TEMPLATE STRING AND WHAT THAT COSTS
62
+ * ===========================================================================
63
+ *
64
+ * A `<style>` element inside the shadow root, whose `textContent` is the string
65
+ * below. Not inline styles: `overlay.ts` uses those and carries nine entries in
66
+ * `audit-allowance.json` for it, and that ledger may only ever get shorter.
67
+ *
68
+ * TWO AUDITS READ THIS REPOSITORY AND THEY DISAGREE ABOUT THIS FILE, which is
69
+ * worth knowing before trusting either.
70
+ *
71
+ * `packages/design-audit` — `pnpm verify`'s ratchet — applies its colour,
72
+ * space, radius and type rules to `.css` FILES ONLY: `scan.ts` classifies
73
+ * every `.ts` as `tsx`, where the only rules are `inline-style` and the
74
+ * class-name pair. It sees nothing here.
75
+ *
76
+ * **The `design-system` CI job does.** It reads CSS out of `.ts` source, and
77
+ * the ledger it judges against — at the repository root, alongside
78
+ * `audit-allowance.json` and under the same only-ever-shorter rule — already
79
+ * carries `overlay.ts` for exactly that. THIS WAS MEASURED RATHER THAN
80
+ * REASONED: the first version of this file was written believing nothing
81
+ * audited it, said so in this comment, and that job failed the pull request
82
+ * with `untokenised-colour` and `untokenised-space` against it. The check won
83
+ * and the assumption is corrected here, where the assumption was.
84
+ *
85
+ * Hence the token block at the top of `CSS`. Every colour, space and radius
86
+ * below is a `var()` into it, so there is one place to change them and the
87
+ * gate can see that there is. They are `--ai-*` and NOT the design system's
88
+ * `--color-*` / `--space-*`, and that is forced rather than chosen: this
89
+ * package has zero dependencies on purpose, so the dictionary is not
90
+ * importable — and inside a shadow root a custom property resolves against the
91
+ * PRODUCT'S, so `var(--color-fg)` would paint this toolbar with whatever the
92
+ * customer happens to mean by that name. A local, prefixed block cannot
93
+ * collide with theirs.
94
+ */
95
+ /**
96
+ * Every word this renders. Generic by default, and all of them injectable.
97
+ *
98
+ * Every field is RENDERED SOMEWHERE. A label nobody displays is an API promise
99
+ * that cannot be kept and bytes on every page of every managed product; three
100
+ * were written and deleted before this shipped for exactly that reason.
101
+ */
102
+ export interface AnnotationToolbarLabels {
103
+ /** Names the whole surface for a screen reader. */
104
+ region: string;
105
+ expand: string;
106
+ collapse: string;
107
+ start: string;
108
+ stop: string;
109
+ done: string;
110
+ comment: string;
111
+ placeholder: string;
112
+ send: string;
113
+ cancel: string;
114
+ sent: string;
115
+ issue: string;
116
+ failed: string;
117
+ retry: string;
118
+ }
119
+ export declare const DEFAULT_LABELS: AnnotationToolbarLabels;
120
+ /** The host's id. Our own node; nothing outside this package looks it up. */
121
+ export declare const TOOLBAR_ID = "ai-annotation-toolbar";
122
+ export interface AnnotationToolbarOptions {
123
+ /**
124
+ * Whether this page may offer an annotation UI at all.
125
+ *
126
+ * INJECTED, and deliberately not decided here. How arming is decided — the
127
+ * token in the URL, the cookie, the `replaceState` that strips it, the
128
+ * production kill-switch — is A-241, and a toolbar that read the URL itself
129
+ * would be a second answer to a question that must have exactly one.
130
+ *
131
+ * Unarmed, NOTHING IS APPENDED TO THE DOCUMENT. Not hidden: absent.
132
+ */
133
+ armed?: boolean;
134
+ doc?: Document;
135
+ /** Only for the viewport clamp, so a fake one is two numbers. */
136
+ win?: Pick<Window, "innerWidth" | "innerHeight">;
137
+ labels?: Partial<AnnotationToolbarLabels>;
138
+ /** *Done* — the reviewer is finished. A-241 owns what that clears. */
139
+ onDone?: () => void;
140
+ }
141
+ export interface AnnotationToolbar {
142
+ /** Is this node ours? Hand it to `AnnotationSdkOptions.ignoreElement`. */
143
+ owns: (node: unknown) => boolean;
144
+ /** Wire the toggle to a live SDK. */
145
+ attach: (sdk: AnnotationSdk) => void;
146
+ /** Hand to `AnnotationSdkOptions.collectComment`. */
147
+ collectComment: CommentCollector;
148
+ /** Hand to `AnnotationSdkOptions.onCaptureResult`. */
149
+ showResult: (outcome: EgressOutcome, retry: () => void) => void;
150
+ /** Whether the panel is open. For tests and for the mounting app. */
151
+ isExpanded: () => boolean;
152
+ destroy: () => void;
153
+ }
154
+ export declare function createAnnotationToolbar(options?: AnnotationToolbarOptions): AnnotationToolbar;
155
+ export interface MountAnnotationOptions extends AnnotationSdkOptions {
156
+ toolbar?: AnnotationToolbarOptions;
157
+ /**
158
+ * The arming decision, from `readArming()` (A-241).
159
+ *
160
+ * Passing it is how the two gates get applied without every consumer
161
+ * re-deriving them: `armed` decides whether a toolbar exists at all, and
162
+ * *Done* calls `disarm` so the cookie and the stored capture token go with it.
163
+ *
164
+ * OPTIONAL because the frame path still mounts a toolbar-less SDK and has no
165
+ * deep link to read. Supplying both this and `toolbar.armed` throws — two
166
+ * answers to a question that must have exactly one.
167
+ */
168
+ arming?: Arming;
169
+ }
170
+ export interface MountedAnnotation {
171
+ sdk: AnnotationSdk;
172
+ toolbar: AnnotationToolbar;
173
+ destroy: () => void;
174
+ }
175
+ /**
176
+ * Build the SDK and the toolbar and connect them (A-240).
177
+ *
178
+ * Three wires, and each one is a silent failure if it is missing:
179
+ * `collectComment` absent files an issue with an empty comment;
180
+ * `onCaptureResult` absent leaves a reviewer who cannot tell a sent capture
181
+ * from a lost one; `ignoreElement` absent captures our own toolbar. None of
182
+ * them throws when omitted, which is exactly why they belong in one function
183
+ * rather than in three lines of every consumer's bootstrap.
184
+ */
185
+ export declare function mountAnnotation(options: MountAnnotationOptions): MountedAnnotation;