@weatherboard/annotation 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,182 @@
1
+ /**
2
+ * Screenshot capture for the annotation payload (A-31).
3
+ *
4
+ * WHAT THIS ACTUALLY CAPTURES — READ THIS BEFORE BELIEVING THE FIELD NAME.
5
+ * This is not a page rasteriser. It captures the bitmap of a single
6
+ * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and
7
+ * nothing else. Click a heading and the payload has no screenshot.
8
+ *
9
+ * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or
10
+ * "`element.toDataURL` via an offscreen canvas where feasible". html2canvas is a
11
+ * runtime dependency, and this package deliberately has none — it ships
12
+ * TypeScript source that two repositories compile into their own bundles, so a
13
+ * dependency here is a dependency, a version and a licence in both of them, for
14
+ * a field `spec/02` itself calls a nice-to-have ("do not block Phase 1 on
15
+ * screenshots; the source ref is what actually matters"). The platform gives us
16
+ * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load
17
+ * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we
18
+ * implement the honest subset rather than a convincing-looking approximation,
19
+ * and the spec's server-side fallback — render `page.url` at `build.gitSha`
20
+ * headless and crop to `target.boundingBox` — remains the route to a real
21
+ * picture of the page.
22
+ *
23
+ * The subset is worth having on its own: "this image is the wrong one" and
24
+ * "this video poster is stretched" are common reports, and for those the
25
+ * element's own bitmap is the entire evidence.
26
+ *
27
+ * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than
28
+ * rejects. A tainted canvas (any cross-origin image without CORS headers, which
29
+ * is most of them) makes `toDataURL` throw `SecurityError`; a detached element
30
+ * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end
31
+ * with `screenshot` absent from an otherwise complete payload, never with a
32
+ * capture the reviewer loses.
33
+ */
34
+ /** The canvas surface this module needs. Narrow so tests need no browser. */
35
+ export interface CanvasLike {
36
+ width: number;
37
+ height: number;
38
+ getContext(id: "2d"): CanvasContextLike | null;
39
+ toDataURL(type?: string, quality?: number): string;
40
+ }
41
+ export interface CanvasContextLike {
42
+ drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;
43
+ }
44
+ /**
45
+ * The subset of an element this module reads.
46
+ *
47
+ * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK
48
+ * runs inside an iframe, and an element from another document fails an
49
+ * `instanceof` check against this realm's constructors even when it is exactly
50
+ * the thing we want.
51
+ */
52
+ export interface CaptureCandidate {
53
+ tagName: string;
54
+ parentElement?: CaptureCandidate | null;
55
+ querySelector?: (selector: string) => CaptureCandidate | null;
56
+ /** `<img>` */
57
+ naturalWidth?: number;
58
+ naturalHeight?: number;
59
+ complete?: boolean;
60
+ /** `<video>` */
61
+ videoWidth?: number;
62
+ videoHeight?: number;
63
+ /** `<canvas>` */
64
+ width?: number;
65
+ height?: number;
66
+ }
67
+ export type Screenshot = {
68
+ dataUrl: string;
69
+ region: "element" | "viewport";
70
+ };
71
+ /**
72
+ * The byte budget for the encoded data URL.
73
+ *
74
+ * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over
75
+ * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB
76
+ * leaves the rest of the payload — twenty console entries with stacks, twenty
77
+ * failed requests, a source chain — room to be large without the server
78
+ * refusing a report the reviewer has already written. A data URL is base64
79
+ * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near
80
+ * enough to compare directly.
81
+ *
82
+ * Over budget we drop the screenshot. Emitting it anyway would trade a payload
83
+ * with no picture for a payload the server 413s, which loses the source ref —
84
+ * the only part that actually matters.
85
+ */
86
+ export declare const MAX_SCREENSHOT_BYTES: number;
87
+ /**
88
+ * Longest edge, in device-independent pixels, of the downscaled capture.
89
+ *
90
+ * A 2000px-wide hero image encodes to well over the budget at any quality worth
91
+ * looking at. 480px is enough to see that the wrong photo is on the card, which
92
+ * is the question this field answers.
93
+ */
94
+ export declare const MAX_SCREENSHOT_EDGE = 480;
95
+ /**
96
+ * A ceiling on how long we will wait, not on how long the work takes.
97
+ *
98
+ * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so
99
+ * a pathological encode does not hold the capture open indefinitely, and the
100
+ * payload goes out without the screenshot instead. Said plainly because a
101
+ * timeout that cannot cancel its work is the kind of thing a reader assumes is
102
+ * stronger than it is.
103
+ */
104
+ export declare const SCREENSHOT_TIMEOUT_MS = 250;
105
+ export interface ScreenshotOptions {
106
+ /**
107
+ * Off switch. `spec/02` treats screenshots as optional, so a host that would
108
+ * rather not encode bitmaps on click — or that serves images this SDK would
109
+ * only ever taint — sets this to `false`.
110
+ */
111
+ enabled?: boolean;
112
+ maxBytes?: number;
113
+ maxEdge?: number;
114
+ timeoutMs?: number;
115
+ /**
116
+ * Canvas factory. Injected so the tests can drive the failure paths — taint,
117
+ * an oversized encode, a context that is not there — none of which are
118
+ * reachable from a fake DOM object alone.
119
+ */
120
+ createCanvas?: () => CanvasLike | null;
121
+ }
122
+ interface ResolvedOptions {
123
+ enabled: boolean;
124
+ maxBytes: number;
125
+ maxEdge: number;
126
+ timeoutMs: number;
127
+ createCanvas: () => CanvasLike | null;
128
+ }
129
+ export declare function resolveScreenshotOptions(options: ScreenshotOptions | false | undefined, doc?: Document): ResolvedOptions;
130
+ export declare function isMediaElement(el: CaptureCandidate | null | undefined): boolean;
131
+ /**
132
+ * The nearest thing with a bitmap: the element itself, then a descendant, then
133
+ * an ancestor.
134
+ *
135
+ * Descendant before ancestor because a click lands on whatever is topmost, and
136
+ * on a card that is usually the overlay or the link wrapping the photo — the
137
+ * photo the reviewer means is inside it, not above it.
138
+ */
139
+ export declare function findCaptureSource(el: CaptureCandidate | null, maxHops?: number): CaptureCandidate | null;
140
+ /**
141
+ * The source's own pixel dimensions, or null if it has none.
142
+ *
143
+ * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:
144
+ * there is no bitmap to capture. The report still carries the source ref and
145
+ * the DOM path, which is what a "this image is broken" ticket needs anyway.
146
+ */
147
+ export declare function intrinsicSize(el: CaptureCandidate): {
148
+ w: number;
149
+ h: number;
150
+ } | null;
151
+ /** Fit inside `maxEdge` without ever upscaling — enlarging invents detail. */
152
+ export declare function fitWithin(size: {
153
+ w: number;
154
+ h: number;
155
+ }, maxEdge: number): {
156
+ w: number;
157
+ h: number;
158
+ };
159
+ /**
160
+ * Capture synchronously, or return null. Exported for the tests, which need to
161
+ * assert the drop decisions without a timer in the way.
162
+ */
163
+ export declare function captureScreenshotSync(el: CaptureCandidate | null, options: ResolvedOptions): Screenshot | null;
164
+ /**
165
+ * The entry point the SDK calls. Resolves to a screenshot or to null, within
166
+ * `timeoutMs`, and never rejects.
167
+ */
168
+ export declare function captureScreenshot(el: CaptureCandidate | null, options: ResolvedOptions): Promise<Screenshot | null>;
169
+ /**
170
+ * Merge a capture into a payload, or hand the payload back untouched.
171
+ *
172
+ * The payload goes in already complete and comes out still complete. The
173
+ * failure mode this shape exists to make impossible is a screenshot problem
174
+ * costing the reviewer the source ref — so the payload is never awaited into
175
+ * existence, only decorated, and `screenshot` is added as a key or not at all
176
+ * rather than set to `undefined` (which `JSON.stringify` drops anyway, but
177
+ * which reads as "we tried and got nothing" to anyone inspecting the object).
178
+ */
179
+ export declare function attachScreenshot<T extends object>(payload: T, el: CaptureCandidate | null, options: ResolvedOptions): Promise<T & {
180
+ screenshot?: Screenshot;
181
+ }>;
182
+ export {};
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Screenshot capture for the annotation payload (A-31).
3
+ *
4
+ * WHAT THIS ACTUALLY CAPTURES — READ THIS BEFORE BELIEVING THE FIELD NAME.
5
+ * This is not a page rasteriser. It captures the bitmap of a single
6
+ * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and
7
+ * nothing else. Click a heading and the payload has no screenshot.
8
+ *
9
+ * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or
10
+ * "`element.toDataURL` via an offscreen canvas where feasible". html2canvas is a
11
+ * runtime dependency, and this package deliberately has none — it ships
12
+ * TypeScript source that two repositories compile into their own bundles, so a
13
+ * dependency here is a dependency, a version and a licence in both of them, for
14
+ * a field `spec/02` itself calls a nice-to-have ("do not block Phase 1 on
15
+ * screenshots; the source ref is what actually matters"). The platform gives us
16
+ * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load
17
+ * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we
18
+ * implement the honest subset rather than a convincing-looking approximation,
19
+ * and the spec's server-side fallback — render `page.url` at `build.gitSha`
20
+ * headless and crop to `target.boundingBox` — remains the route to a real
21
+ * picture of the page.
22
+ *
23
+ * The subset is worth having on its own: "this image is the wrong one" and
24
+ * "this video poster is stretched" are common reports, and for those the
25
+ * element's own bitmap is the entire evidence.
26
+ *
27
+ * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than
28
+ * rejects. A tainted canvas (any cross-origin image without CORS headers, which
29
+ * is most of them) makes `toDataURL` throw `SecurityError`; a detached element
30
+ * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end
31
+ * with `screenshot` absent from an otherwise complete payload, never with a
32
+ * capture the reviewer loses.
33
+ */
34
+ /** The canvas surface this module needs. Narrow so tests need no browser. */
35
+ export interface CanvasLike {
36
+ width: number;
37
+ height: number;
38
+ getContext(id: "2d"): CanvasContextLike | null;
39
+ toDataURL(type?: string, quality?: number): string;
40
+ }
41
+ export interface CanvasContextLike {
42
+ drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;
43
+ }
44
+ /**
45
+ * The subset of an element this module reads.
46
+ *
47
+ * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK
48
+ * runs inside an iframe, and an element from another document fails an
49
+ * `instanceof` check against this realm's constructors even when it is exactly
50
+ * the thing we want.
51
+ */
52
+ export interface CaptureCandidate {
53
+ tagName: string;
54
+ parentElement?: CaptureCandidate | null;
55
+ querySelector?: (selector: string) => CaptureCandidate | null;
56
+ /** `<img>` */
57
+ naturalWidth?: number;
58
+ naturalHeight?: number;
59
+ complete?: boolean;
60
+ /** `<video>` */
61
+ videoWidth?: number;
62
+ videoHeight?: number;
63
+ /** `<canvas>` */
64
+ width?: number;
65
+ height?: number;
66
+ }
67
+ export type Screenshot = {
68
+ dataUrl: string;
69
+ region: "element" | "viewport";
70
+ };
71
+ /**
72
+ * The byte budget for the encoded data URL.
73
+ *
74
+ * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over
75
+ * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB
76
+ * leaves the rest of the payload — twenty console entries with stacks, twenty
77
+ * failed requests, a source chain — room to be large without the server
78
+ * refusing a report the reviewer has already written. A data URL is base64
79
+ * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near
80
+ * enough to compare directly.
81
+ *
82
+ * Over budget we drop the screenshot. Emitting it anyway would trade a payload
83
+ * with no picture for a payload the server 413s, which loses the source ref —
84
+ * the only part that actually matters.
85
+ */
86
+ export declare const MAX_SCREENSHOT_BYTES: number;
87
+ /**
88
+ * Longest edge, in device-independent pixels, of the downscaled capture.
89
+ *
90
+ * A 2000px-wide hero image encodes to well over the budget at any quality worth
91
+ * looking at. 480px is enough to see that the wrong photo is on the card, which
92
+ * is the question this field answers.
93
+ */
94
+ export declare const MAX_SCREENSHOT_EDGE = 480;
95
+ /**
96
+ * A ceiling on how long we will wait, not on how long the work takes.
97
+ *
98
+ * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so
99
+ * a pathological encode does not hold the capture open indefinitely, and the
100
+ * payload goes out without the screenshot instead. Said plainly because a
101
+ * timeout that cannot cancel its work is the kind of thing a reader assumes is
102
+ * stronger than it is.
103
+ */
104
+ export declare const SCREENSHOT_TIMEOUT_MS = 250;
105
+ export interface ScreenshotOptions {
106
+ /**
107
+ * Off switch. `spec/02` treats screenshots as optional, so a host that would
108
+ * rather not encode bitmaps on click — or that serves images this SDK would
109
+ * only ever taint — sets this to `false`.
110
+ */
111
+ enabled?: boolean;
112
+ maxBytes?: number;
113
+ maxEdge?: number;
114
+ timeoutMs?: number;
115
+ /**
116
+ * Canvas factory. Injected so the tests can drive the failure paths — taint,
117
+ * an oversized encode, a context that is not there — none of which are
118
+ * reachable from a fake DOM object alone.
119
+ */
120
+ createCanvas?: () => CanvasLike | null;
121
+ }
122
+ interface ResolvedOptions {
123
+ enabled: boolean;
124
+ maxBytes: number;
125
+ maxEdge: number;
126
+ timeoutMs: number;
127
+ createCanvas: () => CanvasLike | null;
128
+ }
129
+ export declare function resolveScreenshotOptions(options: ScreenshotOptions | false | undefined, doc?: Document): ResolvedOptions;
130
+ export declare function isMediaElement(el: CaptureCandidate | null | undefined): boolean;
131
+ /**
132
+ * The nearest thing with a bitmap: the element itself, then a descendant, then
133
+ * an ancestor.
134
+ *
135
+ * Descendant before ancestor because a click lands on whatever is topmost, and
136
+ * on a card that is usually the overlay or the link wrapping the photo — the
137
+ * photo the reviewer means is inside it, not above it.
138
+ */
139
+ export declare function findCaptureSource(el: CaptureCandidate | null, maxHops?: number): CaptureCandidate | null;
140
+ /**
141
+ * The source's own pixel dimensions, or null if it has none.
142
+ *
143
+ * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:
144
+ * there is no bitmap to capture. The report still carries the source ref and
145
+ * the DOM path, which is what a "this image is broken" ticket needs anyway.
146
+ */
147
+ export declare function intrinsicSize(el: CaptureCandidate): {
148
+ w: number;
149
+ h: number;
150
+ } | null;
151
+ /** Fit inside `maxEdge` without ever upscaling — enlarging invents detail. */
152
+ export declare function fitWithin(size: {
153
+ w: number;
154
+ h: number;
155
+ }, maxEdge: number): {
156
+ w: number;
157
+ h: number;
158
+ };
159
+ /**
160
+ * Capture synchronously, or return null. Exported for the tests, which need to
161
+ * assert the drop decisions without a timer in the way.
162
+ */
163
+ export declare function captureScreenshotSync(el: CaptureCandidate | null, options: ResolvedOptions): Screenshot | null;
164
+ /**
165
+ * The entry point the SDK calls. Resolves to a screenshot or to null, within
166
+ * `timeoutMs`, and never rejects.
167
+ */
168
+ export declare function captureScreenshot(el: CaptureCandidate | null, options: ResolvedOptions): Promise<Screenshot | null>;
169
+ /**
170
+ * Merge a capture into a payload, or hand the payload back untouched.
171
+ *
172
+ * The payload goes in already complete and comes out still complete. The
173
+ * failure mode this shape exists to make impossible is a screenshot problem
174
+ * costing the reviewer the source ref — so the payload is never awaited into
175
+ * existence, only decorated, and `screenshot` is added as a key or not at all
176
+ * rather than set to `undefined` (which `JSON.stringify` drops anyway, but
177
+ * which reads as "we tried and got nothing" to anyone inspecting the object).
178
+ */
179
+ export declare function attachScreenshot<T extends object>(payload: T, el: CaptureCandidate | null, options: ResolvedOptions): Promise<T & {
180
+ screenshot?: Screenshot;
181
+ }>;
182
+ export {};
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Screenshot capture for the annotation payload (A-31).
3
+ *
4
+ * WHAT THIS ACTUALLY CAPTURES — READ THIS BEFORE BELIEVING THE FIELD NAME.
5
+ * This is not a page rasteriser. It captures the bitmap of a single
6
+ * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and
7
+ * nothing else. Click a heading and the payload has no screenshot.
8
+ *
9
+ * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or
10
+ * "`element.toDataURL` via an offscreen canvas where feasible". html2canvas is a
11
+ * runtime dependency, and this package deliberately has none — it ships
12
+ * TypeScript source that two repositories compile into their own bundles, so a
13
+ * dependency here is a dependency, a version and a licence in both of them, for
14
+ * a field `spec/02` itself calls a nice-to-have ("do not block Phase 1 on
15
+ * screenshots; the source ref is what actually matters"). The platform gives us
16
+ * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load
17
+ * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we
18
+ * implement the honest subset rather than a convincing-looking approximation,
19
+ * and the spec's server-side fallback — render `page.url` at `build.gitSha`
20
+ * headless and crop to `target.boundingBox` — remains the route to a real
21
+ * picture of the page.
22
+ *
23
+ * The subset is worth having on its own: "this image is the wrong one" and
24
+ * "this video poster is stretched" are common reports, and for those the
25
+ * element's own bitmap is the entire evidence.
26
+ *
27
+ * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than
28
+ * rejects. A tainted canvas (any cross-origin image without CORS headers, which
29
+ * is most of them) makes `toDataURL` throw `SecurityError`; a detached element
30
+ * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end
31
+ * with `screenshot` absent from an otherwise complete payload, never with a
32
+ * capture the reviewer loses.
33
+ */
34
+ /** The canvas surface this module needs. Narrow so tests need no browser. */
35
+ export interface CanvasLike {
36
+ width: number;
37
+ height: number;
38
+ getContext(id: "2d"): CanvasContextLike | null;
39
+ toDataURL(type?: string, quality?: number): string;
40
+ }
41
+ export interface CanvasContextLike {
42
+ drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;
43
+ }
44
+ /**
45
+ * The subset of an element this module reads.
46
+ *
47
+ * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK
48
+ * runs inside an iframe, and an element from another document fails an
49
+ * `instanceof` check against this realm's constructors even when it is exactly
50
+ * the thing we want.
51
+ */
52
+ export interface CaptureCandidate {
53
+ tagName: string;
54
+ parentElement?: CaptureCandidate | null;
55
+ querySelector?: (selector: string) => CaptureCandidate | null;
56
+ /** `<img>` */
57
+ naturalWidth?: number;
58
+ naturalHeight?: number;
59
+ complete?: boolean;
60
+ /** `<video>` */
61
+ videoWidth?: number;
62
+ videoHeight?: number;
63
+ /** `<canvas>` */
64
+ width?: number;
65
+ height?: number;
66
+ }
67
+ export type Screenshot = {
68
+ dataUrl: string;
69
+ region: "element" | "viewport";
70
+ };
71
+ /**
72
+ * The byte budget for the encoded data URL.
73
+ *
74
+ * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over
75
+ * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB
76
+ * leaves the rest of the payload — twenty console entries with stacks, twenty
77
+ * failed requests, a source chain — room to be large without the server
78
+ * refusing a report the reviewer has already written. A data URL is base64
79
+ * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near
80
+ * enough to compare directly.
81
+ *
82
+ * Over budget we drop the screenshot. Emitting it anyway would trade a payload
83
+ * with no picture for a payload the server 413s, which loses the source ref —
84
+ * the only part that actually matters.
85
+ */
86
+ export declare const MAX_SCREENSHOT_BYTES: number;
87
+ /**
88
+ * Longest edge, in device-independent pixels, of the downscaled capture.
89
+ *
90
+ * A 2000px-wide hero image encodes to well over the budget at any quality worth
91
+ * looking at. 480px is enough to see that the wrong photo is on the card, which
92
+ * is the question this field answers.
93
+ */
94
+ export declare const MAX_SCREENSHOT_EDGE = 480;
95
+ /**
96
+ * A ceiling on how long we will wait, not on how long the work takes.
97
+ *
98
+ * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so
99
+ * a pathological encode does not hold the capture open indefinitely, and the
100
+ * payload goes out without the screenshot instead. Said plainly because a
101
+ * timeout that cannot cancel its work is the kind of thing a reader assumes is
102
+ * stronger than it is.
103
+ */
104
+ export declare const SCREENSHOT_TIMEOUT_MS = 250;
105
+ export interface ScreenshotOptions {
106
+ /**
107
+ * Off switch. `spec/02` treats screenshots as optional, so a host that would
108
+ * rather not encode bitmaps on click — or that serves images this SDK would
109
+ * only ever taint — sets this to `false`.
110
+ */
111
+ enabled?: boolean;
112
+ maxBytes?: number;
113
+ maxEdge?: number;
114
+ timeoutMs?: number;
115
+ /**
116
+ * Canvas factory. Injected so the tests can drive the failure paths — taint,
117
+ * an oversized encode, a context that is not there — none of which are
118
+ * reachable from a fake DOM object alone.
119
+ */
120
+ createCanvas?: () => CanvasLike | null;
121
+ }
122
+ interface ResolvedOptions {
123
+ enabled: boolean;
124
+ maxBytes: number;
125
+ maxEdge: number;
126
+ timeoutMs: number;
127
+ createCanvas: () => CanvasLike | null;
128
+ }
129
+ export declare function resolveScreenshotOptions(options: ScreenshotOptions | false | undefined, doc?: Document): ResolvedOptions;
130
+ export declare function isMediaElement(el: CaptureCandidate | null | undefined): boolean;
131
+ /**
132
+ * The nearest thing with a bitmap: the element itself, then a descendant, then
133
+ * an ancestor.
134
+ *
135
+ * Descendant before ancestor because a click lands on whatever is topmost, and
136
+ * on a card that is usually the overlay or the link wrapping the photo — the
137
+ * photo the reviewer means is inside it, not above it.
138
+ */
139
+ export declare function findCaptureSource(el: CaptureCandidate | null, maxHops?: number): CaptureCandidate | null;
140
+ /**
141
+ * The source's own pixel dimensions, or null if it has none.
142
+ *
143
+ * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:
144
+ * there is no bitmap to capture. The report still carries the source ref and
145
+ * the DOM path, which is what a "this image is broken" ticket needs anyway.
146
+ */
147
+ export declare function intrinsicSize(el: CaptureCandidate): {
148
+ w: number;
149
+ h: number;
150
+ } | null;
151
+ /** Fit inside `maxEdge` without ever upscaling — enlarging invents detail. */
152
+ export declare function fitWithin(size: {
153
+ w: number;
154
+ h: number;
155
+ }, maxEdge: number): {
156
+ w: number;
157
+ h: number;
158
+ };
159
+ /**
160
+ * Capture synchronously, or return null. Exported for the tests, which need to
161
+ * assert the drop decisions without a timer in the way.
162
+ */
163
+ export declare function captureScreenshotSync(el: CaptureCandidate | null, options: ResolvedOptions): Screenshot | null;
164
+ /**
165
+ * The entry point the SDK calls. Resolves to a screenshot or to null, within
166
+ * `timeoutMs`, and never rejects.
167
+ */
168
+ export declare function captureScreenshot(el: CaptureCandidate | null, options: ResolvedOptions): Promise<Screenshot | null>;
169
+ /**
170
+ * Merge a capture into a payload, or hand the payload back untouched.
171
+ *
172
+ * The payload goes in already complete and comes out still complete. The
173
+ * failure mode this shape exists to make impossible is a screenshot problem
174
+ * costing the reviewer the source ref — so the payload is never awaited into
175
+ * existence, only decorated, and `screenshot` is added as a key or not at all
176
+ * rather than set to `undefined` (which `JSON.stringify` drops anyway, but
177
+ * which reads as "we tried and got nothing" to anyone inspecting the object).
178
+ */
179
+ export declare function attachScreenshot<T extends object>(payload: T, el: CaptureCandidate | null, options: ResolvedOptions): Promise<T & {
180
+ screenshot?: Screenshot;
181
+ }>;
182
+ export {};
package/dist/sdk.d.cts ADDED
@@ -0,0 +1,52 @@
1
+ import { type AnnotationPayload, type BuildInfo } from "./protocol.cjs";
2
+ import { type ScreenshotOptions } from "./screenshot.cjs";
3
+ /**
4
+ * The annotation SDK (A-36).
5
+ *
6
+ * Lives in the product bundle, listens for the management app's instructions,
7
+ * and turns a click into a payload pinned to a code location.
8
+ */
9
+ export interface AnnotationState {
10
+ consoleErrors: AnnotationPayload["state"]["consoleErrors"];
11
+ failedRequests: AnnotationPayload["state"]["failedRequests"];
12
+ featureFlags: Record<string, unknown>;
13
+ }
14
+ export interface AnnotationSdkOptions {
15
+ /**
16
+ * The management app's origin. Required and concrete — see
17
+ * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we
18
+ * never post to anywhere else.
19
+ */
20
+ parentOrigin: string;
21
+ build: BuildInfo;
22
+ /**
23
+ * The matched route pattern, e.g. `/pub/[slug]`.
24
+ *
25
+ * Injected rather than derived: only the app knows its own route patterns,
26
+ * and `location.pathname` is the resolved URL, which is what the spec
27
+ * explicitly does not want — `/pub/the-eagle` and `/pub/the-crown` are the
28
+ * same route and should group as one.
29
+ */
30
+ getRoute: () => string;
31
+ /**
32
+ * Page state at capture time. Injected so A-35's ring buffers can supply the
33
+ * real thing without this module knowing how they are collected.
34
+ */
35
+ getState: () => AnnotationState;
36
+ /**
37
+ * Screenshot capture (A-31). Omit for the default, or pass `false` to turn it
38
+ * off entirely. See `screenshot.ts` for what it can and cannot capture — the
39
+ * short version is that it captures a media element's own bitmap and nothing
40
+ * else, so most clicks produce no screenshot at all, by design.
41
+ */
42
+ screenshot?: ScreenshotOptions | false;
43
+ }
44
+ export interface AnnotationSdk {
45
+ enable(): void;
46
+ disable(): void;
47
+ isEnabled(): boolean;
48
+ destroy(): void;
49
+ }
50
+ /** State until A-35 lands its ring buffers. Empty is honest; invented is not. */
51
+ export declare const emptyAnnotationState: () => AnnotationState;
52
+ export declare function createAnnotationSdk(options: AnnotationSdkOptions): AnnotationSdk;
package/dist/sdk.d.mts ADDED
@@ -0,0 +1,52 @@
1
+ import { type AnnotationPayload, type BuildInfo } from "./protocol.mjs";
2
+ import { type ScreenshotOptions } from "./screenshot.mjs";
3
+ /**
4
+ * The annotation SDK (A-36).
5
+ *
6
+ * Lives in the product bundle, listens for the management app's instructions,
7
+ * and turns a click into a payload pinned to a code location.
8
+ */
9
+ export interface AnnotationState {
10
+ consoleErrors: AnnotationPayload["state"]["consoleErrors"];
11
+ failedRequests: AnnotationPayload["state"]["failedRequests"];
12
+ featureFlags: Record<string, unknown>;
13
+ }
14
+ export interface AnnotationSdkOptions {
15
+ /**
16
+ * The management app's origin. Required and concrete — see
17
+ * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we
18
+ * never post to anywhere else.
19
+ */
20
+ parentOrigin: string;
21
+ build: BuildInfo;
22
+ /**
23
+ * The matched route pattern, e.g. `/pub/[slug]`.
24
+ *
25
+ * Injected rather than derived: only the app knows its own route patterns,
26
+ * and `location.pathname` is the resolved URL, which is what the spec
27
+ * explicitly does not want — `/pub/the-eagle` and `/pub/the-crown` are the
28
+ * same route and should group as one.
29
+ */
30
+ getRoute: () => string;
31
+ /**
32
+ * Page state at capture time. Injected so A-35's ring buffers can supply the
33
+ * real thing without this module knowing how they are collected.
34
+ */
35
+ getState: () => AnnotationState;
36
+ /**
37
+ * Screenshot capture (A-31). Omit for the default, or pass `false` to turn it
38
+ * off entirely. See `screenshot.ts` for what it can and cannot capture — the
39
+ * short version is that it captures a media element's own bitmap and nothing
40
+ * else, so most clicks produce no screenshot at all, by design.
41
+ */
42
+ screenshot?: ScreenshotOptions | false;
43
+ }
44
+ export interface AnnotationSdk {
45
+ enable(): void;
46
+ disable(): void;
47
+ isEnabled(): boolean;
48
+ destroy(): void;
49
+ }
50
+ /** State until A-35 lands its ring buffers. Empty is honest; invented is not. */
51
+ export declare const emptyAnnotationState: () => AnnotationState;
52
+ export declare function createAnnotationSdk(options: AnnotationSdkOptions): AnnotationSdk;