@fixback/sdk 0.1.0 → 0.3.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/README.md +72 -0
- package/dist/annotation.d.ts +106 -0
- package/dist/auto-report-backoff.d.ts +47 -0
- package/dist/boot.d.ts +17 -3
- package/dist/breadcrumbs.d.ts +448 -0
- package/dist/dom.d.ts +7 -0
- package/dist/draw-surface.d.ts +51 -0
- package/dist/error-capture.d.ts +135 -0
- package/dist/fixback.umd.js +538 -28
- package/dist/fixback.umd.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.mjs +3645 -468
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +100 -7
- package/dist/invite.d.ts +104 -0
- package/dist/launcher.d.ts +29 -9
- package/dist/onboarding-styles.d.ts +8 -0
- package/dist/onboarding.d.ts +44 -0
- package/dist/overlay-styles.d.ts +1 -1
- package/dist/overlay.d.ts +36 -6
- package/dist/region-capture.d.ts +40 -0
- package/dist/report.d.ts +33 -7
- package/dist/screenshot.d.ts +39 -18
- package/dist/scrub.d.ts +61 -0
- package/dist/styles.d.ts +11 -1
- package/dist/submit.d.ts +22 -4
- package/dist/version.d.ts +10 -4
- package/package.json +4 -1
package/dist/init.d.ts
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
import { type IdentityInputs } from "./boot";
|
|
2
|
+
import { type BeforeBreadcrumb, type BreadcrumbLevel, type TraceStream } from "./breadcrumbs";
|
|
3
|
+
import type { BeforeSend } from "./scrub";
|
|
2
4
|
/**
|
|
3
5
|
* The hosted Fixback API origin the SDK talks to by default. A self-hosted or
|
|
4
6
|
* local deployment overrides it with `apiUrl` (the dashboard's install snippet
|
|
5
7
|
* pre-fills the right value for the Project).
|
|
6
8
|
*/
|
|
7
9
|
export declare const DEFAULT_API_URL = "https://api.fixback.dev";
|
|
10
|
+
/**
|
|
11
|
+
* Does `url` address one of the SDK's **own** API endpoints? Every request the SDK
|
|
12
|
+
* issues lives under two namespaces beneath the API origin: `/api/ingest/*` — boot
|
|
13
|
+
* (`bootEndpoint`, boot.ts) and feedback (`feedbackEndpoint`, submit.ts) — and
|
|
14
|
+
* `/api/invites/*` (`inviteStatusEndpoint` / `redeemEndpoint`, invite.ts).
|
|
15
|
+
*
|
|
16
|
+
* The trace's network capture ignores **only** these, never the whole origin. That
|
|
17
|
+
* distinction matters when the instrumented app and the Fixback API share an origin — a
|
|
18
|
+
* self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
|
|
19
|
+
* ignoring the origin wholesale would swallow all of the app's own requests and leave the
|
|
20
|
+
* Network tab empty. When the app and the API sit on different origins (the common case)
|
|
21
|
+
* this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
|
|
22
|
+
* request URL — absolute for every call the SDK makes — so a prefix test over the
|
|
23
|
+
* normalised origin is exact.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isFixbackApiRequest(apiUrl: string, url: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Trace buffer tuning (spec #122 §A). These are configurable **starting points**
|
|
28
|
+
* from the grill — three per-stream ring budgets (network ≈ 100 / console ≈ 80 /
|
|
29
|
+
* breadcrumbs ≈ 40), a shared age cap (~3 min, on), and **all** console levels
|
|
30
|
+
* captured (eviction priority, not capture, keeps warn/error above the chatter) —
|
|
31
|
+
* never frozen magic numbers. Pass `false` for {@link InitOptions.trace} to disable
|
|
32
|
+
* capture entirely.
|
|
33
|
+
*/
|
|
34
|
+
export interface TraceOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Per-stream ring budgets (network / console / breadcrumbs); each stream evicts
|
|
37
|
+
* independently, so a chatty stream can't drop another's lead-up. Any stream
|
|
38
|
+
* omitted uses its default.
|
|
39
|
+
*/
|
|
40
|
+
readonly budgets?: Partial<Record<TraceStream, number>>;
|
|
41
|
+
/** Shared age cap in ms. Defaults to ~3 min (on); pass `0` to disable age pruning. */
|
|
42
|
+
readonly maxAgeMs?: number;
|
|
43
|
+
/** Console levels captured. Defaults to **all** levels (log/info/warn/error/assert/debug). */
|
|
44
|
+
readonly consoleLevels?: readonly BreadcrumbLevel[];
|
|
45
|
+
/** Per-crumb filter: mute a category, edit a crumb, or drop it (`null`). */
|
|
46
|
+
readonly beforeBreadcrumb?: BeforeBreadcrumb;
|
|
47
|
+
}
|
|
8
48
|
/** Options for {@link init}. Only `key` is required. */
|
|
9
49
|
export interface InitOptions extends IdentityInputs {
|
|
10
50
|
/** The Project's **publishable** key — an identifier that ships in the page. */
|
|
@@ -13,6 +53,51 @@ export interface InitOptions extends IdentityInputs {
|
|
|
13
53
|
readonly apiUrl?: string;
|
|
14
54
|
/** Where to mount the launcher. Defaults to `document.body`. */
|
|
15
55
|
readonly target?: HTMLElement;
|
|
56
|
+
/**
|
|
57
|
+
* Still the launcher's pulse and motion. An explicit opt-in that complements
|
|
58
|
+
* the visitor's OS-level `prefers-reduced-motion`, which the launcher already
|
|
59
|
+
* honours on its own.
|
|
60
|
+
*/
|
|
61
|
+
readonly reduceMotion?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The synchronous, network-free client scrub hook every report passes through
|
|
64
|
+
* before transport (spec §C). Mutate the draft to scrub further, or return
|
|
65
|
+
* `null` to drop the report. Runs after the default scrubbers.
|
|
66
|
+
*/
|
|
67
|
+
readonly beforeSend?: BeforeSend;
|
|
68
|
+
/** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
|
|
69
|
+
readonly scrub?: boolean;
|
|
70
|
+
/** Trace buffer tuning, or `false` to turn the buffer off entirely. */
|
|
71
|
+
readonly trace?: TraceOptions | false;
|
|
72
|
+
/**
|
|
73
|
+
* The dev-side override of the Project's server-served capture config (spec #122
|
|
74
|
+
* §L; ticket #138). Capture is **default-on** and normally governed per-project
|
|
75
|
+
* from the dashboard, surfaced on the boot answer; set a stream here to override
|
|
76
|
+
* what the server serves for it — `{ network: false }` turns network capture off
|
|
77
|
+
* even where the Project leaves it on, and a stream left unset follows the served
|
|
78
|
+
* config. Independent of {@link trace} `false`, which turns the whole buffer off.
|
|
79
|
+
*/
|
|
80
|
+
readonly capture?: {
|
|
81
|
+
readonly console?: boolean;
|
|
82
|
+
readonly network?: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Automatic error capture — the SDK files uncaught exceptions / unhandled
|
|
86
|
+
* rejections as `source: auto` Feedback with no prompt (spec §E, ADR-0011).
|
|
87
|
+
* **Default-on across all Gates**; set `false` for the per-project toggle that
|
|
88
|
+
* turns it off. It is gated by boot's `canSubmit` either way, so an auto-error is
|
|
89
|
+
* never filed where a manual report would be refused.
|
|
90
|
+
*/
|
|
91
|
+
readonly autoCapture?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Options for {@link redeem} — the explicit form of the `?fixback_invite=` URL
|
|
95
|
+
* detection {@link init} performs. Identical to {@link InitOptions} plus the
|
|
96
|
+
* `token` to redeem (the value the invite link carried).
|
|
97
|
+
*/
|
|
98
|
+
export interface RedeemOptions extends InitOptions {
|
|
99
|
+
/** The invite token to redeem — the `?fixback_invite=` value from the link. */
|
|
100
|
+
readonly token: string;
|
|
16
101
|
}
|
|
17
102
|
/** A running SDK instance. */
|
|
18
103
|
export interface FixbackInstance {
|
|
@@ -23,12 +108,20 @@ export interface FixbackInstance {
|
|
|
23
108
|
* Boot the SDK and mount the launcher when — and only when — a submission would
|
|
24
109
|
* be accepted for this key, origin, and Gate.
|
|
25
110
|
*
|
|
26
|
-
* On load the SDK
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
111
|
+
* On load the SDK first checks the page URL for an invite token
|
|
112
|
+
* (`?fixback_invite=`, spec §F); when one is present and live it renders the
|
|
113
|
+
* onboarding modal and defers the launcher until the tester redeems. Otherwise it
|
|
114
|
+
* calls the ingest boot endpoint and mounts the launcher solely when the answer's
|
|
115
|
+
* `canSubmit` is true — so a Reporter is never shown a launcher a submission would
|
|
116
|
+
* be refused, and on an Invited / Internal Gate the launcher stays absent until
|
|
117
|
+
* redemption. Everything is wrapped so a Fixback problem — unreachable, refused,
|
|
118
|
+
* or an unexpected error — resolves to a no-op instance.
|
|
33
119
|
*/
|
|
34
120
|
export declare function init(options: InitOptions): Promise<FixbackInstance>;
|
|
121
|
+
/**
|
|
122
|
+
* Redeem an invite token explicitly, the programmatic equivalent of landing on a
|
|
123
|
+
* page with `?fixback_invite=<token>`. Renders the onboarding modal, and on
|
|
124
|
+
* confirm redeems, persists the `reporterId`, and mounts the launcher (spec §F).
|
|
125
|
+
* Use it when the token reaches the page some way other than the URL.
|
|
126
|
+
*/
|
|
127
|
+
export declare function redeem(options: RedeemOptions): Promise<FixbackInstance>;
|
package/dist/invite.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invite redemption & reporter identity (spec §F).
|
|
3
|
+
*
|
|
4
|
+
* The zero-integration path for an invited tester: detect an invite token on the
|
|
5
|
+
* page (`?fixback_invite=<token>`) or an explicit `redeem()` call, read the
|
|
6
|
+
* Invite's public status, and — on confirmation — redeem it into a persisted
|
|
7
|
+
* `reporterId` the SDK thereafter presents as an identity input.
|
|
8
|
+
*
|
|
9
|
+
* This module is the wire + storage half (pure enough to unit-test directly): the
|
|
10
|
+
* two invite endpoints, the localStorage persistence scoped by publishable key,
|
|
11
|
+
* and the URL token detection / one-time consumption. The onboarding modal is
|
|
12
|
+
* `onboarding.ts`; `init` wires the two together.
|
|
13
|
+
*
|
|
14
|
+
* Like `boot.ts`, the wire types are a **vendored** slice of the server contract
|
|
15
|
+
* (`apps/api/src/invites/invite-redemption.controller.ts`) — the SDK never imports
|
|
16
|
+
* the private server package. Keep them in lock-step with that controller.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReporterTier } from "./boot";
|
|
19
|
+
/** The Invite's shape. Mirrors the server's `InviteKind`. */
|
|
20
|
+
export type InviteKind = "targeted" | "shared";
|
|
21
|
+
/** The dead/unknown states a status or redeem read can resolve to. */
|
|
22
|
+
export type InviteDeadStatus = "revoked" | "expired" | "exhausted" | "not_found";
|
|
23
|
+
/**
|
|
24
|
+
* The public status of an Invite (`GET /api/invites/:token`). A live (`pending`)
|
|
25
|
+
* Invite reveals the minimal facts the onboarding modal needs — the Project, the
|
|
26
|
+
* Invite's shape, and the **server-derived** tier the redemption would grant; a
|
|
27
|
+
* dead or unknown token reveals only its state.
|
|
28
|
+
*/
|
|
29
|
+
export type InviteStatusAnswer = {
|
|
30
|
+
readonly status: "pending";
|
|
31
|
+
readonly projectId: string;
|
|
32
|
+
readonly kind: InviteKind;
|
|
33
|
+
readonly tier: ReporterTier;
|
|
34
|
+
} | {
|
|
35
|
+
readonly status: InviteDeadStatus;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The result of redeeming an Invite (`POST /api/invites/:token/redeem`): the
|
|
39
|
+
* minted Reporter (its handle + server-derived tier + Project), or why it was
|
|
40
|
+
* refused.
|
|
41
|
+
*/
|
|
42
|
+
export type RedeemAnswer = {
|
|
43
|
+
readonly status: "redeemed";
|
|
44
|
+
readonly reporterId: string;
|
|
45
|
+
readonly tier: ReporterTier;
|
|
46
|
+
readonly projectId: string;
|
|
47
|
+
} | {
|
|
48
|
+
readonly status: InviteDeadStatus;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The self-provided display fields captured in the onboarding modal. They ride
|
|
52
|
+
* along as a Reporter's chosen name / email — **never** a trust signal (the tier
|
|
53
|
+
* is always server-derived, spec §F).
|
|
54
|
+
*/
|
|
55
|
+
export interface ReporterDisplay {
|
|
56
|
+
readonly name?: string;
|
|
57
|
+
readonly email?: string;
|
|
58
|
+
}
|
|
59
|
+
/** A persisted redeemed Reporter: the server handle plus the display fields. */
|
|
60
|
+
export interface StoredReporter extends ReporterDisplay {
|
|
61
|
+
readonly reporterId: string;
|
|
62
|
+
}
|
|
63
|
+
/** The URL query parameter that carries an invite token. */
|
|
64
|
+
export declare const INVITE_QUERY_PARAM = "fixback_invite";
|
|
65
|
+
/**
|
|
66
|
+
* Read the invite token from a page URL's `?fixback_invite=` param. Returns the
|
|
67
|
+
* token, or `null` when absent, empty, or the URL cannot be parsed — never throws.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readInviteToken(href: string): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Strip the invite token from the address bar (one-time consumption, spec §F) via
|
|
72
|
+
* `history.replaceState`, so a reload or a shared link cannot re-trigger — or
|
|
73
|
+
* re-consume — an already-redeemed Invite. Other params, the path, and the hash
|
|
74
|
+
* are preserved. Best-effort: it never throws into the host page.
|
|
75
|
+
*/
|
|
76
|
+
export declare function stripInviteToken(win: Window): void;
|
|
77
|
+
/**
|
|
78
|
+
* Persist a redeemed Reporter for `key`. The `reporterId` is the identity the SDK
|
|
79
|
+
* presents on later boots; the display name / email ride along as chosen fields.
|
|
80
|
+
* Best-effort — storage being unavailable (private mode) is never fatal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function persistReporter(key: string, reporter: StoredReporter, store?: Storage | null): void;
|
|
83
|
+
/**
|
|
84
|
+
* Read the redeemed Reporter persisted for `key`, or `null` when none is stored,
|
|
85
|
+
* the record is malformed, or it carries no `reporterId`. Never throws.
|
|
86
|
+
*/
|
|
87
|
+
export declare function readStoredReporter(key: string, store?: Storage | null): StoredReporter | null;
|
|
88
|
+
/** Join an API base URL with the invite status path, tolerating a trailing slash. */
|
|
89
|
+
export declare function inviteStatusEndpoint(apiUrl: string, token: string): string;
|
|
90
|
+
/** Join an API base URL with the redeem path for a token. */
|
|
91
|
+
export declare function redeemEndpoint(apiUrl: string, token: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Read an Invite's public status. Resolves to the answer, or `null` when Fixback
|
|
94
|
+
* could not be reached or the body was not a recognised answer. The endpoint
|
|
95
|
+
* answers 200 for a live or dead Invite and 404 (with a JSON body) for an unknown
|
|
96
|
+
* token, so the body — not the HTTP status — is what the caller narrows on. Never
|
|
97
|
+
* throws: an outage stays invisible to the host page.
|
|
98
|
+
*/
|
|
99
|
+
export declare function fetchInviteStatus(apiUrl: string, token: string, fetchImpl?: typeof fetch): Promise<InviteStatusAnswer | null>;
|
|
100
|
+
/**
|
|
101
|
+
* Redeem an Invite by token, minting and returning a Reporter. Resolves to the
|
|
102
|
+
* answer, or `null` on an unreachable API or an unrecognised body. Never throws.
|
|
103
|
+
*/
|
|
104
|
+
export declare function redeemInviteToken(apiUrl: string, token: string, fetchImpl?: typeof fetch): Promise<RedeemAnswer | null>;
|
package/dist/launcher.d.ts
CHANGED
|
@@ -2,22 +2,42 @@
|
|
|
2
2
|
export declare const ROOT_ATTRIBUTE = "data-fixback-root";
|
|
3
3
|
/**
|
|
4
4
|
* Dispatched from the host element when the launcher is activated. The report
|
|
5
|
-
* overlay this opens is
|
|
6
|
-
*
|
|
7
|
-
* `composed` so host-page listeners outside the Shadow DOM can hear it.
|
|
5
|
+
* overlay this opens is wired in `init`; this event is the seam it hangs off. It
|
|
6
|
+
* is `composed` so host-page listeners outside the Shadow DOM can hear it.
|
|
8
7
|
*/
|
|
9
8
|
export declare const LAUNCH_EVENT = "fixback:launch";
|
|
9
|
+
/** Options that shape a mounted launcher. */
|
|
10
|
+
export interface LauncherOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The Project's publishable key. Scopes the first-visit welcome flag so the
|
|
13
|
+
* welcome greets once per key per browser.
|
|
14
|
+
*/
|
|
15
|
+
readonly key?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Still the pulse and motion. An explicit opt-in that complements the OS-level
|
|
18
|
+
* `prefers-reduced-motion` (which the styles already honour on their own).
|
|
19
|
+
*/
|
|
20
|
+
readonly reduceMotion?: boolean;
|
|
21
|
+
}
|
|
10
22
|
/** A mounted launcher and the handle needed to remove it again. */
|
|
11
23
|
export interface Launcher {
|
|
12
24
|
readonly host: HTMLElement;
|
|
25
|
+
/** Clears the launcher's pending timers. Called by {@link unmountLauncher}. */
|
|
26
|
+
readonly dispose: () => void;
|
|
13
27
|
}
|
|
14
28
|
/**
|
|
15
|
-
* Mount the launcher into `target` (typically `document.body`).
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
29
|
+
* Mount the launcher into `target` (typically `document.body`). Everything the
|
|
30
|
+
* launcher draws — the **Feedback** pill, its **tuck** control, the edge **nub**
|
|
31
|
+
* and corner **hover-zone** that peek it back, and the first-visit **welcome**
|
|
32
|
+
* and tuck-away **hint** toasts — lives inside one open Shadow DOM, so its styles
|
|
33
|
+
* are fully isolated from the host page and vice-versa. The host element is
|
|
34
|
+
* fixed-positioned and out of flow, so mounting never shifts the host page's
|
|
35
|
+
* layout. Only one launcher can exist at a time — an earlier one is removed first.
|
|
36
|
+
*
|
|
37
|
+
* Tuck/peek state is expressed as `data-fb-hidden` / `data-fb-peeking` attributes
|
|
38
|
+
* on the host, which the Shadow DOM stylesheet animates; `reduceMotion` sets
|
|
39
|
+
* `data-fb-reduce-motion`, which (with the OS `prefers-reduced-motion`) stills it.
|
|
20
40
|
*/
|
|
21
|
-
export declare function mountLauncher(target: HTMLElement): Launcher;
|
|
41
|
+
export declare function mountLauncher(target: HTMLElement, options?: LauncherOptions): Launcher;
|
|
22
42
|
/** Remove a mounted launcher. Safe to call more than once. */
|
|
23
43
|
export declare function unmountLauncher(launcher: Launcher): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles for the invite onboarding modal (spec §F) — the Signal look of the
|
|
3
|
+
* frozen Reporter prototype (`docs/design/Fixback Reporter.dc.html`), scoped to
|
|
4
|
+
* the modal's own Shadow DOM so the host page is never touched and never touches
|
|
5
|
+
* it. Tokens mirror `overlay-styles.ts` so launcher, overlay, and modal read as
|
|
6
|
+
* one system.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ONBOARDING_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-hint: #b7c1cb;\n --fb-color-border: #dce3ea;\n --fb-color-border-soft: #e6ebf0;\n --fb-color-surface: #ffffff;\n --fb-color-card: #f6f8fa;\n --fb-color-accent-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 all: initial;\n}\n\n*, *::before, *::after { box-sizing: border-box; }\n\n.fb-ob-backdrop {\n position: fixed;\n inset: 0;\n z-index: 2147483010;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: rgba(15, 23, 32, 0.55);\n backdrop-filter: blur(3px);\n font-family: var(--fb-font-sans);\n color: var(--fb-color-text);\n animation: fb-ob-fade 0.25s ease both;\n}\n\n.fb-ob-panel {\n width: 400px;\n max-width: 100%;\n background: var(--fb-color-surface);\n border-radius: 16px;\n box-shadow: 0 30px 70px rgba(15, 40, 70, 0.4);\n overflow: hidden;\n animation: fb-ob-pop 0.32s cubic-bezier(0.2, 0.8, 0.3, 1) both;\n}\n\n.fb-ob-head {\n padding: 22px 24px 0;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.fb-ob-mark {\n width: 22px;\n height: 22px;\n border-radius: 6px;\n background: var(--fb-color-accent);\n flex: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.fb-ob-mark::after {\n content: \"\";\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: #fff;\n}\n.fb-ob-brand { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ob-chip {\n margin-left: auto;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: #8a97a3;\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 6px;\n padding: 2px 7px;\n}\n\n.fb-ob-body { padding: 16px 24px 8px; }\n.fb-ob-title {\n font-size: 19px;\n font-weight: 700;\n color: var(--fb-color-ink);\n letter-spacing: -0.01em;\n}\n.fb-ob-lede {\n font-size: 13.5px;\n color: var(--fb-color-muted);\n line-height: 1.5;\n margin: 7px 0 0;\n}\n.fb-ob-lede strong { color: var(--fb-color-text); }\n\n.fb-ob-card {\n margin: 16px 0;\n padding: 13px 14px;\n background: var(--fb-color-card);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 10px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.fb-ob-row { display: flex; align-items: center; gap: 10px; }\n.fb-ob-row + .fb-ob-row {\n border-top: 1px solid #eef2f6;\n padding-top: 10px;\n}\n.fb-ob-rowlabel {\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: var(--fb-color-faint);\n width: 70px;\n flex: none;\n}\n.fb-ob-site { font-size: 13px; color: var(--fb-color-text); font-weight: 500; }\n.fb-ob-tier {\n font-size: 11px;\n font-weight: 600;\n color: var(--fb-color-accent);\n background: var(--fb-color-accent-bg);\n padding: 3px 9px;\n border-radius: 6px;\n}\n.fb-ob-tiernote { font-size: 11.5px; color: #8a97a3; }\n\n.fb-ob-fieldlabel {\n display: block;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--fb-color-faint);\n margin-bottom: 6px;\n}\n.fb-ob-fields { display: flex; gap: 8px; }\n.fb-ob-input {\n height: 38px;\n padding: 0 12px;\n border: 1px solid var(--fb-color-border);\n border-radius: 9px;\n font-family: inherit;\n font-size: 13px;\n color: var(--fb-color-text);\n outline: none;\n min-width: 0;\n}\n.fb-ob-input:focus { border-color: var(--fb-color-accent); }\n.fb-ob-name { flex: 1; }\n.fb-ob-email { flex: 1.3; font-family: var(--fb-font-mono); font-size: 12.5px; color: var(--fb-color-muted); }\n\n.fb-ob-privacy {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n margin-top: 14px;\n font-size: 11.5px;\n color: #8a97a3;\n line-height: 1.5;\n}\n.fb-ob-privacy svg { flex: none; margin-top: 1px; }\n.fb-ob-privacy strong { color: var(--fb-color-muted); }\n\n.fb-ob-foot { padding: 16px 24px 22px; }\n.fb-ob-confirm {\n width: 100%;\n height: 44px;\n border: 0;\n border-radius: 11px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n.fb-ob-confirm:hover { background: var(--fb-color-accent-hover); }\n.fb-ob-confirm:disabled { opacity: 0.6; cursor: default; }\n\n@keyframes fb-ob-fade { from { opacity: 0; } to { opacity: 1; } }\n@keyframes fb-ob-pop {\n from { opacity: 0; transform: translateY(8px) scale(0.98); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fb-ob-backdrop, .fb-ob-panel { animation: none; }\n}\n";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The invite onboarding modal (spec §F) — the SDK-rendered redemption screen an
|
|
3
|
+
* invited tester sees when a page carries an invite token. It shows the site
|
|
4
|
+
* they were invited to, the **server-derived** access tier the redemption grants,
|
|
5
|
+
* a private-by-default note, and a "Continue as" name / email, then confirms.
|
|
6
|
+
*
|
|
7
|
+
* Built to the frozen Signal Reporter prototype
|
|
8
|
+
* (`docs/design/Fixback Reporter.dc.html`). It mounts lazily inside its own open
|
|
9
|
+
* Shadow DOM so the host page is fully isolated, and — like the launcher and
|
|
10
|
+
* overlay — never throws into the host page. It renders only data the SDK
|
|
11
|
+
* truthfully holds: the site's own origin and the server-derived tier. The name /
|
|
12
|
+
* email are **self-provided display fields**, never a trust signal (§F).
|
|
13
|
+
*/
|
|
14
|
+
import type { ReporterTier } from "./boot";
|
|
15
|
+
import type { ReporterDisplay } from "./invite";
|
|
16
|
+
/** Marks the modal's host element in the light DOM, so it is findable and unique. */
|
|
17
|
+
export declare const ONBOARDING_ATTRIBUTE = "data-fixback-onboard";
|
|
18
|
+
/** Configuration for {@link createOnboardingModal}. */
|
|
19
|
+
export interface OnboardingConfig {
|
|
20
|
+
/** The site the tester was invited to — its origin (`window.location.host`). */
|
|
21
|
+
readonly origin: string;
|
|
22
|
+
/** The **server-derived** tier the redemption grants (from the invite status). */
|
|
23
|
+
readonly tier: ReporterTier;
|
|
24
|
+
/** Prefill the Continue-as fields from a previously stored display identity. */
|
|
25
|
+
readonly defaults?: ReporterDisplay;
|
|
26
|
+
/** Called with the self-provided display fields when the tester confirms. */
|
|
27
|
+
readonly onConfirm: (display: ReporterDisplay) => void;
|
|
28
|
+
/** Where to mount the modal host. Defaults to `document.body`. */
|
|
29
|
+
readonly target?: HTMLElement;
|
|
30
|
+
/** The document to build in. Defaults to the target's owner document. */
|
|
31
|
+
readonly doc?: Document;
|
|
32
|
+
}
|
|
33
|
+
/** A mounted onboarding modal. */
|
|
34
|
+
export interface OnboardingModal {
|
|
35
|
+
destroy(): void;
|
|
36
|
+
readonly host: HTMLElement;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Render the onboarding modal into `target` (default `document.body`) and return a
|
|
40
|
+
* handle to remove it. Confirming reads the name / email, fires `onConfirm` once
|
|
41
|
+
* (further clicks are ignored while the caller redeems), and leaves teardown to
|
|
42
|
+
* the caller so it can strip the token and mount the launcher first.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createOnboardingModal(config: OnboardingConfig): OnboardingModal;
|
package/dist/overlay-styles.d.ts
CHANGED
|
@@ -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-idea: #8b5cf6;\n --fb-color-idea-bg: #f2ecfe;\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-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-tabs { display: flex; gap: 6px; padding: 12px 14px 6px; }\n.fb-ov-tab {\n flex: 1;\n text-align: center;\n font-size: 11px;\n font-weight: 600;\n padding: 6px;\n border-radius: 7px;\n border: 1px solid var(--fb-color-border);\n background: var(--fb-color-surface);\n color: var(--fb-color-muted);\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-tab:hover { border-color: #cfd8e2; }\n.fb-ov-tab.is-active[data-kind=\"bug\"] {\n background: var(--fb-color-bug-bg); color: var(--fb-color-bug); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"improvement\"] {\n background: var(--fb-color-impr-bg); color: var(--fb-color-impr); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"idea\"] {\n background: var(--fb-color-idea-bg); color: var(--fb-color-idea); border-color: transparent;\n}\n\n.fb-ov-mark {\n margin: 10px 14px;\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: 8px; padding: 8px 14px 14px; }\n.fb-ov-pickbtn {\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-pickbtn:hover { border-color: #cfd8e2; }\n.fb-ov-pickbtn.is-active {\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\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.fb-ov-pick { position: fixed; inset: 0; pointer-events: none; z-index: 2147483002; display: none; }\n.fb-ov-panel.is-picking + .fb-ov-pick { display: block; }\n.fb-ov-panel.is-picking { visibility: hidden; }\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@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-idea: #8b5cf6;\n --fb-color-idea-bg: #f2ecfe;\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-tabs { display: flex; gap: 6px; padding: 12px 14px 6px; }\n.fb-ov-tab {\n flex: 1;\n text-align: center;\n font-size: 11px;\n font-weight: 600;\n padding: 6px;\n border-radius: 7px;\n border: 1px solid var(--fb-color-border);\n background: var(--fb-color-surface);\n color: var(--fb-color-muted);\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-tab:hover { border-color: #cfd8e2; }\n.fb-ov-tab.is-active[data-kind=\"bug\"] {\n background: var(--fb-color-bug-bg); color: var(--fb-color-bug); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"improvement\"] {\n background: var(--fb-color-impr-bg); color: var(--fb-color-impr); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"idea\"] {\n background: var(--fb-color-idea-bg); color: var(--fb-color-idea); border-color: transparent;\n}\n\n.fb-ov-mark {\n margin: 10px 14px;\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";
|
package/dist/overlay.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { IdentityInputs } from "./boot";
|
|
1
|
+
import type { IdentityInputs, ProjectGate, ReporterTier } from "./boot";
|
|
2
|
+
import type { Breadcrumb } from "./breadcrumbs";
|
|
2
3
|
import { type ElementPicker, type ElementPickerOptions } from "./element-picker";
|
|
4
|
+
import { type BeforeSend } from "./scrub";
|
|
3
5
|
import { type Capture, type CaptureOptions } from "./screenshot";
|
|
4
6
|
import { type SubmitInput, type SubmitResult } from "./submit";
|
|
5
7
|
/** Marks the overlay's host element in the light DOM (skipped by capture/picker). */
|
|
@@ -13,17 +15,40 @@ export interface OverlayDeps {
|
|
|
13
15
|
readonly submitReport: SubmitReportFn;
|
|
14
16
|
readonly startElementPicker: StartPickerFn;
|
|
15
17
|
}
|
|
18
|
+
/** A read-only view of the trace buffer the overlay attaches to a report. */
|
|
19
|
+
export interface BreadcrumbSource {
|
|
20
|
+
snapshot(): readonly Breadcrumb[];
|
|
21
|
+
}
|
|
16
22
|
/** Configuration for {@link createOverlay}. */
|
|
17
23
|
export interface OverlayConfig {
|
|
18
24
|
readonly apiUrl: string;
|
|
19
25
|
readonly key: string;
|
|
20
26
|
readonly identity?: IdentityInputs;
|
|
27
|
+
/**
|
|
28
|
+
* The Project's Gate, forwarded verbatim from the boot answer. Reserved for
|
|
29
|
+
* Gate-aware launcher / redemption behaviour (spec §A/§F); the overlay itself
|
|
30
|
+
* does not read it yet, but `init` plumbs it through here (ticket #85 / §G).
|
|
31
|
+
*/
|
|
32
|
+
readonly gate?: ProjectGate;
|
|
33
|
+
/**
|
|
34
|
+
* The Reporter's **server-derived** trust tier from the boot answer, or `null`
|
|
35
|
+
* when a presented identity was refused. Rendered as a **display-only** chip in
|
|
36
|
+
* the overlay header (spec §G) — never a trust signal, never read for a decision,
|
|
37
|
+
* and never sent back to the server. When `null`/absent no tier is asserted.
|
|
38
|
+
*/
|
|
39
|
+
readonly tier?: ReporterTier | null;
|
|
21
40
|
readonly sdkVersion?: string;
|
|
22
41
|
/** Where to mount the overlay host. Defaults to `document.body`. */
|
|
23
42
|
readonly target?: HTMLElement;
|
|
24
43
|
readonly doc?: Document;
|
|
25
44
|
readonly win?: Window;
|
|
26
45
|
readonly deps?: Partial<OverlayDeps>;
|
|
46
|
+
/** The trace buffer whose snapshot rides on each report (spec §C). */
|
|
47
|
+
readonly buffer?: BreadcrumbSource | null;
|
|
48
|
+
/** Per-project client scrub hook, run at the `beforeSend` choke point. */
|
|
49
|
+
readonly beforeSend?: BeforeSend;
|
|
50
|
+
/** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
|
|
51
|
+
readonly scrub?: boolean;
|
|
27
52
|
}
|
|
28
53
|
/** A mounted overlay the launcher opens. */
|
|
29
54
|
export interface OverlayController {
|
|
@@ -34,11 +59,16 @@ export interface OverlayController {
|
|
|
34
59
|
}
|
|
35
60
|
/**
|
|
36
61
|
* Create the report overlay — the on-page panel a Reporter files a report from,
|
|
37
|
-
* built to the frozen Signal
|
|
38
|
-
* Shadow DOM (isolated from the host page, and
|
|
39
|
-
* element-picker skip it), opens on the launcher's
|
|
40
|
-
*
|
|
41
|
-
*
|
|
62
|
+
* built to the frozen Signal Reporter prototype (`docs/design/Fixback Reporter.dc.html`).
|
|
63
|
+
* It mounts lazily inside its own Shadow DOM (isolated from the host page, and
|
|
64
|
+
* marked so the screenshot and element-picker skip it), opens on the launcher's
|
|
65
|
+
* `fixback:launch` seam, and offers three **composable, optional** marking layers
|
|
66
|
+
* over one full masked screenshot (spec §B): **element-pick**, **region-capture**
|
|
67
|
+
* (drag), and **draw** (arrow / box / pen / text, with undo / cancel / attach). On
|
|
68
|
+
* Send it captures the masked screenshot, assembles the structured Annotation
|
|
69
|
+
* (`{ element?, region?, marks? }`, spec §D — marks stay vector, never baked into
|
|
70
|
+
* the PNG), and submits to ingest — showing a confirmation on success and failing
|
|
71
|
+
* quietly otherwise.
|
|
42
72
|
*/
|
|
43
73
|
export declare function createOverlay(config: OverlayConfig): OverlayController;
|
|
44
74
|
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region-capture — drag a rectangle over the page to focus the report on one area
|
|
3
|
+
* (spec 0003 §B, the Reporter prototype's capture layer). It produces a `region`
|
|
4
|
+
* rect in screenshot (viewport) coordinates; Escape or a too-small drag abandons
|
|
5
|
+
* it. One of the overlay's three composable, optional marking layers, alongside
|
|
6
|
+
* element-pick and draw.
|
|
7
|
+
*
|
|
8
|
+
* Listeners are attached in the **capture phase** so the drag is intercepted before
|
|
9
|
+
* any host-page handler sees it. It is armed deliberately — the overlay lays a
|
|
10
|
+
* full-screen capture layer over the page (and hides the panel) while it runs — so
|
|
11
|
+
* it captures whatever the Reporter drags, its own layer included. Purely a producer
|
|
12
|
+
* of a `Rect`: it draws nothing; the overlay renders the live selection box from
|
|
13
|
+
* `onProgress`.
|
|
14
|
+
*/
|
|
15
|
+
import { type Rect } from "./annotation";
|
|
16
|
+
/** Options for {@link startRegionCapture}. */
|
|
17
|
+
export interface RegionCaptureOptions {
|
|
18
|
+
/** Document to attach to. Defaults to the global `document`. */
|
|
19
|
+
readonly doc?: Document;
|
|
20
|
+
/** Called on each drag update with the current rect (to draw a selection box). */
|
|
21
|
+
readonly onProgress?: (rect: Rect) => void;
|
|
22
|
+
/** Called with the captured rect when the drag ends large enough to keep. */
|
|
23
|
+
readonly onComplete: (rect: Rect) => void;
|
|
24
|
+
/** Called when the capture is abandoned (Escape, or a drag below the floor). */
|
|
25
|
+
readonly onCancel?: () => void;
|
|
26
|
+
/** Minimum width and height, in px, to count as a capture. Defaults to 12. */
|
|
27
|
+
readonly minSize?: number;
|
|
28
|
+
}
|
|
29
|
+
/** A running region-capture; call {@link RegionCapture.stop} to detach it. */
|
|
30
|
+
export interface RegionCapture {
|
|
31
|
+
stop(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Enter region-capture mode: the next drag on the host page draws out a rectangle,
|
|
35
|
+
* reported live through `onProgress` and committed through `onComplete` on release
|
|
36
|
+
* (when it clears the size floor). Escape, or a drag too small to be meaningful,
|
|
37
|
+
* calls `onCancel`. `stop()` (called automatically on complete/cancel) detaches all
|
|
38
|
+
* listeners.
|
|
39
|
+
*/
|
|
40
|
+
export declare function startRegionCapture(options: RegionCaptureOptions): RegionCapture;
|
package/dist/report.d.ts
CHANGED
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
* (`apps/api/src/ingest/ingest.controller.ts` — `feedbackContentBody`) and the
|
|
9
9
|
* `SubmissionContent` it parses (`apps/api/src/ingest/reporter-identity.ts`).
|
|
10
10
|
*/
|
|
11
|
+
import { type Annotation, type Mark, type Rect } from "./annotation";
|
|
12
|
+
import type { Breadcrumb } from "./breadcrumbs";
|
|
11
13
|
/** The Kind a Reporter tags a report with. Mirrors the server's `ISSUE_KINDS`. */
|
|
12
14
|
export type IssueKind = "bug" | "improvement" | "idea";
|
|
15
|
+
/**
|
|
16
|
+
* Where a Feedback came from — a human in the overlay (`reporter`, the default) or
|
|
17
|
+
* the SDK's automatic error capture (`auto`). Mirrors the server's `FEEDBACK_SOURCES`;
|
|
18
|
+
* the server derives trust independently and ignores anything else the client claims.
|
|
19
|
+
*/
|
|
20
|
+
export type FeedbackSource = "reporter" | "auto";
|
|
13
21
|
/** The picked element's viewport rectangle, as the server's annotation `rect`. */
|
|
14
22
|
export interface ElementRect {
|
|
15
23
|
readonly x: number;
|
|
@@ -18,9 +26,9 @@ export interface ElementRect {
|
|
|
18
26
|
readonly height: number;
|
|
19
27
|
}
|
|
20
28
|
/**
|
|
21
|
-
* The
|
|
22
|
-
* readable DOM path, the tag, and the bounding rect. Exactly the
|
|
23
|
-
*
|
|
29
|
+
* The picked element — the `element` layer of an {@link Annotation}: a stable CSS
|
|
30
|
+
* selector, a readable DOM path, the tag, and the bounding rect. Exactly the
|
|
31
|
+
* server's annotation `element` object shape.
|
|
24
32
|
*/
|
|
25
33
|
export interface SelectedElement {
|
|
26
34
|
readonly selector: string;
|
|
@@ -39,23 +47,41 @@ export interface CaptureEnvironment {
|
|
|
39
47
|
* The JSON content of a feedback submission — the object serialised into the
|
|
40
48
|
* multipart `payload` part next to the `key` and identity evidence. Every field
|
|
41
49
|
* is optional: none of it feeds the server's trust decision, so a submission may
|
|
42
|
-
* carry any subset. `annotation` is the
|
|
43
|
-
* separate binary part, never part of this JSON.
|
|
50
|
+
* carry any subset. `annotation` is the structured `{ element?, region?, marks? }`
|
|
51
|
+
* (spec §D); the screenshot is a separate binary part, never part of this JSON.
|
|
44
52
|
*/
|
|
45
53
|
export interface ReportContent {
|
|
46
54
|
readonly comment?: string;
|
|
47
55
|
readonly kind?: IssueKind;
|
|
48
56
|
readonly url?: string;
|
|
49
57
|
readonly environment?: CaptureEnvironment;
|
|
50
|
-
readonly annotation?:
|
|
58
|
+
readonly annotation?: Annotation;
|
|
59
|
+
/** The masked breadcrumb trace buffer that rode on this report (spec §C). */
|
|
60
|
+
readonly trace?: readonly Breadcrumb[];
|
|
61
|
+
/**
|
|
62
|
+
* Provenance (spec §D/§E). Omitted for a manual report — the transport stamps the
|
|
63
|
+
* `reporter` default on the wire; set to `auto` by the SDK's error capture.
|
|
64
|
+
*/
|
|
65
|
+
readonly source?: FeedbackSource;
|
|
66
|
+
/** For `source: auto` only — the SDK's per-session error fingerprint (spec §E). */
|
|
67
|
+
readonly errorSignature?: string;
|
|
68
|
+
/** For `source: auto` only — the running occurrence count within the session (spec §E). */
|
|
69
|
+
readonly occurrences?: number;
|
|
51
70
|
}
|
|
52
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* What the overlay hands to {@link assembleContent} when the Reporter sends. The
|
|
73
|
+
* three marking layers arrive flat (`element` / `region` / `marks`); `assembleContent`
|
|
74
|
+
* folds whatever is present into the structured {@link Annotation}.
|
|
75
|
+
*/
|
|
53
76
|
export interface ReportDraft {
|
|
54
77
|
readonly kind?: IssueKind;
|
|
55
78
|
readonly comment?: string;
|
|
56
79
|
readonly element?: SelectedElement;
|
|
80
|
+
readonly region?: Rect;
|
|
81
|
+
readonly marks?: ReadonlyArray<Mark>;
|
|
57
82
|
readonly url?: string;
|
|
58
83
|
readonly environment?: CaptureEnvironment;
|
|
84
|
+
readonly trace?: readonly Breadcrumb[];
|
|
59
85
|
}
|
|
60
86
|
/**
|
|
61
87
|
* Read the capture environment off a window: the viewport size, the browser's
|