@fixback/sdk 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fixback
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @fixback/sdk
2
+
3
+ The Fixback capture SDK — a tiny, self-isolating on-page feedback widget.
4
+
5
+ `init({ key })` asks Fixback whether a submission would be accepted for your
6
+ publishable key, the visitor's origin, and the Project's Gate, and mounts a
7
+ launcher **only when it would**. Nothing renders when the origin isn't
8
+ allowlisted or the Gate turns the visitor away, and if Fixback can't be reached
9
+ the SDK stays completely silent — it never throws into the host page.
10
+
11
+ Activating the launcher opens the **report overlay**: the Reporter picks a
12
+ **Kind** (bug / improve / idea), writes a comment, optionally **points at the
13
+ element** they mean, and hits **Send** — which captures a **masked screenshot**
14
+ of the current view, assembles the annotation, and submits it to Fixback. A
15
+ successful send shows a confirmation; a refusal or an unreachable Fixback fails
16
+ quietly, leaving the host page untouched.
17
+
18
+ - **MIT-licensed** and dependency-free.
19
+ - **Isolated styles** — the launcher and overlay each live in a Shadow DOM, so
20
+ the host page's CSS can't reach in and the SDK's CSS can't leak out.
21
+ - **Private by default** — input values are masked **before** the screenshot is
22
+ captured, so private text never leaves the page.
23
+ - **Two builds** — an ESM entry for bundlers and a single minified `<script>`
24
+ file for no-build sites.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install @fixback/sdk
30
+ ```
31
+
32
+ ### With a bundler (ESM)
33
+
34
+ ```ts
35
+ import { init } from "@fixback/sdk";
36
+
37
+ init({ key: "pk_live_your_publishable_key" });
38
+ ```
39
+
40
+ ### No build step (`<script>` tag)
41
+
42
+ The minified UMD build is served from any npm CDN and exposes a `Fixback`
43
+ global:
44
+
45
+ ```html
46
+ <script src="https://unpkg.com/@fixback/sdk"></script>
47
+ <script>
48
+ Fixback.init({ key: "pk_live_your_publishable_key" });
49
+ </script>
50
+ ```
51
+
52
+ ## Options
53
+
54
+ `init(options)` returns a promise that resolves to an instance with a
55
+ `destroy()` method. All options other than `key` are optional.
56
+
57
+ | Option | Type | Default | Description |
58
+ | ---------------- | ------------- | -------------------------- | ----------------------------------------------------------------------- |
59
+ | `key` | `string` | — | Your Project's **publishable** key (an identifier, not a secret). |
60
+ | `apiUrl` | `string` | `https://api.fixback.dev` | The Fixback API origin. Override for a self-hosted or local deployment. |
61
+ | `signedIdentity` | `string` | — | A Signed-identity token minted by your server, for trusted reporters. |
62
+ | `reporterId` | `string` | — | The handle returned when a reporter redeems an invite. |
63
+ | `anonymousId` | `string` | a persisted per-browser id | A stable first-party id for an anonymous reporter. |
64
+ | `target` | `HTMLElement` | `document.body` | Where to mount the launcher. |
65
+
66
+ None of the identity fields is a trust tier — Fixback derives trust on the
67
+ server and never honours a self-declared tier.
68
+
69
+ ```ts
70
+ const fixback = await init({ key: "pk_live_..." });
71
+ // later, to tear the launcher down:
72
+ fixback.destroy();
73
+ ```
74
+
75
+ ## The launch event
76
+
77
+ Activating the launcher opens the SDK's own report overlay. It also dispatches a
78
+ `composed`, bubbling `fixback:launch` event from the SDK's host element, so the
79
+ host page can react to it too:
80
+
81
+ ```ts
82
+ import { LAUNCH_EVENT } from "@fixback/sdk";
83
+
84
+ document.addEventListener(LAUNCH_EVENT, () => {
85
+ // the report overlay is opening — react here if you need to
86
+ });
87
+ ```
88
+
89
+ ## How the boot gate works
90
+
91
+ On `init`, the SDK `POST`s to `${apiUrl}/api/ingest/boot` with your key (the
92
+ browser attaches the `Origin` header itself). The server answers with
93
+ `canSubmit`, and the launcher is mounted only when that is `true`. Any
94
+ non-answer — an unreachable API, a refused key, an unexpected error — is treated
95
+ as "don't show the launcher", silently.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ pnpm --filter @fixback/sdk test # DOM tests (jsdom)
101
+ pnpm --filter @fixback/sdk build # dist/index.mjs + dist/fixback.umd.js + types
102
+ ```
103
+
104
+ `examples/index.html` is a minimal page that loads the built UMD file and calls
105
+ `Fixback.init` — run the build first, then open it in a browser.
106
+
107
+ ## License
108
+
109
+ MIT — see [LICENSE](./LICENSE).
package/dist/boot.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The ingest **boot** wire-contract, vendored.
3
+ *
4
+ * The SDK deliberately does not import `@fixback/shared` — that package is
5
+ * private and server-shaped (ticket #47). The boot request/response is small and
6
+ * stable, so the exact slice the SDK needs is copied here. Keep it in lock-step
7
+ * with the server: the request body accepted by `POST /api/ingest/boot`
8
+ * (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
9
+ * `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
10
+ */
11
+ /** A Project's Gate — who may submit. Mirrors the server's `ProjectGate`. */
12
+ export type ProjectGate = "open" | "invited" | "internal";
13
+ /** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */
14
+ export type ReporterTier = "public" | "invited" | "internal";
15
+ /**
16
+ * Optional identity evidence the SDK forwards to boot. None of it is a tier: the
17
+ * server re-derives trust from this evidence and never honours a self-declared
18
+ * tier, so the SDK does not send one.
19
+ */
20
+ export interface IdentityInputs {
21
+ readonly signedIdentity?: string;
22
+ readonly reporterId?: string;
23
+ readonly anonymousId?: string;
24
+ }
25
+ /** The JSON body `POST /api/ingest/boot` accepts. */
26
+ export interface BootRequest extends IdentityInputs {
27
+ readonly key: string;
28
+ }
29
+ /**
30
+ * The boot answer: whether this origin is allowlisted, the Project's Gate, the
31
+ * caller's derived tier (`null` when a presented identity was refused), and
32
+ * whether a submission would be accepted right now. The launcher shows only when
33
+ * `canSubmit` is true.
34
+ */
35
+ export interface BootAnswer {
36
+ readonly originAllowed: boolean;
37
+ readonly gate: ProjectGate;
38
+ readonly tier: ReporterTier | null;
39
+ readonly canSubmit: boolean;
40
+ }
41
+ /** Join an API base URL with the boot path, tolerating a trailing slash. */
42
+ export declare function bootEndpoint(apiUrl: string): string;
43
+ /**
44
+ * Ask ingest whether a submission would be accepted for this key / origin / Gate.
45
+ * Resolves to the boot answer, or `null` when Fixback could not be reached, the
46
+ * key was refused, or the response was not a boot answer. It never throws: any
47
+ * non-answer is treated by the caller as "do not show the launcher", so a Fixback
48
+ * outage stays invisible to the host page (ticket #47: "fails quietly").
49
+ *
50
+ * The browser attaches the `Origin` header itself on this cross-origin request —
51
+ * the server reads it to decide `originAllowed` — so the SDK neither sets nor
52
+ * needs to set it. `fetchImpl` is injectable purely so the boot call is testable.
53
+ */
54
+ export declare function requestBoot(apiUrl: string, request: BootRequest, fetchImpl?: typeof fetch): Promise<BootAnswer | null>;
package/dist/dom.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Small DOM helpers shared across the SDK's report surfaces.
3
+ *
4
+ * The SDK mounts its own host elements into the host page — the launcher and the
5
+ * overlay panel — each tagged with a `data-fixback-*` marker attribute (the
6
+ * element-picker's highlight lives inside the overlay's shadow, so it is covered
7
+ * by the shadow-boundary walk below). These helpers let the picker skip the SDK's
8
+ * own UI and let the screenshot exclude it, so a report never captures or targets
9
+ * Fixback's chrome.
10
+ */
11
+ /** Marker attributes on the SDK's own host elements in the light DOM. */
12
+ export declare const FIXBACK_HOST_MARKERS: readonly ["data-fixback-root", "data-fixback-overlay"];
13
+ /**
14
+ * Is this node part of the SDK's own UI? Walks up parents and out through any
15
+ * Shadow DOM boundary (via `getRootNode().host`), so a click inside the overlay's
16
+ * shadow is recognised as Fixback's own — not a host-page element to pick.
17
+ */
18
+ export declare function isFixbackNode(node: EventTarget | null | undefined): boolean;
19
+ /**
20
+ * Remove the SDK's own host elements from a (cloned) subtree, so a captured
21
+ * screenshot never contains the launcher, overlay, or picker highlight.
22
+ */
23
+ export declare function stripFixbackNodes(root: ParentNode): void;
@@ -0,0 +1,44 @@
1
+ import type { SelectedElement } from "./report";
2
+ /**
3
+ * A stable CSS selector for `el`: its own id when it has one, otherwise a path of
4
+ * tag (with `:nth-of-type` to disambiguate same-tag siblings) climbing until it
5
+ * reaches an ancestor with an id — the shortest anchor that still resolves.
6
+ */
7
+ export declare function cssSelectorFor(el: Element): string;
8
+ /** A readable ancestry path from the document root down to `el`. */
9
+ export declare function domPathFor(el: Element): string;
10
+ /**
11
+ * Describe a picked element as the Annotation ingest stores: a stable selector, a
12
+ * readable DOM path, the tag, and the viewport bounding rect (spec MVP §B).
13
+ */
14
+ export declare function describeElement(el: Element): SelectedElement;
15
+ /** Options for {@link startElementPicker}. */
16
+ export interface ElementPickerOptions {
17
+ /** Document to attach to. Defaults to the global `document`. */
18
+ readonly doc?: Document;
19
+ /** Called with the element being hovered (or `null` over nothing pickable). */
20
+ readonly onHover?: (element: Element | null) => void;
21
+ /** Called with the element the Reporter clicked to pin. */
22
+ readonly onPick: (element: Element) => void;
23
+ /** Called when the Reporter presses Escape to abandon the pick. */
24
+ readonly onCancel?: () => void;
25
+ /** Return true to skip a node (defaults to skipping the SDK's own UI). */
26
+ readonly ignore?: (element: Element) => boolean;
27
+ }
28
+ /** A running element-picker; call {@link ElementPicker.stop} to detach it. */
29
+ export interface ElementPicker {
30
+ stop(): void;
31
+ }
32
+ /**
33
+ * Enter element-select mode: as the Reporter moves over the host page the element
34
+ * under the pointer is reported through `onHover` (the overlay draws a highlight),
35
+ * a click pins it through `onPick`, and Escape abandons through `onCancel`. The
36
+ * click that pins is swallowed (prevent-default + stop-propagation) so it never
37
+ * also activates the host page. The SDK's own UI is skipped by default, so the
38
+ * Reporter can never pick the overlay itself.
39
+ *
40
+ * Listeners are attached in the capture phase so the pick is intercepted before
41
+ * any host-page handler sees it; `stop()` (called automatically on pick/cancel)
42
+ * detaches them all.
43
+ */
44
+ export declare function startElementPicker(options: ElementPickerOptions): ElementPicker;
@@ -0,0 +1,297 @@
1
+ (function(w,S){typeof exports=="object"&&typeof module!="undefined"?S(exports):typeof define=="function"&&define.amd?define(["exports"],S):(w=typeof globalThis!="undefined"?globalThis:w||self,S(w.Fixback={}))})(this,function(w){Object.defineProperty(w,Symbol.toStringTag,{value:"Module"});function S(e){return`${e.replace(/\/+$/,"")}/api/ingest/boot`}function le(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.originAllowed=="boolean"&&typeof t.canSubmit=="boolean"&&typeof t.gate=="string"&&(t.tier===null||typeof t.tier=="string")}async function ce(e,t,o=fetch){let n;try{n=await o(S(e),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t)})}catch{return null}if(!n.ok)return null;let i;try{i=await n.json()}catch{return null}return le(i)?i:null}var D="fixback.anonymousId";function K(){const e=globalThis.crypto;return e&&typeof e.randomUUID=="function"?e.randomUUID():`fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}function de(){try{const e=globalThis.localStorage,t=e.getItem(D);if(t)return t;const o=K();return e.setItem(D,o),o}catch{return K()}}var fe=`
2
+ :host {
3
+ /* Vendored Signal tokens (packages/ui/src/tokens.css). */
4
+ --fb-color-accent: #2f6fed;
5
+ --fb-color-accent-hover: #245fd0;
6
+ --fb-color-on-emphasis: #ffffff;
7
+ --fb-color-text: #0f1720;
8
+ --fb-font-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto,
9
+ Helvetica, Arial, sans-serif;
10
+
11
+ display: block;
12
+ color: var(--fb-color-text);
13
+ font-family: var(--fb-font-sans);
14
+ font-size: 13px;
15
+ line-height: 1.4;
16
+ -webkit-font-smoothing: antialiased;
17
+ }
18
+
19
+ .fb-launcher {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ gap: 8px;
23
+ box-sizing: border-box;
24
+ height: 40px;
25
+ margin: 0;
26
+ padding: 0 16px;
27
+ border: 0;
28
+ border-radius: 999px;
29
+ background: var(--fb-color-accent);
30
+ color: var(--fb-color-on-emphasis);
31
+ font-family: inherit;
32
+ font-size: 13px;
33
+ font-weight: 600;
34
+ letter-spacing: 0.01em;
35
+ cursor: pointer;
36
+ box-shadow:
37
+ 0 6px 18px rgba(15, 23, 32, 0.16),
38
+ 0 1px 2px rgba(15, 23, 32, 0.12);
39
+ transition:
40
+ background-color 120ms ease,
41
+ transform 120ms ease;
42
+ }
43
+
44
+ .fb-launcher:hover {
45
+ background: var(--fb-color-accent-hover);
46
+ }
47
+
48
+ .fb-launcher:active {
49
+ transform: translateY(1px);
50
+ }
51
+
52
+ .fb-launcher:focus-visible {
53
+ outline: 2px solid var(--fb-color-accent);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ .fb-launcher__icon {
58
+ display: block;
59
+ flex: none;
60
+ width: 16px;
61
+ height: 16px;
62
+ }
63
+
64
+ .fb-launcher__label {
65
+ white-space: nowrap;
66
+ }
67
+
68
+ @media (prefers-reduced-motion: reduce) {
69
+ .fb-launcher {
70
+ transition: none;
71
+ }
72
+ }
73
+ `,q="data-fixback-root",A="fixback:launch",ue='<svg class="fb-launcher__icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M3.25 2h9.5A1.25 1.25 0 0 1 14 3.25v6.5A1.25 1.25 0 0 1 12.75 11H7.6l-3.19 2.55A.6.6 0 0 1 3.4 13.1V11h-.15A1.25 1.25 0 0 1 2 9.75v-6.5A1.25 1.25 0 0 1 3.25 2Z"/></svg>';function be(e){const t=e.ownerDocument,o=t.querySelector(`[${q}]`);o&&o.remove();const n=t.createElement("div");n.setAttribute(q,""),n.style.cssText="position:fixed;right:20px;bottom:20px;z-index:2147483000;margin:0;padding:0;border:0;";const i=n.attachShadow({mode:"open"}),l=t.createElement("style");l.textContent=fe,i.appendChild(l);const a=t.createElement("button");return a.type="button",a.className="fb-launcher",a.setAttribute("aria-haspopup","dialog"),a.setAttribute("aria-label","Give feedback"),a.innerHTML=`${ue}<span class="fb-launcher__label">Feedback</span>`,a.addEventListener("click",()=>{n.dispatchEvent(new CustomEvent(A,{bubbles:!0,composed:!0}))}),i.appendChild(a),e.appendChild(n),{host:n}}function pe(e){e.host.remove()}var X=["data-fixback-root","data-fixback-overlay"],ve=X.map(e=>`[${e}]`).join(",");function he(e){let t=e instanceof Node?e:null;for(;t;){if(t instanceof Element){for(const n of X)if(t.hasAttribute(n))return!0}const o=t.getRootNode();if(o instanceof ShadowRoot&&o!==t){t=o.host;continue}t=t.parentNode}return!1}function me(e){for(const t of Array.from(e.querySelectorAll(ve)))t.remove()}function Y(e){const t=globalThis.CSS;return t!=null&&t.escape?t.escape(e):e.replace(/[^\w-]/g,o=>`\\${o}`)}function ge(e){let t=1,o=e.previousElementSibling;for(;o;)o.tagName===e.tagName&&(t+=1),o=o.previousElementSibling;return t}function xe(e){if(e.id)return`#${Y(e.id)}`;const t=[];let o=e;for(;o&&o.tagName.toLowerCase()!=="html";){if(o.id){t.unshift(`#${Y(o.id)}`);break}const n=o.tagName.toLowerCase(),i=o.parentElement;if(i){const l=Array.from(i.children).filter(a=>a.tagName===o.tagName);t.unshift(l.length>1?`${n}:nth-of-type(${ge(o)})`:n)}else t.unshift(n);o=i}return t.join(" > ")}function ye(e){const t=e.tagName.toLowerCase();if(e.id)return`${t}#${e.id}`;const o=e.classList[0];return o?`${t}.${o}`:t}function ke(e){const t=[];let o=e;for(;o;)t.unshift(ye(o)),o=o.parentElement;return t.join(" > ")}function we(e){const t=e.getBoundingClientRect();return{x:Math.round(t.x),y:Math.round(t.y),width:Math.round(t.width),height:Math.round(t.height)}}function Ee(e){return{selector:xe(e),domPath:ke(e),tag:e.tagName.toLowerCase(),rect:we(e)}}function Se(e){var t,o;const n=(t=e.doc)!==null&&t!==void 0?t:document,i=(o=e.ignore)!==null&&o!==void 0?o:he;let l=null,a=!1;function c(u){var f;const y=(f=(typeof u.composedPath=="function"?u.composedPath():[])[0])!==null&&f!==void 0?f:u.target;return y instanceof Element?i(y)?null:y:null}function b(u){var f;a||(l=c(u),(f=e.onHover)===null||f===void 0||f.call(e,l))}function p(u){var f;if(a)return;const y=(f=c(u))!==null&&f!==void 0?f:l;y&&(u.preventDefault(),u.stopPropagation(),s(),e.onPick(y))}function g(u){if(!a&&u.key==="Escape"){var f;u.preventDefault(),s(),(f=e.onCancel)===null||f===void 0||f.call(e)}}function s(){a||(a=!0,n.removeEventListener("mouseover",b,!0),n.removeEventListener("click",p,!0),n.removeEventListener("keydown",g,!0),l=null)}return n.addEventListener("mouseover",b,!0),n.addEventListener("click",p,!0),n.addEventListener("keydown",g,!0),{stop:s}}function Te(e,t){var o;const n={},i=e.innerWidth;typeof i=="number"&&i>0&&(n.viewportWidth=Math.round(i));const l=e.innerHeight;typeof l=="number"&&l>0&&(n.viewportHeight=Math.round(l));const a=(o=e.navigator)===null||o===void 0?void 0:o.userAgent;return typeof a=="string"&&a.length>0&&(n.browser=a),t.length>0&&(n.sdkVersion=t),n}function Ce(e){var t;const o={};e.kind&&(o.kind=e.kind);const n=(t=e.comment)===null||t===void 0?void 0:t.trim();return n&&(o.comment=n),e.url&&(o.url=e.url),e.environment&&Object.keys(e.environment).length>0&&(o.environment=e.environment),e.element&&(o.annotation=e.element),o}var Le=`
74
+ :host {
75
+ --fb-color-accent: #2f6fed;
76
+ --fb-color-accent-hover: #245fd0;
77
+ --fb-color-on-emphasis: #ffffff;
78
+ --fb-color-ink: #0f1720;
79
+ --fb-color-text: #1a2530;
80
+ --fb-color-muted: #5a6875;
81
+ --fb-color-faint: #9aa7b2;
82
+ --fb-color-border: #e0e6ec;
83
+ --fb-color-border-soft: #e6ebf0;
84
+ --fb-color-surface: #ffffff;
85
+ --fb-color-bug: #e5484d;
86
+ --fb-color-bug-bg: #fdecec;
87
+ --fb-color-impr: #2f6fed;
88
+ --fb-color-impr-bg: #eaf1fe;
89
+ --fb-color-idea: #8b5cf6;
90
+ --fb-color-idea-bg: #f2ecfe;
91
+ --fb-color-success: #2f9e5b;
92
+ --fb-font-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto,
93
+ Helvetica, Arial, sans-serif;
94
+ --fb-font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas,
95
+ monospace;
96
+
97
+ display: block;
98
+ color: var(--fb-color-text);
99
+ font-family: var(--fb-font-sans);
100
+ font-size: 13px;
101
+ line-height: 1.45;
102
+ -webkit-font-smoothing: antialiased;
103
+ }
104
+
105
+ * { box-sizing: border-box; }
106
+
107
+ .fb-ov-panel {
108
+ width: 300px;
109
+ max-width: calc(100vw - 40px);
110
+ background: var(--fb-color-surface);
111
+ border: 1px solid var(--fb-color-border);
112
+ border-radius: 13px;
113
+ box-shadow: 0 18px 44px rgba(20, 40, 70, 0.2);
114
+ overflow: hidden;
115
+ }
116
+
117
+ .fb-ov-head {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 9px;
121
+ padding: 12px 14px;
122
+ border-bottom: 1px solid var(--fb-color-border-soft);
123
+ }
124
+ .fb-ov-mark-sq {
125
+ width: 13px;
126
+ height: 13px;
127
+ border-radius: 4px;
128
+ background: var(--fb-color-accent);
129
+ flex: none;
130
+ }
131
+ .fb-ov-brand { font-size: 14px; font-weight: 600; color: var(--fb-color-ink); }
132
+ .fb-ov-close {
133
+ margin-left: auto;
134
+ border: 0;
135
+ background: none;
136
+ color: var(--fb-color-faint);
137
+ font-size: 16px;
138
+ line-height: 1;
139
+ padding: 2px 4px;
140
+ cursor: pointer;
141
+ border-radius: 6px;
142
+ }
143
+ .fb-ov-close:hover { color: var(--fb-color-muted); background: #f4f7fa; }
144
+
145
+ .fb-ov-tabs { display: flex; gap: 6px; padding: 12px 14px 6px; }
146
+ .fb-ov-tab {
147
+ flex: 1;
148
+ text-align: center;
149
+ font-size: 11px;
150
+ font-weight: 600;
151
+ padding: 6px;
152
+ border-radius: 7px;
153
+ border: 1px solid var(--fb-color-border);
154
+ background: var(--fb-color-surface);
155
+ color: var(--fb-color-muted);
156
+ cursor: pointer;
157
+ font-family: inherit;
158
+ }
159
+ .fb-ov-tab:hover { border-color: #cfd8e2; }
160
+ .fb-ov-tab.is-active[data-kind="bug"] {
161
+ background: var(--fb-color-bug-bg); color: var(--fb-color-bug); border-color: transparent;
162
+ }
163
+ .fb-ov-tab.is-active[data-kind="improvement"] {
164
+ background: var(--fb-color-impr-bg); color: var(--fb-color-impr); border-color: transparent;
165
+ }
166
+ .fb-ov-tab.is-active[data-kind="idea"] {
167
+ background: var(--fb-color-idea-bg); color: var(--fb-color-idea); border-color: transparent;
168
+ }
169
+
170
+ .fb-ov-mark {
171
+ margin: 10px 14px;
172
+ min-height: 52px;
173
+ border-radius: 9px;
174
+ border: 1px solid var(--fb-color-border-soft);
175
+ background: repeating-linear-gradient(135deg, #f4f7fa, #f4f7fa 7px, #eaeff4 7px, #eaeff4 14px);
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: flex-start;
179
+ justify-content: center;
180
+ gap: 6px;
181
+ padding: 10px 12px;
182
+ }
183
+ .fb-ov-selector {
184
+ display: none;
185
+ max-width: 100%;
186
+ font-family: var(--fb-font-mono);
187
+ font-size: 10px;
188
+ color: var(--fb-color-on-emphasis);
189
+ background: var(--fb-color-accent);
190
+ padding: 3px 8px;
191
+ border-radius: 5px;
192
+ overflow: hidden;
193
+ text-overflow: ellipsis;
194
+ white-space: nowrap;
195
+ }
196
+ .fb-ov-mark.has-element .fb-ov-selector { display: inline-block; }
197
+ .fb-ov-mark-caption { font-size: 10px; color: var(--fb-color-faint); font-family: var(--fb-font-mono); }
198
+
199
+ .fb-ov-comment {
200
+ display: block;
201
+ width: calc(100% - 28px);
202
+ margin: 0 14px 10px;
203
+ min-height: 62px;
204
+ resize: vertical;
205
+ font-family: inherit;
206
+ font-size: 13px;
207
+ color: var(--fb-color-text);
208
+ border: 1px solid var(--fb-color-border-soft);
209
+ border-radius: 9px;
210
+ padding: 10px 11px;
211
+ }
212
+ .fb-ov-comment::placeholder { color: var(--fb-color-faint); }
213
+ .fb-ov-comment:focus-visible { outline: 2px solid var(--fb-color-accent); outline-offset: 1px; }
214
+
215
+ .fb-ov-status { padding: 0 14px; font-size: 11px; min-height: 0; }
216
+ .fb-ov-status.is-error { color: var(--fb-color-bug); }
217
+
218
+ .fb-ov-tools { display: flex; align-items: center; gap: 8px; padding: 8px 14px 14px; }
219
+ .fb-ov-pickbtn {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ gap: 6px;
223
+ font-family: inherit;
224
+ font-size: 12px;
225
+ color: var(--fb-color-muted);
226
+ background: var(--fb-color-surface);
227
+ border: 1px solid var(--fb-color-border);
228
+ border-radius: 8px;
229
+ padding: 8px 11px;
230
+ cursor: pointer;
231
+ }
232
+ .fb-ov-pickbtn:hover { border-color: #cfd8e2; }
233
+ .fb-ov-pickbtn.is-active {
234
+ color: var(--fb-color-accent);
235
+ border-color: var(--fb-color-accent);
236
+ background: var(--fb-color-impr-bg);
237
+ }
238
+ .fb-ov-pickbtn__glyph { font-size: 14px; line-height: 1; }
239
+
240
+ .fb-ov-send {
241
+ margin-left: auto;
242
+ font-family: inherit;
243
+ font-size: 13px;
244
+ font-weight: 600;
245
+ color: var(--fb-color-on-emphasis);
246
+ background: var(--fb-color-accent);
247
+ border: 0;
248
+ border-radius: 8px;
249
+ padding: 9px 18px;
250
+ cursor: pointer;
251
+ }
252
+ .fb-ov-send:hover { background: var(--fb-color-accent-hover); }
253
+ .fb-ov-send:disabled { opacity: 0.6; cursor: default; }
254
+
255
+ .fb-ov-done { display: none; padding: 24px 18px; text-align: center; }
256
+ .fb-ov-panel.is-sent .fb-ov-form { display: none; }
257
+ .fb-ov-panel.is-sent .fb-ov-done { display: block; }
258
+ .fb-ov-done__check {
259
+ width: 40px; height: 40px; margin: 0 auto 12px;
260
+ border-radius: 50%;
261
+ background: #e7f6ee; color: var(--fb-color-success);
262
+ display: flex; align-items: center; justify-content: center;
263
+ font-size: 20px; font-weight: 700;
264
+ }
265
+ .fb-ov-done__title { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }
266
+ .fb-ov-done__sub { font-size: 12px; color: var(--fb-color-muted); margin-top: 4px; }
267
+
268
+ .fb-ov-pick { position: fixed; inset: 0; pointer-events: none; z-index: 2147483002; display: none; }
269
+ .fb-ov-panel.is-picking + .fb-ov-pick { display: block; }
270
+ .fb-ov-panel.is-picking { visibility: hidden; }
271
+ .fb-ov-highlight {
272
+ position: absolute;
273
+ border: 2px dashed var(--fb-color-accent);
274
+ border-radius: 6px;
275
+ box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);
276
+ transition: all 60ms ease;
277
+ }
278
+ .fb-ov-hint {
279
+ position: absolute;
280
+ top: 16px;
281
+ left: 50%;
282
+ transform: translateX(-50%);
283
+ font-family: var(--fb-font-mono);
284
+ font-size: 11px;
285
+ color: var(--fb-color-on-emphasis);
286
+ background: var(--fb-color-ink);
287
+ padding: 6px 12px;
288
+ border-radius: 7px;
289
+ box-shadow: 0 8px 20px rgba(20, 40, 70, 0.25);
290
+ }
291
+
292
+ @media (prefers-reduced-motion: reduce) {
293
+ .fb-ov-highlight { transition: none; }
294
+ }
295
+ `,rt="•",Ae=32,Ie=new Set(["button","submit","reset","checkbox","radio","range","color","file","image","hidden"]);function R(e){const t=Math.min(Math.max(e,1),Ae);return"•".repeat(t)}function _e(e){for(const n of Array.from(e.querySelectorAll("input"))){var t;const i=((t=n.getAttribute("type"))!==null&&t!==void 0?t:"text").toLowerCase();if(Ie.has(i))continue;const l=n.value;if(!l)continue;const a=R(l.length);n.value=a,n.setAttribute("value",a)}for(const n of Array.from(e.querySelectorAll("textarea"))){const i=n.value||n.textContent||"";if(!i)continue;const l=R(i.length);n.value=l,n.textContent=l}for(const n of Array.from(e.querySelectorAll("[contenteditable]"))){var o;if(n.getAttribute("contenteditable")==="false")continue;const i=(o=n.textContent)!==null&&o!==void 0?o:"";i.trim()&&(n.textContent=R(i.length))}}function G(e,t,o){const n=e==null?void 0:e[`inner${o}`],i=t.documentElement[`client${o}`];return Math.max(1,Math.round((typeof n=="number"&&n>0?n:i)||0)||1)}function Ne(e,t,o){return`<svg xmlns="http://www.w3.org/2000/svg" width="${t}" height="${o}"><foreignObject x="0" y="0" width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml">${new XMLSerializer().serializeToString(e)}</div></foreignObject></svg>`}var Oe=4e3,Re=(e,{width:t,height:o,type:n})=>new Promise(i=>{let l=!1;const a=c=>{l||(l=!0,i(c))};try{const c=new Image,b=setTimeout(()=>a(null),Oe);c.onload=()=>{try{const p=document.createElement("canvas");p.width=t,p.height=o;const g=p.getContext("2d");if(!g){clearTimeout(b),a(null);return}g.drawImage(c,0,0),p.toBlob(s=>{clearTimeout(b),a(s)},n)}catch{clearTimeout(b),a(null)}},c.onerror=()=>{clearTimeout(b),a(null)},c.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(e)}`}catch{a(null)}});async function Me(e={}){try{var t,o,n,i,l,a;const c=(t=e.doc)!==null&&t!==void 0?t:document,b=(o=(n=e.win)!==null&&n!==void 0?n:c.defaultView)!==null&&o!==void 0?o:void 0,p=(i=e.target)!==null&&i!==void 0?i:c.documentElement,g=(l=e.type)!==null&&l!==void 0?l:"image/png",s=G(b,c,"Width"),u=G(b,c,"Height"),f=p.cloneNode(!0);me(f),_e(f);const y=Ne(f,s,u),h=await((a=e.rasterize)!==null&&a!==void 0?a:Re)(y,{width:s,height:u,type:g});return h?{blob:h,width:s,height:u,type:g}:null}catch{return null}}var Be="screenshot";function Pe(e){return`${e.replace(/\/+$/,"")}/api/ingest/feedback`}function ze(e){if(!e)return{};const t={};return e.signedIdentity&&(t.signedIdentity=e.signedIdentity),e.reporterId&&(t.reporterId=e.reporterId),e.anonymousId&&(t.anonymousId=e.anonymousId),t}async function Ue(e,t,o=fetch){var n;const i={key:t.key,...ze(t.identity),...t.content},l=new FormData;if(l.append("payload",JSON.stringify(i)),!((n=t.screenshot)===null||n===void 0)&&n.blob){var a;l.append(Be,t.screenshot.blob,(a=t.screenshot.filename)!==null&&a!==void 0?a:"screenshot.png")}let c;try{c=await o(Pe(e),{method:"POST",body:l})}catch{return{ok:!1,reason:"unreachable"}}if(!c.ok)return{ok:!1,reason:"refused",status:c.status};try{return{ok:!0,feedback:await c.json()}}catch{return{ok:!0,feedback:null}}}var W="0.1.0",$e="data-fixback-overlay",He=[{value:"bug",label:"Bug"},{value:"improvement",label:"Improve"},{value:"idea",label:"Idea"}],Ve=1800;function d(e,t,o={},n=[]){const i=e.createElement(t);for(const[l,a]of Object.entries(o))a!==void 0&&i.setAttribute(l,a);for(const l of n)i.appendChild(typeof l=="string"?e.createTextNode(l):l);return i}var je={"image/png":"png","image/jpeg":"jpg","image/webp":"webp","image/gif":"gif"};function Fe(e){var t;return(t=je[e])!==null&&t!==void 0?t:"png"}function De(e){var t,o,n,i,l,a,c,b,p,g;const s=(t=e.doc)!==null&&t!==void 0?t:document,u=(o=(n=e.win)!==null&&n!==void 0?n:s.defaultView)!==null&&o!==void 0?o:window,f=(i=e.sdkVersion)!==null&&i!==void 0?i:W,y={captureView:(l=(a=e.deps)===null||a===void 0?void 0:a.captureView)!==null&&l!==void 0?l:Me,submitReport:(c=(b=e.deps)===null||b===void 0?void 0:b.submitReport)!==null&&c!==void 0?c:Ue,startElementPicker:(p=(g=e.deps)===null||g===void 0?void 0:g.startElementPicker)!==null&&p!==void 0?p:Se};let h=null,r=null,T=null,_=null,N=!1;const x={kind:"bug",selectedElement:null,phase:"compose"};function O(v,m){r&&(r.status.textContent=v,r.status.classList.toggle("is-error",m))}function Q(v){if(x.kind=v,!!r)for(const[m,E]of r.tabs){const k=m===v;E.classList.toggle("is-active",k),E.setAttribute("aria-pressed",String(k))}}function ee(){r&&(x.selectedElement?(r.mark.classList.add("has-element"),r.selector.textContent=x.selectedElement.selector,r.caption.textContent="Element selected · masked screenshot on Send",r.pickBtn.textContent="",r.pickBtn.append(J(s),s.createTextNode("Change element"))):(r.mark.classList.remove("has-element"),r.selector.textContent="",r.caption.textContent="Masked screenshot attached on Send",r.pickBtn.textContent="",r.pickBtn.append(J(s),s.createTextNode("Pick element"))))}function qe(v){if(!r)return;if(!v){r.highlight.style.display="none";return}const m=v.getBoundingClientRect();r.highlight.style.display="block",r.highlight.style.left=`${m.left}px`,r.highlight.style.top=`${m.top}px`,r.highlight.style.width=`${m.width}px`,r.highlight.style.height=`${m.height}px`}function M(){T==null||T.stop(),T=null}function C(){r&&(r.panel.classList.remove("is-picking"),r.pickBtn.classList.remove("is-active"),r.highlight.style.display="none"),M()}function Xe(){r&&(M(),r.panel.classList.add("is-picking"),r.pickBtn.classList.add("is-active"),T=y.startElementPicker({doc:s,onHover:qe,onPick:v=>{x.selectedElement=Ee(v),C(),ee()},onCancel:C}))}function B(){_!==null&&(clearTimeout(_),_=null)}async function Ye(){if(!(!r||x.phase==="sending")){x.phase="sending",r.sendBtn.disabled=!0,r.sendBtn.textContent="Sending…",O("",!1);try{var v,m;const E=Ce({kind:x.kind,comment:r.comment.value,element:(v=x.selectedElement)!==null&&v!==void 0?v:void 0,url:(m=u.location)===null||m===void 0?void 0:m.href,environment:Te(u,f)}),k=await y.captureView({doc:s,win:u}),U=k?{blob:k.blob,filename:`screenshot.${Fe(k.type)}`}:null,L=await y.submitReport(e.apiUrl,{key:e.key,identity:e.identity,content:E,screenshot:U});L.ok?(x.phase="sent",r.panel.classList.add("is-sent"),_=setTimeout(z,Ve)):(P(),O(L.reason==="unreachable"?"Couldn't reach Fixback — try again.":"Fixback couldn't accept this report.",!0))}catch{P(),O("Something went wrong — try again.",!0)}}}function P(){x.phase="compose",r&&(r.sendBtn.disabled=!1,r.sendBtn.textContent="Send")}function Ge(){var v;h=d(s,"div",{[$e]:""}),h.style.cssText="position:fixed;right:20px;bottom:84px;z-index:2147483001;margin:0;padding:0;border:0;display:none;";const m=h.attachShadow({mode:"open"}),E=s.createElement("style");E.textContent=Le,m.appendChild(E);const k=d(s,"button",{type:"button",class:"fb-ov-close","aria-label":"Close"},["✕"]),U=d(s,"div",{class:"fb-ov-head"},[d(s,"span",{class:"fb-ov-mark-sq"}),d(s,"span",{class:"fb-ov-brand"},["Fixback"]),k]),L=new Map,te=d(s,"div",{class:"fb-ov-tabs"});for(const{value:j,label:nt}of He){const F=d(s,"button",{type:"button",class:"fb-ov-tab","data-kind":j},[nt]);F.addEventListener("click",()=>Q(j)),L.set(j,F),te.appendChild(F)}const oe=d(s,"span",{class:"fb-ov-selector"}),ne=d(s,"span",{class:"fb-ov-mark-caption"}),re=d(s,"div",{class:"fb-ov-mark"},[oe,ne]),ie=d(s,"textarea",{class:"fb-ov-comment",placeholder:"Describe what you saw or want…","aria-label":"Comment"}),ae=d(s,"div",{class:"fb-ov-status",role:"status","aria-live":"polite"}),$=d(s,"button",{type:"button",class:"fb-ov-pickbtn"});$.addEventListener("click",()=>{x.phase!=="sending"&&(r!=null&&r.panel.classList.contains("is-picking")?C():Xe())});const H=d(s,"button",{type:"button",class:"fb-ov-send"},["Send"]);H.addEventListener("click",()=>{Ye()});const Qe=d(s,"div",{class:"fb-ov-tools"},[$,H]),et=d(s,"div",{class:"fb-ov-form"},[te,re,ie,ae,Qe]),tt=d(s,"div",{class:"fb-ov-done"},[d(s,"div",{class:"fb-ov-done__check"},["✓"]),d(s,"div",{class:"fb-ov-done__title"},["Report sent"]),d(s,"div",{class:"fb-ov-done__sub"},["Thanks — the team can see it now."])]),se=d(s,"div",{class:"fb-ov-panel",role:"dialog","aria-label":"Fixback feedback"},[U,et,tt]),V=d(s,"div",{class:"fb-ov-highlight"});V.style.display="none";const ot=d(s,"div",{class:"fb-ov-pick"},[d(s,"div",{class:"fb-ov-hint"},["Click the element you mean · Esc to cancel"]),V]);m.appendChild(se),m.appendChild(ot),k.addEventListener("click",z),((v=e.target)!==null&&v!==void 0?v:s.body).appendChild(h),r={panel:se,tabs:L,mark:re,selector:oe,caption:ne,comment:ie,status:ae,pickBtn:$,sendBtn:H,highlight:V}}function We(){(!h||!r)&&Ge()}function Je(){if(We(),!(!h||!r)){B(),C(),x.phase="compose",x.selectedElement=null,r.panel.classList.remove("is-sent"),r.comment.value="",P(),O("",!1),Q("bug"),ee(),h.style.display="block",N=!0;try{r.comment.focus()}catch{}}}function z(){B(),C(),h&&(h.style.display="none"),N=!1}function Ze(){B(),M(),h==null||h.remove(),h=null,r=null,N=!1}return{open:Je,close:z,destroy:Ze,get isOpen(){return N}}}function J(e){return d(e,"span",{class:"fb-ov-pickbtn__glyph","aria-hidden":"true"},["⌖"])}var Z="https://api.fixback.dev",I={destroy(){}};async function Ke(e){try{var t,o,n;if(typeof document=="undefined")return I;const i=e==null?void 0:e.key;if(typeof i!="string"||i.length===0)return I;const l=(t=e.apiUrl)!==null&&t!==void 0?t:Z,a=(o=e.anonymousId)!==null&&o!==void 0?o:de(),c=await ce(l,{key:i,signedIdentity:e.signedIdentity,reporterId:e.reporterId,anonymousId:a});if(!(c!=null&&c.canSubmit))return I;const b=(n=e.target)!==null&&n!==void 0?n:document.body,p=be(b),g=De({apiUrl:l,key:i,identity:{signedIdentity:e.signedIdentity,reporterId:e.reporterId,anonymousId:a},sdkVersion:W,target:b}),s=()=>g.open();return p.host.addEventListener(A,s),{destroy:()=>{p.host.removeEventListener(A,s),g.destroy(),pe(p)}}}catch{return I}}w.DEFAULT_API_URL=Z,w.LAUNCH_EVENT=A,w.init=Ke});
296
+
297
+ //# sourceMappingURL=fixback.umd.js.map