@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/ADOPTING.md +36 -0
- package/COMPATIBILITY.md +97 -0
- package/README.md +9 -3
- package/dist/arming.d.cts +142 -0
- package/dist/arming.d.mts +142 -0
- package/dist/arming.d.ts +142 -0
- package/dist/host.cjs +1 -1
- package/dist/host.cjs.map +2 -2
- package/dist/host.mjs +1 -1
- package/dist/host.mjs.map +2 -2
- package/dist/index.cjs +64 -1
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.cts +5 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.mjs +64 -1
- package/dist/index.mjs.map +4 -4
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +3 -3
- package/dist/protocol.d.cts +114 -0
- package/dist/protocol.d.mts +114 -0
- package/dist/protocol.d.ts +114 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.mjs.map +3 -3
- package/dist/sdk.d.cts +92 -3
- package/dist/sdk.d.mts +92 -3
- package/dist/sdk.d.ts +92 -3
- package/dist/toolbar.d.cts +185 -0
- package/dist/toolbar.d.mts +185 -0
- package/dist/toolbar.d.ts +185 -0
- package/dist/transport.d.cts +218 -0
- package/dist/transport.d.mts +218 -0
- package/dist/transport.d.ts +218 -0
- package/fixtures/README.md +35 -0
- package/fixtures/v1-minimal.json +17 -0
- package/fixtures/v1-no-source-ref.json +22 -0
- package/fixtures/v1-region-selection.json +23 -0
- package/fixtures/v1-screenshot.json +19 -0
- package/fixtures/v1-source-chain.json +24 -0
- package/package.json +11 -4
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|