@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 +21 -0
- package/README.md +109 -0
- package/dist/boot.d.ts +54 -0
- package/dist/dom.d.ts +23 -0
- package/dist/element-picker.d.ts +44 -0
- package/dist/fixback.umd.js +297 -0
- package/dist/fixback.umd.js.map +1 -0
- package/dist/identity.d.ts +12 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.mjs +891 -0
- package/dist/index.mjs.map +1 -0
- package/dist/init.d.ts +34 -0
- package/dist/launcher.d.ts +23 -0
- package/dist/overlay-styles.d.ts +9 -0
- package/dist/overlay.d.ts +44 -0
- package/dist/report.d.ts +73 -0
- package/dist/screenshot.d.ts +52 -0
- package/dist/styles.d.ts +10 -0
- package/dist/submit.d.ts +39 -0
- package/dist/version.d.ts +7 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/boot.ts","../src/identity.ts","../src/styles.ts","../src/launcher.ts","../src/dom.ts","../src/element-picker.ts","../src/report.ts","../src/overlay-styles.ts","../src/screenshot.ts","../src/submit.ts","../src/version.ts","../src/overlay.ts","../src/init.ts"],"sourcesContent":["/**\n * The ingest **boot** wire-contract, vendored.\n *\n * The SDK deliberately does not import `@fixback/shared` — that package is\n * private and server-shaped (ticket #47). The boot request/response is small and\n * stable, so the exact slice the SDK needs is copied here. Keep it in lock-step\n * with the server: the request body accepted by `POST /api/ingest/boot`\n * (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by\n * `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).\n */\n\n/** A Project's Gate — who may submit. Mirrors the server's `ProjectGate`. */\nexport type ProjectGate = \"open\" | \"invited\" | \"internal\";\n\n/** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */\nexport type ReporterTier = \"public\" | \"invited\" | \"internal\";\n\n/**\n * Optional identity evidence the SDK forwards to boot. None of it is a tier: the\n * server re-derives trust from this evidence and never honours a self-declared\n * tier, so the SDK does not send one.\n */\nexport interface IdentityInputs {\n readonly signedIdentity?: string;\n readonly reporterId?: string;\n readonly anonymousId?: string;\n}\n\n/** The JSON body `POST /api/ingest/boot` accepts. */\nexport interface BootRequest extends IdentityInputs {\n readonly key: string;\n}\n\n/**\n * The boot answer: whether this origin is allowlisted, the Project's Gate, the\n * caller's derived tier (`null` when a presented identity was refused), and\n * whether a submission would be accepted right now. The launcher shows only when\n * `canSubmit` is true.\n */\nexport interface BootAnswer {\n readonly originAllowed: boolean;\n readonly gate: ProjectGate;\n readonly tier: ReporterTier | null;\n readonly canSubmit: boolean;\n}\n\n/** Join an API base URL with the boot path, tolerating a trailing slash. */\nexport function bootEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/ingest/boot`;\n}\n\n/** Narrow an unknown JSON body to a `BootAnswer` before the SDK trusts it. */\nfunction isBootAnswer(value: unknown): value is BootAnswer {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.originAllowed === \"boolean\" &&\n typeof v.canSubmit === \"boolean\" &&\n typeof v.gate === \"string\" &&\n (v.tier === null || typeof v.tier === \"string\")\n );\n}\n\n/**\n * Ask ingest whether a submission would be accepted for this key / origin / Gate.\n * Resolves to the boot answer, or `null` when Fixback could not be reached, the\n * key was refused, or the response was not a boot answer. It never throws: any\n * non-answer is treated by the caller as \"do not show the launcher\", so a Fixback\n * outage stays invisible to the host page (ticket #47: \"fails quietly\").\n *\n * The browser attaches the `Origin` header itself on this cross-origin request —\n * the server reads it to decide `originAllowed` — so the SDK neither sets nor\n * needs to set it. `fetchImpl` is injectable purely so the boot call is testable.\n */\nexport async function requestBoot(\n apiUrl: string,\n request: BootRequest,\n fetchImpl: typeof fetch = fetch,\n): Promise<BootAnswer | null> {\n let response: Response;\n try {\n response = await fetchImpl(bootEndpoint(apiUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request),\n });\n } catch {\n return null; // network error / Fixback unreachable\n }\n\n if (!response.ok) return null; // 401 unknown key, or any other refusal\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n return null;\n }\n\n return isBootAnswer(body) ? body : null;\n}\n","/**\n * The Public Reporter's stable, first-party anonymous id.\n *\n * When the host site does not supply its own identity, the SDK establishes a\n * per-browser id and persists it, so repeated boots from the same browser are one\n * Reporter (the server counts distinct Reporters by it). Persistence is\n * best-effort: if storage is unavailable (private mode, blocked cookies), we fall\n * back to an ephemeral id rather than throw — the SDK must never disturb the host\n * page.\n */\n\nconst STORAGE_KEY = \"fixback.anonymousId\";\n\n/** A random id: a UUID where the platform offers one, else a compact fallback. */\nfunction generateId(): string {\n const c: Crypto | undefined = globalThis.crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/** Return this browser's anonymous id, creating and persisting one on first use. */\nexport function ensureAnonymousId(): string {\n try {\n const store = globalThis.localStorage;\n const existing = store.getItem(STORAGE_KEY);\n if (existing) return existing;\n const id = generateId();\n store.setItem(STORAGE_KEY, id);\n return id;\n } catch {\n // Storage blocked or absent — an ephemeral id keeps this boot working.\n return generateId();\n }\n}\n","/**\n * The launcher's isolated styles.\n *\n * These live inside the launcher's Shadow DOM (see `launcher.ts`), so nothing\n * here can reach the host page and nothing on the host page can reach in. The\n * token values are **vendored** Signal design tokens — copied from\n * `packages/ui/src/tokens.css` rather than imported, because the SDK must not\n * depend on `@fixback/ui` at runtime (ticket #47). Keep them in sync by value.\n */\nexport const LAUNCHER_STYLES = `\n:host {\n /* Vendored Signal tokens (packages/ui/src/tokens.css). */\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-text: #0f1720;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\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.4;\n -webkit-font-smoothing: antialiased;\n}\n\n.fb-launcher {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n height: 40px;\n margin: 0;\n padding: 0 16px;\n border: 0;\n border-radius: 999px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n cursor: pointer;\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 1px 2px rgba(15, 23, 32, 0.12);\n transition:\n background-color 120ms ease,\n transform 120ms ease;\n}\n\n.fb-launcher:hover {\n background: var(--fb-color-accent-hover);\n}\n\n.fb-launcher:active {\n transform: translateY(1px);\n}\n\n.fb-launcher:focus-visible {\n outline: 2px solid var(--fb-color-accent);\n outline-offset: 2px;\n}\n\n.fb-launcher__icon {\n display: block;\n flex: none;\n width: 16px;\n height: 16px;\n}\n\n.fb-launcher__label {\n white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fb-launcher {\n transition: none;\n }\n}\n`;\n","import { LAUNCHER_STYLES } from \"./styles\";\n\n/** Marks the SDK's host element in the light DOM, so it is findable and unique. */\nexport const ROOT_ATTRIBUTE = \"data-fixback-root\";\n\n/**\n * Dispatched from the host element when the launcher is activated. The report\n * overlay this opens is a later ticket (#54); for now the launcher is the mounted\n * entry point, and this event is the seam the overlay will hang off. It is\n * `composed` so host-page listeners outside the Shadow DOM can hear it.\n */\nexport const LAUNCH_EVENT = \"fixback:launch\";\n\nconst ICON =\n '<svg class=\"fb-launcher__icon\" viewBox=\"0 0 16 16\" aria-hidden=\"true\" focusable=\"false\">' +\n '<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\"/>' +\n \"</svg>\";\n\n/** A mounted launcher and the handle needed to remove it again. */\nexport interface Launcher {\n readonly host: HTMLElement;\n}\n\n/**\n * Mount the launcher into `target` (typically `document.body`). The visible\n * button lives inside an open Shadow DOM so its styles are fully isolated from\n * the host page and vice-versa; the host element itself is fixed-positioned and\n * out of flow, so mounting never shifts the host page's layout. Only one launcher\n * can exist at a time — an earlier one is removed first.\n */\nexport function mountLauncher(target: HTMLElement): Launcher {\n const doc = target.ownerDocument;\n\n const previous = doc.querySelector(`[${ROOT_ATTRIBUTE}]`);\n if (previous) previous.remove();\n\n const host = doc.createElement(\"div\");\n host.setAttribute(ROOT_ATTRIBUTE, \"\");\n // Positioning is inlined on the host (not in the Shadow DOM sheet) so it holds\n // even against a hostile host-page reset, and so the launcher is anchored to\n // the viewport without ever participating in the page's flow.\n host.style.cssText =\n \"position:fixed;right:20px;bottom:20px;z-index:2147483000;margin:0;padding:0;border:0;\";\n\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = doc.createElement(\"style\");\n style.textContent = LAUNCHER_STYLES;\n shadow.appendChild(style);\n\n const button = doc.createElement(\"button\");\n button.type = \"button\";\n button.className = \"fb-launcher\";\n button.setAttribute(\"aria-haspopup\", \"dialog\");\n button.setAttribute(\"aria-label\", \"Give feedback\");\n button.innerHTML = `${ICON}<span class=\"fb-launcher__label\">Feedback</span>`;\n button.addEventListener(\"click\", () => {\n host.dispatchEvent(\n new CustomEvent(LAUNCH_EVENT, { bubbles: true, composed: true }),\n );\n });\n shadow.appendChild(button);\n\n target.appendChild(host);\n return { host };\n}\n\n/** Remove a mounted launcher. Safe to call more than once. */\nexport function unmountLauncher(launcher: Launcher): void {\n launcher.host.remove();\n}\n","/**\n * Small DOM helpers shared across the SDK's report surfaces.\n *\n * The SDK mounts its own host elements into the host page — the launcher and the\n * overlay panel — each tagged with a `data-fixback-*` marker attribute (the\n * element-picker's highlight lives inside the overlay's shadow, so it is covered\n * by the shadow-boundary walk below). These helpers let the picker skip the SDK's\n * own UI and let the screenshot exclude it, so a report never captures or targets\n * Fixback's chrome.\n */\n\n/** Marker attributes on the SDK's own host elements in the light DOM. */\nexport const FIXBACK_HOST_MARKERS = [\n \"data-fixback-root\",\n \"data-fixback-overlay\",\n] as const;\n\nconst HOST_SELECTOR = FIXBACK_HOST_MARKERS.map((m) => `[${m}]`).join(\",\");\n\n/**\n * Is this node part of the SDK's own UI? Walks up parents and out through any\n * Shadow DOM boundary (via `getRootNode().host`), so a click inside the overlay's\n * shadow is recognised as Fixback's own — not a host-page element to pick.\n */\nexport function isFixbackNode(node: EventTarget | null | undefined): boolean {\n let current: Node | null = node instanceof Node ? node : null;\n while (current) {\n if (current instanceof Element) {\n for (const marker of FIXBACK_HOST_MARKERS) {\n if (current.hasAttribute(marker)) return true;\n }\n }\n const root = current.getRootNode();\n if (root instanceof ShadowRoot && root !== current) {\n current = root.host;\n continue;\n }\n current = current.parentNode;\n }\n return false;\n}\n\n/**\n * Remove the SDK's own host elements from a (cloned) subtree, so a captured\n * screenshot never contains the launcher, overlay, or picker highlight.\n */\nexport function stripFixbackNodes(root: ParentNode): void {\n for (const el of Array.from(root.querySelectorAll(HOST_SELECTOR))) {\n el.remove();\n }\n}\n","import { isFixbackNode } from \"./dom\";\nimport type { ElementRect, SelectedElement } from \"./report\";\n\n/**\n * Escape a string for use in a CSS selector, preferring the platform's\n * `CSS.escape` and falling back to a conservative backslash escape where it is\n * unavailable (older engines, some test environments).\n */\nfunction cssEscape(value: string): string {\n const api = (globalThis as { CSS?: { escape?: (v: string) => string } }).CSS;\n if (api?.escape) return api.escape(value);\n return value.replace(/[^\\w-]/g, (ch) => `\\\\${ch}`);\n}\n\n/** The 1-based position of `el` among its same-tag siblings (`:nth-of-type`). */\nfunction nthOfType(el: Element): number {\n let n = 1;\n let sib = el.previousElementSibling;\n while (sib) {\n if (sib.tagName === el.tagName) n += 1;\n sib = sib.previousElementSibling;\n }\n return n;\n}\n\n/**\n * A stable CSS selector for `el`: its own id when it has one, otherwise a path of\n * tag (with `:nth-of-type` to disambiguate same-tag siblings) climbing until it\n * reaches an ancestor with an id — the shortest anchor that still resolves.\n */\nexport function cssSelectorFor(el: Element): string {\n if (el.id) return `#${cssEscape(el.id)}`;\n\n const parts: string[] = [];\n let node: Element | null = el;\n while (node && node.tagName.toLowerCase() !== \"html\") {\n if (node.id) {\n parts.unshift(`#${cssEscape(node.id)}`);\n break;\n }\n const tag = node.tagName.toLowerCase();\n const parent: Element | null = node.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter(\n (c) => c.tagName === node!.tagName,\n );\n parts.unshift(\n sameTag.length > 1 ? `${tag}:nth-of-type(${nthOfType(node)})` : tag,\n );\n } else {\n parts.unshift(tag);\n }\n node = parent;\n }\n return parts.join(\" > \");\n}\n\n/** One node's readable label: `tag`, `tag#id`, or `tag.firstClass`. */\nfunction describeNode(el: Element): string {\n const tag = el.tagName.toLowerCase();\n if (el.id) return `${tag}#${el.id}`;\n const firstClass = el.classList[0];\n return firstClass ? `${tag}.${firstClass}` : tag;\n}\n\n/** A readable ancestry path from the document root down to `el`. */\nexport function domPathFor(el: Element): string {\n const chain: string[] = [];\n let node: Element | null = el;\n while (node) {\n chain.unshift(describeNode(node));\n node = node.parentElement;\n }\n return chain.join(\" > \");\n}\n\nfunction roundedRect(el: Element): ElementRect {\n const r = el.getBoundingClientRect();\n return {\n x: Math.round(r.x),\n y: Math.round(r.y),\n width: Math.round(r.width),\n height: Math.round(r.height),\n };\n}\n\n/**\n * Describe a picked element as the Annotation ingest stores: a stable selector, a\n * readable DOM path, the tag, and the viewport bounding rect (spec MVP §B).\n */\nexport function describeElement(el: Element): SelectedElement {\n return {\n selector: cssSelectorFor(el),\n domPath: domPathFor(el),\n tag: el.tagName.toLowerCase(),\n rect: roundedRect(el),\n };\n}\n\n/** Options for {@link startElementPicker}. */\nexport interface ElementPickerOptions {\n /** Document to attach to. Defaults to the global `document`. */\n readonly doc?: Document;\n /** Called with the element being hovered (or `null` over nothing pickable). */\n readonly onHover?: (element: Element | null) => void;\n /** Called with the element the Reporter clicked to pin. */\n readonly onPick: (element: Element) => void;\n /** Called when the Reporter presses Escape to abandon the pick. */\n readonly onCancel?: () => void;\n /** Return true to skip a node (defaults to skipping the SDK's own UI). */\n readonly ignore?: (element: Element) => boolean;\n}\n\n/** A running element-picker; call {@link ElementPicker.stop} to detach it. */\nexport interface ElementPicker {\n stop(): void;\n}\n\n/**\n * Enter element-select mode: as the Reporter moves over the host page the element\n * under the pointer is reported through `onHover` (the overlay draws a highlight),\n * a click pins it through `onPick`, and Escape abandons through `onCancel`. The\n * click that pins is swallowed (prevent-default + stop-propagation) so it never\n * also activates the host page. The SDK's own UI is skipped by default, so the\n * Reporter can never pick the overlay itself.\n *\n * Listeners are attached in the capture phase so the pick is intercepted before\n * any host-page handler sees it; `stop()` (called automatically on pick/cancel)\n * detaches them all.\n */\nexport function startElementPicker(options: ElementPickerOptions): ElementPicker {\n const doc = options.doc ?? document;\n const ignore = options.ignore ?? isFixbackNode;\n let current: Element | null = null;\n let stopped = false;\n\n function resolveTarget(event: Event): Element | null {\n const path =\n typeof event.composedPath === \"function\" ? event.composedPath() : [];\n const deepest = path[0] ?? event.target;\n if (deepest instanceof Element) return ignore(deepest) ? null : deepest;\n return null;\n }\n\n function onOver(event: Event): void {\n if (stopped) return;\n current = resolveTarget(event);\n options.onHover?.(current);\n }\n\n function onClick(event: MouseEvent): void {\n if (stopped) return;\n const el = resolveTarget(event) ?? current;\n if (!el) return; // nothing pickable under the pointer — let the click pass\n event.preventDefault();\n event.stopPropagation();\n stop();\n options.onPick(el);\n }\n\n function onKey(event: KeyboardEvent): void {\n if (stopped) return;\n if (event.key === \"Escape\") {\n event.preventDefault();\n stop();\n options.onCancel?.();\n }\n }\n\n function stop(): void {\n if (stopped) return;\n stopped = true;\n doc.removeEventListener(\"mouseover\", onOver, true);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"keydown\", onKey, true);\n current = null;\n }\n\n doc.addEventListener(\"mouseover\", onOver, true);\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"keydown\", onKey, true);\n\n return { stop };\n}\n","/**\n * The ingest **feedback** content wire-contract, vendored, plus the helpers that\n * assemble it from what a Reporter composed in the overlay.\n *\n * Like `boot.ts`, the SDK does not import `@fixback/shared`; the slice of the\n * submission shape it needs is copied here. Keep it in lock-step with the server:\n * the JSON `payload` part accepted by `POST /api/ingest/feedback`\n * (`apps/api/src/ingest/ingest.controller.ts` — `feedbackContentBody`) and the\n * `SubmissionContent` it parses (`apps/api/src/ingest/reporter-identity.ts`).\n */\n\n/** The Kind a Reporter tags a report with. Mirrors the server's `ISSUE_KINDS`. */\nexport type IssueKind = \"bug\" | \"improvement\" | \"idea\";\n\n/** The picked element's viewport rectangle, as the server's annotation `rect`. */\nexport interface ElementRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * The Annotation — the element a Reporter pointed at: a stable CSS selector, a\n * readable DOM path, the tag, and the bounding rect. Exactly the server's\n * `annotation` object shape.\n */\nexport interface SelectedElement {\n readonly selector: string;\n readonly domPath: string;\n readonly tag: string;\n readonly rect: ElementRect;\n}\n\n/** The capture environment recorded alongside a report. */\nexport interface CaptureEnvironment {\n readonly viewportWidth?: number;\n readonly viewportHeight?: number;\n readonly browser?: string;\n readonly sdkVersion?: string;\n}\n\n/**\n * The JSON content of a feedback submission — the object serialised into the\n * multipart `payload` part next to the `key` and identity evidence. Every field\n * is optional: none of it feeds the server's trust decision, so a submission may\n * carry any subset. `annotation` is the selected element; the screenshot is a\n * separate binary part, never part of this JSON.\n */\nexport interface ReportContent {\n readonly comment?: string;\n readonly kind?: IssueKind;\n readonly url?: string;\n readonly environment?: CaptureEnvironment;\n readonly annotation?: SelectedElement;\n}\n\n/** What the overlay hands to {@link assembleContent} when the Reporter sends. */\nexport interface ReportDraft {\n readonly kind?: IssueKind;\n readonly comment?: string;\n readonly element?: SelectedElement;\n readonly url?: string;\n readonly environment?: CaptureEnvironment;\n}\n\n/**\n * Read the capture environment off a window: the viewport size, the browser's\n * user-agent, and the SDK version. A dimension is recorded only when it is a\n * positive number (a headless/zero viewport records nothing rather than an\n * invalid `0`, which the server would reject).\n */\nexport function collectEnvironment(\n win: Window,\n sdkVersion: string,\n): CaptureEnvironment {\n const env: {\n viewportWidth?: number;\n viewportHeight?: number;\n browser?: string;\n sdkVersion?: string;\n } = {};\n\n const width = win.innerWidth;\n if (typeof width === \"number\" && width > 0) env.viewportWidth = Math.round(width);\n const height = win.innerHeight;\n if (typeof height === \"number\" && height > 0) {\n env.viewportHeight = Math.round(height);\n }\n\n const ua = win.navigator?.userAgent;\n if (typeof ua === \"string\" && ua.length > 0) env.browser = ua;\n\n if (sdkVersion.length > 0) env.sdkVersion = sdkVersion;\n\n return env;\n}\n\n/**\n * Assemble the ingest content from what the Reporter composed. Empty pieces are\n * dropped rather than sent as blanks: a whitespace-only comment, an absent\n * element, or an empty environment are simply omitted, so the payload carries\n * only what was actually provided (mirroring the server's all-optional content).\n */\nexport function assembleContent(draft: ReportDraft): ReportContent {\n const content: {\n comment?: string;\n kind?: IssueKind;\n url?: string;\n environment?: CaptureEnvironment;\n annotation?: SelectedElement;\n } = {};\n\n if (draft.kind) content.kind = draft.kind;\n\n const comment = draft.comment?.trim();\n if (comment) content.comment = comment;\n\n if (draft.url) content.url = draft.url;\n\n if (draft.environment && Object.keys(draft.environment).length > 0) {\n content.environment = draft.environment;\n }\n\n if (draft.element) content.annotation = draft.element;\n\n return content;\n}\n","/**\n * The report overlay's isolated styles, built to the frozen Signal Reporter\n * overlay (`docs/design/Fixback Visual Identity.dc.html`, badge `1d`). Like the\n * launcher's styles these live inside the overlay's Shadow DOM, so nothing here\n * reaches the host page and nothing on the host page reaches in. Token values are\n * vendored Signal tokens (copied from `packages/ui/src/tokens.css`, not imported —\n * the SDK must not depend on `@fixback/ui` at runtime). Keep them in sync by value.\n */\nexport 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`;\n","import { stripFixbackNodes } from \"./dom\";\n\n/**\n * Client-side masked screenshot capture (spec MVP §E; ticket #54).\n *\n * The capture is **private-by-default**: input values are masked and the SDK's\n * own UI is removed from a *clone* of the view **before** anything is rasterised,\n * so no unmasked text and none of Fixback's chrome ever reaches the image. The\n * approach is dependency-free — the cloned, masked DOM is serialised into an SVG\n * `<foreignObject>` and drawn onto a `<canvas>` — honouring the SDK's \"no runtime\n * dependencies / no host-page disturbance\" constraints. The raster step is\n * injectable so the pipeline (and the masking-before-capture guarantee) is\n * testable without a real canvas, and it fails quietly: any problem resolves to\n * `null` and the report is simply sent without a screenshot.\n */\n\n/** The character private input content is replaced with. */\nexport const MASK_CHAR = \"•\";\n\n/** Upper bound on mask length, so a huge field doesn't produce a huge string. */\nconst MAX_MASK_LENGTH = 32;\n\n/** Input `type`s that carry no free-text private data, so are left as-is. */\nconst NON_TEXT_INPUT_TYPES = new Set([\n \"button\",\n \"submit\",\n \"reset\",\n \"checkbox\",\n \"radio\",\n \"range\",\n \"color\",\n \"file\",\n \"image\",\n \"hidden\",\n]);\n\nfunction maskString(length: number): string {\n const n = Math.min(Math.max(length, 1), MAX_MASK_LENGTH);\n return MASK_CHAR.repeat(n);\n}\n\n/**\n * Mask the private, user-entered content in a subtree: text `input` values,\n * `textarea` content, and `contenteditable` text. Developer-authored text\n * (placeholders, button labels, non-text controls) is left untouched. Operates in\n * place — call it on a *clone* of the view, never the live page.\n */\nexport function maskInputs(root: ParentNode): void {\n for (const input of Array.from(root.querySelectorAll(\"input\"))) {\n const type = (input.getAttribute(\"type\") ?? \"text\").toLowerCase();\n if (NON_TEXT_INPUT_TYPES.has(type)) continue;\n const value = input.value;\n if (!value) continue;\n const masked = maskString(value.length);\n input.value = masked;\n input.setAttribute(\"value\", masked);\n }\n\n for (const area of Array.from(root.querySelectorAll(\"textarea\"))) {\n const value = area.value || area.textContent || \"\";\n if (!value) continue;\n const masked = maskString(value.length);\n area.value = masked;\n area.textContent = masked;\n }\n\n for (const editable of Array.from(\n root.querySelectorAll<HTMLElement>(\"[contenteditable]\"),\n )) {\n if (editable.getAttribute(\"contenteditable\") === \"false\") continue;\n const text = editable.textContent ?? \"\";\n if (!text.trim()) continue;\n editable.textContent = maskString(text.length);\n }\n}\n\n/** A captured screenshot: the image bytes plus its dimensions and content type. */\nexport interface Capture {\n readonly blob: Blob;\n readonly width: number;\n readonly height: number;\n readonly type: string;\n}\n\n/** Turns a serialised SVG of the view into image bytes (injectable for tests). */\nexport type Rasterize = (\n svg: string,\n meta: { width: number; height: number; type: string },\n) => Promise<Blob | null>;\n\n/** Options for {@link captureView}. */\nexport interface CaptureOptions {\n /** The element to capture. Defaults to the document element (the full view). */\n readonly target?: Element;\n readonly doc?: Document;\n readonly win?: Window;\n /** Output content type. Defaults to `image/png`. */\n readonly type?: string;\n /** Override the raster step (the default draws via an SVG + `<canvas>`). */\n readonly rasterize?: Rasterize;\n}\n\nfunction viewportExtent(\n win: Window | undefined,\n doc: Document,\n axis: \"Width\" | \"Height\",\n): number {\n const inner = win?.[`inner${axis}`];\n const client = doc.documentElement[`client${axis}`];\n const value = typeof inner === \"number\" && inner > 0 ? inner : client;\n return Math.max(1, Math.round(value || 0) || 1);\n}\n\nfunction serializeToSvg(node: Element, width: number, height: number): string {\n const serialized = new XMLSerializer().serializeToString(node);\n return (\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">` +\n `<foreignObject x=\"0\" y=\"0\" width=\"100%\" height=\"100%\">` +\n `<div xmlns=\"http://www.w3.org/1999/xhtml\">${serialized}</div>` +\n `</foreignObject></svg>`\n );\n}\n\n/** How long the default rasteriser waits for the SVG image before giving up. */\nconst RASTER_TIMEOUT_MS = 4000;\n\n/**\n * The default raster step: load the serialised SVG as an image and draw it onto a\n * canvas. Guarded at every turn — a missing 2D context, a tainted canvas (a\n * cross-origin image on the page), an image that never loads, or any thrown error\n * resolves to `null` rather than rejecting, so a capture failure never breaks the\n * Send.\n */\nconst rasterizeViaCanvas: Rasterize = (svg, { width, height, type }) =>\n new Promise((resolve) => {\n let settled = false;\n const done = (blob: Blob | null): void => {\n if (settled) return;\n settled = true;\n resolve(blob);\n };\n try {\n const image = new Image();\n const timer = setTimeout(() => done(null), RASTER_TIMEOUT_MS);\n image.onload = () => {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n clearTimeout(timer);\n done(null);\n return;\n }\n ctx.drawImage(image, 0, 0);\n canvas.toBlob((blob) => {\n clearTimeout(timer);\n done(blob);\n }, type);\n } catch {\n clearTimeout(timer);\n done(null);\n }\n };\n image.onerror = () => {\n clearTimeout(timer);\n done(null);\n };\n image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n } catch {\n done(null);\n }\n });\n\n/**\n * Capture the current view as a masked screenshot. Clones the target, removes the\n * SDK's own UI, masks input values — **all before** serialising and rasterising —\n * then returns the image bytes, or `null` if capture wasn't possible.\n */\nexport async function captureView(\n options: CaptureOptions = {},\n): Promise<Capture | null> {\n try {\n const doc = options.doc ?? document;\n const win = options.win ?? doc.defaultView ?? undefined;\n const target = options.target ?? doc.documentElement;\n const type = options.type ?? \"image/png\";\n const width = viewportExtent(win, doc, \"Width\");\n const height = viewportExtent(win, doc, \"Height\");\n\n const clone = target.cloneNode(true) as Element;\n stripFixbackNodes(clone);\n maskInputs(clone);\n\n const svg = serializeToSvg(clone, width, height);\n const rasterize = options.rasterize ?? rasterizeViaCanvas;\n const blob = await rasterize(svg, { width, height, type });\n if (!blob) return null;\n return { blob, width, height, type };\n } catch {\n return null;\n }\n}\n","import type { IdentityInputs, ReporterTier } from \"./boot\";\nimport type { ReportContent } from \"./report\";\n\n/**\n * The multipart feedback submission — the transport half of the capture loop.\n *\n * `POST /api/ingest/feedback` (spec MVP §C; tickets #50/#53) is a multipart\n * request: a single text part `payload` carrying the JSON (publishable key +\n * identity evidence + content) and an optional binary part `screenshot`. The\n * content-type header is deliberately not set, so the runtime writes the\n * `multipart/form-data` boundary itself. Like `requestBoot`, this never throws:\n * an unreachable Fixback or a refusal is a quiet result, so a Fixback problem\n * never breaks the host page.\n */\n\n/** The multipart field the screenshot binary is sent under (mirrors the server). */\nconst SCREENSHOT_FIELD = \"screenshot\";\n\n/** What ingest returns for an accepted submission (vendored server shape). */\nexport interface RecordedFeedback {\n readonly feedbackId: string;\n readonly issueId: string;\n readonly reporterId: string;\n readonly tier: ReporterTier;\n}\n\n/** The captured screenshot to attach, if any. */\nexport interface SubmitScreenshot {\n readonly blob: Blob;\n readonly filename?: string;\n}\n\n/** Everything a single feedback submission carries. */\nexport interface SubmitInput {\n readonly key: string;\n readonly identity?: IdentityInputs;\n readonly content: ReportContent;\n readonly screenshot?: SubmitScreenshot | null;\n}\n\n/** The outcome of a submission — never an exception. */\nexport type SubmitResult =\n | { readonly ok: true; readonly feedback: RecordedFeedback | null }\n | {\n readonly ok: false;\n readonly reason: \"unreachable\" | \"refused\";\n readonly status?: number;\n };\n\n/** Join an API base URL with the feedback path, tolerating a trailing slash. */\nexport function feedbackEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/ingest/feedback`;\n}\n\n/** Drop `undefined` identity fields so the payload carries only what was given. */\nfunction compactIdentity(identity: IdentityInputs | undefined): IdentityInputs {\n if (!identity) return {};\n const out: { signedIdentity?: string; reporterId?: string; anonymousId?: string } = {};\n if (identity.signedIdentity) out.signedIdentity = identity.signedIdentity;\n if (identity.reporterId) out.reporterId = identity.reporterId;\n if (identity.anonymousId) out.anonymousId = identity.anonymousId;\n return out;\n}\n\n/**\n * Submit a report to ingest. Assembles the `payload` JSON (key + identity +\n * content) and the optional `screenshot` file into a `FormData`, posts it, and\n * resolves to the recorded Feedback on success or a named failure otherwise. The\n * `fetchImpl` seam exists purely so the call is testable.\n */\nexport async function submitReport(\n apiUrl: string,\n input: SubmitInput,\n fetchImpl: typeof fetch = fetch,\n): Promise<SubmitResult> {\n const payload = {\n key: input.key,\n ...compactIdentity(input.identity),\n ...input.content,\n };\n\n const form = new FormData();\n form.append(\"payload\", JSON.stringify(payload));\n if (input.screenshot?.blob) {\n form.append(\n SCREENSHOT_FIELD,\n input.screenshot.blob,\n input.screenshot.filename ?? \"screenshot.png\",\n );\n }\n\n let response: Response;\n try {\n response = await fetchImpl(feedbackEndpoint(apiUrl), {\n method: \"POST\",\n body: form,\n });\n } catch {\n return { ok: false, reason: \"unreachable\" };\n }\n\n if (!response.ok) {\n return { ok: false, reason: \"refused\", status: response.status };\n }\n\n try {\n const feedback = (await response.json()) as RecordedFeedback;\n return { ok: true, feedback };\n } catch {\n // Accepted, but the body wasn't the expected JSON — still a success.\n return { ok: true, feedback: null };\n }\n}\n","/**\n * The SDK's own version string, reported to ingest as `environment.sdkVersion`\n * (spec MVP §B/§E). Kept as a hand-maintained constant rather than imported from\n * `package.json`, so the bundle stays a single self-contained file with no JSON\n * import — keep it in step with `package.json` and the Changesets bump.\n */\nexport const SDK_VERSION = \"0.1.0\";\n","import type { IdentityInputs } from \"./boot\";\nimport {\n describeElement,\n startElementPicker,\n type ElementPicker,\n type ElementPickerOptions,\n} from \"./element-picker\";\nimport {\n assembleContent,\n collectEnvironment,\n type IssueKind,\n type SelectedElement,\n} from \"./report\";\nimport { OVERLAY_STYLES } from \"./overlay-styles\";\nimport { captureView, type Capture, type CaptureOptions } from \"./screenshot\";\nimport { submitReport, type SubmitInput, type SubmitResult } from \"./submit\";\nimport { SDK_VERSION } from \"./version\";\n\n/** Marks the overlay's host element in the light DOM (skipped by capture/picker). */\nexport const OVERLAY_ATTRIBUTE = \"data-fixback-overlay\";\n\n/** The Kind tabs, in order — value is the ingest Kind, label is the overlay text. */\nconst KIND_TABS: ReadonlyArray<{ value: IssueKind; label: string }> = [\n { value: \"bug\", label: \"Bug\" },\n { value: \"improvement\", label: \"Improve\" },\n { value: \"idea\", label: \"Idea\" },\n];\n\n/** How long the confirmation shows before the overlay closes itself. */\nconst AUTO_CLOSE_MS = 1800;\n\ntype CaptureViewFn = (options?: CaptureOptions) => Promise<Capture | null>;\ntype SubmitReportFn = (\n apiUrl: string,\n input: SubmitInput,\n fetchImpl?: typeof fetch,\n) => Promise<SubmitResult>;\ntype StartPickerFn = (options: ElementPickerOptions) => ElementPicker;\n\n/** Injectable collaborators, defaulted to the real implementations. */\nexport interface OverlayDeps {\n readonly captureView: CaptureViewFn;\n readonly submitReport: SubmitReportFn;\n readonly startElementPicker: StartPickerFn;\n}\n\n/** Configuration for {@link createOverlay}. */\nexport interface OverlayConfig {\n readonly apiUrl: string;\n readonly key: string;\n readonly identity?: IdentityInputs;\n readonly sdkVersion?: string;\n /** Where to mount the overlay host. Defaults to `document.body`. */\n readonly target?: HTMLElement;\n readonly doc?: Document;\n readonly win?: Window;\n readonly deps?: Partial<OverlayDeps>;\n}\n\n/** A mounted overlay the launcher opens. */\nexport interface OverlayController {\n open(): void;\n close(): void;\n destroy(): void;\n readonly isOpen: boolean;\n}\n\ninterface Refs {\n readonly panel: HTMLElement;\n readonly tabs: Map<IssueKind, HTMLButtonElement>;\n readonly mark: HTMLElement;\n readonly selector: HTMLElement;\n readonly caption: HTMLElement;\n readonly comment: HTMLTextAreaElement;\n readonly status: HTMLElement;\n readonly pickBtn: HTMLButtonElement;\n readonly sendBtn: HTMLButtonElement;\n readonly highlight: HTMLElement;\n}\n\nfunction h<K extends keyof HTMLElementTagNameMap>(\n doc: Document,\n tag: K,\n props: Partial<Record<string, string>> = {},\n children: ReadonlyArray<Node | string> = [],\n): HTMLElementTagNameMap[K] {\n const node = doc.createElement(tag);\n for (const [name, value] of Object.entries(props)) {\n if (value !== undefined) node.setAttribute(name, value);\n }\n for (const child of children) {\n node.appendChild(typeof child === \"string\" ? doc.createTextNode(child) : child);\n }\n return node;\n}\n\nconst EXTENSIONS: Readonly<Record<string, string>> = {\n \"image/png\": \"png\",\n \"image/jpeg\": \"jpg\",\n \"image/webp\": \"webp\",\n \"image/gif\": \"gif\",\n};\n\nfunction extensionFor(type: string): string {\n return EXTENSIONS[type] ?? \"png\";\n}\n\n/**\n * Create the report overlay — the on-page panel a Reporter files a report from,\n * built to the frozen Signal overlay (`1d`). It mounts lazily inside its own\n * Shadow DOM (isolated from the host page, and marked so the screenshot and\n * element-picker skip it), opens on the launcher's `fixback:launch` seam, and on\n * Send captures a masked screenshot, assembles the Annotation, and submits to\n * ingest — showing a confirmation on success and failing quietly otherwise.\n */\nexport function createOverlay(config: OverlayConfig): OverlayController {\n const doc = config.doc ?? document;\n const win = config.win ?? doc.defaultView ?? window;\n const sdkVersion = config.sdkVersion ?? SDK_VERSION;\n const deps: OverlayDeps = {\n captureView: config.deps?.captureView ?? captureView,\n submitReport: config.deps?.submitReport ?? submitReport,\n startElementPicker: config.deps?.startElementPicker ?? startElementPicker,\n };\n\n let host: HTMLElement | null = null;\n let refs: Refs | null = null;\n let picker: ElementPicker | null = null;\n let autoClose: ReturnType<typeof setTimeout> | null = null;\n let open = false;\n\n const state: {\n kind: IssueKind;\n selectedElement: SelectedElement | null;\n phase: \"compose\" | \"sending\" | \"sent\";\n } = { kind: \"bug\", selectedElement: null, phase: \"compose\" };\n\n function setStatus(message: string, isError: boolean): void {\n if (!refs) return;\n refs.status.textContent = message;\n refs.status.classList.toggle(\"is-error\", isError);\n }\n\n function setKind(kind: IssueKind): void {\n state.kind = kind;\n if (!refs) return;\n for (const [value, button] of refs.tabs) {\n const active = value === kind;\n button.classList.toggle(\"is-active\", active);\n button.setAttribute(\"aria-pressed\", String(active));\n }\n }\n\n function renderElement(): void {\n if (!refs) return;\n if (state.selectedElement) {\n refs.mark.classList.add(\"has-element\");\n refs.selector.textContent = state.selectedElement.selector;\n refs.caption.textContent = \"Element selected · masked screenshot on Send\";\n refs.pickBtn.textContent = \"\";\n refs.pickBtn.append(glyph(doc), doc.createTextNode(\"Change element\"));\n } else {\n refs.mark.classList.remove(\"has-element\");\n refs.selector.textContent = \"\";\n refs.caption.textContent = \"Masked screenshot attached on Send\";\n refs.pickBtn.textContent = \"\";\n refs.pickBtn.append(glyph(doc), doc.createTextNode(\"Pick element\"));\n }\n }\n\n function positionHighlight(element: Element | null): void {\n if (!refs) return;\n if (!element) {\n refs.highlight.style.display = \"none\";\n return;\n }\n const rect = element.getBoundingClientRect();\n refs.highlight.style.display = \"block\";\n refs.highlight.style.left = `${rect.left}px`;\n refs.highlight.style.top = `${rect.top}px`;\n refs.highlight.style.width = `${rect.width}px`;\n refs.highlight.style.height = `${rect.height}px`;\n }\n\n function stopPicker(): void {\n picker?.stop();\n picker = null;\n }\n\n function endPick(): void {\n if (refs) {\n refs.panel.classList.remove(\"is-picking\");\n refs.pickBtn.classList.remove(\"is-active\");\n refs.highlight.style.display = \"none\";\n }\n stopPicker();\n }\n\n function beginPick(): void {\n if (!refs) return;\n stopPicker();\n refs.panel.classList.add(\"is-picking\");\n refs.pickBtn.classList.add(\"is-active\");\n picker = deps.startElementPicker({\n doc,\n onHover: positionHighlight,\n onPick: (element) => {\n state.selectedElement = describeElement(element);\n endPick();\n renderElement();\n },\n onCancel: endPick,\n });\n }\n\n function clearAutoClose(): void {\n if (autoClose !== null) {\n clearTimeout(autoClose);\n autoClose = null;\n }\n }\n\n async function send(): Promise<void> {\n if (!refs || state.phase === \"sending\") return;\n state.phase = \"sending\";\n refs.sendBtn.disabled = true;\n refs.sendBtn.textContent = \"Sending…\";\n setStatus(\"\", false);\n try {\n const content = assembleContent({\n kind: state.kind,\n comment: refs.comment.value,\n element: state.selectedElement ?? undefined,\n url: win.location?.href,\n environment: collectEnvironment(win, sdkVersion),\n });\n const capture = await deps.captureView({ doc, win });\n const screenshot = capture\n ? { blob: capture.blob, filename: `screenshot.${extensionFor(capture.type)}` }\n : null;\n const result = await deps.submitReport(config.apiUrl, {\n key: config.key,\n identity: config.identity,\n content,\n screenshot,\n });\n if (result.ok) {\n state.phase = \"sent\";\n refs.panel.classList.add(\"is-sent\");\n autoClose = setTimeout(close, AUTO_CLOSE_MS);\n } else {\n resetSendControl();\n setStatus(\n result.reason === \"unreachable\"\n ? \"Couldn't reach Fixback — try again.\"\n : \"Fixback couldn't accept this report.\",\n true,\n );\n }\n } catch {\n resetSendControl();\n setStatus(\"Something went wrong — try again.\", true);\n }\n }\n\n function resetSendControl(): void {\n state.phase = \"compose\";\n if (!refs) return;\n refs.sendBtn.disabled = false;\n refs.sendBtn.textContent = \"Send\";\n }\n\n function build(): void {\n host = h(doc, \"div\", { [OVERLAY_ATTRIBUTE]: \"\" });\n host.style.cssText =\n \"position:fixed;right:20px;bottom:84px;z-index:2147483001;margin:0;padding:0;border:0;display:none;\";\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = doc.createElement(\"style\");\n style.textContent = OVERLAY_STYLES;\n shadow.appendChild(style);\n\n // Header\n const closeBtn = h(doc, \"button\", { type: \"button\", class: \"fb-ov-close\", \"aria-label\": \"Close\" }, [\"✕\"]);\n const head = h(doc, \"div\", { class: \"fb-ov-head\" }, [\n h(doc, \"span\", { class: \"fb-ov-mark-sq\" }),\n h(doc, \"span\", { class: \"fb-ov-brand\" }, [\"Fixback\"]),\n closeBtn,\n ]);\n\n // Kind tabs\n const tabs = new Map<IssueKind, HTMLButtonElement>();\n const tabRow = h(doc, \"div\", { class: \"fb-ov-tabs\" });\n for (const { value, label } of KIND_TABS) {\n const tab = h(doc, \"button\", {\n type: \"button\",\n class: \"fb-ov-tab\",\n \"data-kind\": value,\n }, [label]);\n tab.addEventListener(\"click\", () => setKind(value));\n tabs.set(value, tab);\n tabRow.appendChild(tab);\n }\n\n // Annotation / screenshot marking region\n const selector = h(doc, \"span\", { class: \"fb-ov-selector\" });\n const caption = h(doc, \"span\", { class: \"fb-ov-mark-caption\" });\n const mark = h(doc, \"div\", { class: \"fb-ov-mark\" }, [selector, caption]);\n\n // Comment\n const comment = h(doc, \"textarea\", {\n class: \"fb-ov-comment\",\n placeholder: \"Describe what you saw or want…\",\n \"aria-label\": \"Comment\",\n });\n\n // Status + tools\n const status = h(doc, \"div\", { class: \"fb-ov-status\", role: \"status\", \"aria-live\": \"polite\" });\n const pickBtn = h(doc, \"button\", { type: \"button\", class: \"fb-ov-pickbtn\" });\n pickBtn.addEventListener(\"click\", () => {\n if (state.phase === \"sending\") return;\n if (refs?.panel.classList.contains(\"is-picking\")) endPick();\n else beginPick();\n });\n const sendBtn = h(doc, \"button\", { type: \"button\", class: \"fb-ov-send\" }, [\"Send\"]);\n sendBtn.addEventListener(\"click\", () => void send());\n const tools = h(doc, \"div\", { class: \"fb-ov-tools\" }, [pickBtn, sendBtn]);\n\n const form = h(doc, \"div\", { class: \"fb-ov-form\" }, [tabRow, mark, comment, status, tools]);\n\n // Confirmation\n const done = h(doc, \"div\", { class: \"fb-ov-done\" }, [\n h(doc, \"div\", { class: \"fb-ov-done__check\" }, [\"✓\"]),\n h(doc, \"div\", { class: \"fb-ov-done__title\" }, [\"Report sent\"]),\n h(doc, \"div\", { class: \"fb-ov-done__sub\" }, [\"Thanks — the team can see it now.\"]),\n ]);\n\n const panel = h(doc, \"div\", {\n class: \"fb-ov-panel\",\n role: \"dialog\",\n \"aria-label\": \"Fixback feedback\",\n }, [head, form, done]);\n\n // Element-pick highlight layer (visual only; never intercepts host events)\n const highlight = h(doc, \"div\", { class: \"fb-ov-highlight\" });\n highlight.style.display = \"none\";\n const pickLayer = h(doc, \"div\", { class: \"fb-ov-pick\" }, [\n h(doc, \"div\", { class: \"fb-ov-hint\" }, [\"Click the element you mean · Esc to cancel\"]),\n highlight,\n ]);\n\n shadow.appendChild(panel);\n shadow.appendChild(pickLayer);\n\n closeBtn.addEventListener(\"click\", close);\n\n (config.target ?? doc.body).appendChild(host);\n refs = { panel, tabs, mark, selector, caption, comment, status, pickBtn, sendBtn, highlight };\n }\n\n function ensureBuilt(): void {\n if (!host || !refs) build();\n }\n\n function openOverlay(): void {\n ensureBuilt();\n if (!host || !refs) return;\n clearAutoClose();\n endPick();\n state.phase = \"compose\";\n state.selectedElement = null;\n refs.panel.classList.remove(\"is-sent\");\n refs.comment.value = \"\";\n resetSendControl();\n setStatus(\"\", false);\n setKind(\"bug\");\n renderElement();\n host.style.display = \"block\";\n open = true;\n try {\n refs.comment.focus();\n } catch {\n /* focus is best-effort — never let it throw into the host page */\n }\n }\n\n function close(): void {\n clearAutoClose();\n endPick();\n if (host) host.style.display = \"none\";\n open = false;\n }\n\n function destroy(): void {\n clearAutoClose();\n stopPicker();\n host?.remove();\n host = null;\n refs = null;\n open = false;\n }\n\n return {\n open: openOverlay,\n close,\n destroy,\n get isOpen() {\n return open;\n },\n };\n}\n\n/** The crosshair glyph on the element-pick control (matches the `⌖` in `1d`). */\nfunction glyph(doc: Document): HTMLElement {\n return h(doc, \"span\", { class: \"fb-ov-pickbtn__glyph\", \"aria-hidden\": \"true\" }, [\"⌖\"]);\n}\n","import { type IdentityInputs, requestBoot } from \"./boot\";\nimport { ensureAnonymousId } from \"./identity\";\nimport { LAUNCH_EVENT, type Launcher, mountLauncher, unmountLauncher } from \"./launcher\";\nimport { createOverlay } from \"./overlay\";\nimport { SDK_VERSION } from \"./version\";\n\n/**\n * The hosted Fixback API origin the SDK talks to by default. A self-hosted or\n * local deployment overrides it with `apiUrl` (the dashboard's install snippet\n * pre-fills the right value for the Project).\n */\nexport const DEFAULT_API_URL = \"https://api.fixback.dev\";\n\n/** Options for {@link init}. Only `key` is required. */\nexport interface InitOptions extends IdentityInputs {\n /** The Project's **publishable** key — an identifier that ships in the page. */\n readonly key: string;\n /** The Fixback API origin. Defaults to {@link DEFAULT_API_URL}. */\n readonly apiUrl?: string;\n /** Where to mount the launcher. Defaults to `document.body`. */\n readonly target?: HTMLElement;\n}\n\n/** A running SDK instance. */\nexport interface FixbackInstance {\n /** Remove the launcher and release its DOM. Safe to call more than once. */\n destroy(): void;\n}\n\nconst NOOP_INSTANCE: FixbackInstance = { destroy() {} };\n\n/**\n * Boot the SDK and mount the launcher when — and only when — a submission would\n * be accepted for this key, origin, and Gate.\n *\n * On load the SDK calls the ingest boot endpoint; it mounts the launcher solely\n * when the answer's `canSubmit` is true, so a Reporter is never shown a launcher\n * a submission would be refused (origin off the allowlist, or the Gate turns their\n * tier away). Activating the launcher opens the report overlay (ticket #54) — wired\n * to the launcher's `fixback:launch` seam — from which a Reporter files a complete\n * report. Everything is wrapped so a Fixback problem — unreachable, refused, or an\n * unexpected error — resolves to a no-op instance and never surfaces on the host page.\n */\nexport async function init(options: InitOptions): Promise<FixbackInstance> {\n try {\n // No DOM (server-side render, worker) — nothing to mount.\n if (typeof document === \"undefined\") return NOOP_INSTANCE;\n\n const key = options?.key;\n if (typeof key !== \"string\" || key.length === 0) return NOOP_INSTANCE;\n\n const apiUrl = options.apiUrl ?? DEFAULT_API_URL;\n const anonymousId = options.anonymousId ?? ensureAnonymousId();\n\n const answer = await requestBoot(apiUrl, {\n key,\n signedIdentity: options.signedIdentity,\n reporterId: options.reporterId,\n anonymousId,\n });\n\n // Unreachable, refused, or the Gate/origin says no — show nothing.\n if (!answer?.canSubmit) return NOOP_INSTANCE;\n\n const target = options.target ?? document.body;\n const launcher: Launcher = mountLauncher(target);\n\n // The overlay the launcher opens — carrying the same key and identity, so a\n // filed report is attributed to the very Reporter boot recognised.\n const overlay = createOverlay({\n apiUrl,\n key,\n identity: {\n signedIdentity: options.signedIdentity,\n reporterId: options.reporterId,\n anonymousId,\n },\n sdkVersion: SDK_VERSION,\n target,\n });\n const onLaunch = (): void => overlay.open();\n launcher.host.addEventListener(LAUNCH_EVENT, onLaunch);\n\n return {\n destroy: () => {\n launcher.host.removeEventListener(LAUNCH_EVENT, onLaunch);\n overlay.destroy();\n unmountLauncher(launcher);\n },\n };\n } catch {\n // A Fixback failure must never break the host page.\n return NOOP_INSTANCE;\n }\n}\n"],"mappings":"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"}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type IdentityInputs } from "./boot";
|
|
2
|
+
/**
|
|
3
|
+
* The hosted Fixback API origin the SDK talks to by default. A self-hosted or
|
|
4
|
+
* local deployment overrides it with `apiUrl` (the dashboard's install snippet
|
|
5
|
+
* pre-fills the right value for the Project).
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_API_URL = "https://api.fixback.dev";
|
|
8
|
+
/** Options for {@link init}. Only `key` is required. */
|
|
9
|
+
export interface InitOptions extends IdentityInputs {
|
|
10
|
+
/** The Project's **publishable** key — an identifier that ships in the page. */
|
|
11
|
+
readonly key: string;
|
|
12
|
+
/** The Fixback API origin. Defaults to {@link DEFAULT_API_URL}. */
|
|
13
|
+
readonly apiUrl?: string;
|
|
14
|
+
/** Where to mount the launcher. Defaults to `document.body`. */
|
|
15
|
+
readonly target?: HTMLElement;
|
|
16
|
+
}
|
|
17
|
+
/** A running SDK instance. */
|
|
18
|
+
export interface FixbackInstance {
|
|
19
|
+
/** Remove the launcher and release its DOM. Safe to call more than once. */
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Boot the SDK and mount the launcher when — and only when — a submission would
|
|
24
|
+
* be accepted for this key, origin, and Gate.
|
|
25
|
+
*
|
|
26
|
+
* On load the SDK calls the ingest boot endpoint; it mounts the launcher solely
|
|
27
|
+
* when the answer's `canSubmit` is true, so a Reporter is never shown a launcher
|
|
28
|
+
* a submission would be refused (origin off the allowlist, or the Gate turns their
|
|
29
|
+
* tier away). Activating the launcher opens the report overlay (ticket #54) — wired
|
|
30
|
+
* to the launcher's `fixback:launch` seam — from which a Reporter files a complete
|
|
31
|
+
* report. Everything is wrapped so a Fixback problem — unreachable, refused, or an
|
|
32
|
+
* unexpected error — resolves to a no-op instance and never surfaces on the host page.
|
|
33
|
+
*/
|
|
34
|
+
export declare function init(options: InitOptions): Promise<FixbackInstance>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Marks the SDK's host element in the light DOM, so it is findable and unique. */
|
|
2
|
+
export declare const ROOT_ATTRIBUTE = "data-fixback-root";
|
|
3
|
+
/**
|
|
4
|
+
* Dispatched from the host element when the launcher is activated. The report
|
|
5
|
+
* overlay this opens is a later ticket (#54); for now the launcher is the mounted
|
|
6
|
+
* entry point, and this event is the seam the overlay will hang off. It is
|
|
7
|
+
* `composed` so host-page listeners outside the Shadow DOM can hear it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LAUNCH_EVENT = "fixback:launch";
|
|
10
|
+
/** A mounted launcher and the handle needed to remove it again. */
|
|
11
|
+
export interface Launcher {
|
|
12
|
+
readonly host: HTMLElement;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mount the launcher into `target` (typically `document.body`). The visible
|
|
16
|
+
* button lives inside an open Shadow DOM so its styles are fully isolated from
|
|
17
|
+
* the host page and vice-versa; the host element itself is fixed-positioned and
|
|
18
|
+
* out of flow, so mounting never shifts the host page's layout. Only one launcher
|
|
19
|
+
* can exist at a time — an earlier one is removed first.
|
|
20
|
+
*/
|
|
21
|
+
export declare function mountLauncher(target: HTMLElement): Launcher;
|
|
22
|
+
/** Remove a mounted launcher. Safe to call more than once. */
|
|
23
|
+
export declare function unmountLauncher(launcher: Launcher): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report overlay's isolated styles, built to the frozen Signal Reporter
|
|
3
|
+
* overlay (`docs/design/Fixback Visual Identity.dc.html`, badge `1d`). Like the
|
|
4
|
+
* launcher's styles these live inside the overlay's Shadow DOM, so nothing here
|
|
5
|
+
* reaches the host page and nothing on the host page reaches in. Token values are
|
|
6
|
+
* vendored Signal tokens (copied from `packages/ui/src/tokens.css`, not imported —
|
|
7
|
+
* the SDK must not depend on `@fixback/ui` at runtime). Keep them in sync by value.
|
|
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";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { IdentityInputs } from "./boot";
|
|
2
|
+
import { type ElementPicker, type ElementPickerOptions } from "./element-picker";
|
|
3
|
+
import { type Capture, type CaptureOptions } from "./screenshot";
|
|
4
|
+
import { type SubmitInput, type SubmitResult } from "./submit";
|
|
5
|
+
/** Marks the overlay's host element in the light DOM (skipped by capture/picker). */
|
|
6
|
+
export declare const OVERLAY_ATTRIBUTE = "data-fixback-overlay";
|
|
7
|
+
type CaptureViewFn = (options?: CaptureOptions) => Promise<Capture | null>;
|
|
8
|
+
type SubmitReportFn = (apiUrl: string, input: SubmitInput, fetchImpl?: typeof fetch) => Promise<SubmitResult>;
|
|
9
|
+
type StartPickerFn = (options: ElementPickerOptions) => ElementPicker;
|
|
10
|
+
/** Injectable collaborators, defaulted to the real implementations. */
|
|
11
|
+
export interface OverlayDeps {
|
|
12
|
+
readonly captureView: CaptureViewFn;
|
|
13
|
+
readonly submitReport: SubmitReportFn;
|
|
14
|
+
readonly startElementPicker: StartPickerFn;
|
|
15
|
+
}
|
|
16
|
+
/** Configuration for {@link createOverlay}. */
|
|
17
|
+
export interface OverlayConfig {
|
|
18
|
+
readonly apiUrl: string;
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly identity?: IdentityInputs;
|
|
21
|
+
readonly sdkVersion?: string;
|
|
22
|
+
/** Where to mount the overlay host. Defaults to `document.body`. */
|
|
23
|
+
readonly target?: HTMLElement;
|
|
24
|
+
readonly doc?: Document;
|
|
25
|
+
readonly win?: Window;
|
|
26
|
+
readonly deps?: Partial<OverlayDeps>;
|
|
27
|
+
}
|
|
28
|
+
/** A mounted overlay the launcher opens. */
|
|
29
|
+
export interface OverlayController {
|
|
30
|
+
open(): void;
|
|
31
|
+
close(): void;
|
|
32
|
+
destroy(): void;
|
|
33
|
+
readonly isOpen: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create the report overlay — the on-page panel a Reporter files a report from,
|
|
37
|
+
* built to the frozen Signal overlay (`1d`). It mounts lazily inside its own
|
|
38
|
+
* Shadow DOM (isolated from the host page, and marked so the screenshot and
|
|
39
|
+
* element-picker skip it), opens on the launcher's `fixback:launch` seam, and on
|
|
40
|
+
* Send captures a masked screenshot, assembles the Annotation, and submits to
|
|
41
|
+
* ingest — showing a confirmation on success and failing quietly otherwise.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createOverlay(config: OverlayConfig): OverlayController;
|
|
44
|
+
export {};
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ingest **feedback** content wire-contract, vendored, plus the helpers that
|
|
3
|
+
* assemble it from what a Reporter composed in the overlay.
|
|
4
|
+
*
|
|
5
|
+
* Like `boot.ts`, the SDK does not import `@fixback/shared`; the slice of the
|
|
6
|
+
* submission shape it needs is copied here. Keep it in lock-step with the server:
|
|
7
|
+
* the JSON `payload` part accepted by `POST /api/ingest/feedback`
|
|
8
|
+
* (`apps/api/src/ingest/ingest.controller.ts` — `feedbackContentBody`) and the
|
|
9
|
+
* `SubmissionContent` it parses (`apps/api/src/ingest/reporter-identity.ts`).
|
|
10
|
+
*/
|
|
11
|
+
/** The Kind a Reporter tags a report with. Mirrors the server's `ISSUE_KINDS`. */
|
|
12
|
+
export type IssueKind = "bug" | "improvement" | "idea";
|
|
13
|
+
/** The picked element's viewport rectangle, as the server's annotation `rect`. */
|
|
14
|
+
export interface ElementRect {
|
|
15
|
+
readonly x: number;
|
|
16
|
+
readonly y: number;
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The Annotation — the element a Reporter pointed at: a stable CSS selector, a
|
|
22
|
+
* readable DOM path, the tag, and the bounding rect. Exactly the server's
|
|
23
|
+
* `annotation` object shape.
|
|
24
|
+
*/
|
|
25
|
+
export interface SelectedElement {
|
|
26
|
+
readonly selector: string;
|
|
27
|
+
readonly domPath: string;
|
|
28
|
+
readonly tag: string;
|
|
29
|
+
readonly rect: ElementRect;
|
|
30
|
+
}
|
|
31
|
+
/** The capture environment recorded alongside a report. */
|
|
32
|
+
export interface CaptureEnvironment {
|
|
33
|
+
readonly viewportWidth?: number;
|
|
34
|
+
readonly viewportHeight?: number;
|
|
35
|
+
readonly browser?: string;
|
|
36
|
+
readonly sdkVersion?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The JSON content of a feedback submission — the object serialised into the
|
|
40
|
+
* multipart `payload` part next to the `key` and identity evidence. Every field
|
|
41
|
+
* is optional: none of it feeds the server's trust decision, so a submission may
|
|
42
|
+
* carry any subset. `annotation` is the selected element; the screenshot is a
|
|
43
|
+
* separate binary part, never part of this JSON.
|
|
44
|
+
*/
|
|
45
|
+
export interface ReportContent {
|
|
46
|
+
readonly comment?: string;
|
|
47
|
+
readonly kind?: IssueKind;
|
|
48
|
+
readonly url?: string;
|
|
49
|
+
readonly environment?: CaptureEnvironment;
|
|
50
|
+
readonly annotation?: SelectedElement;
|
|
51
|
+
}
|
|
52
|
+
/** What the overlay hands to {@link assembleContent} when the Reporter sends. */
|
|
53
|
+
export interface ReportDraft {
|
|
54
|
+
readonly kind?: IssueKind;
|
|
55
|
+
readonly comment?: string;
|
|
56
|
+
readonly element?: SelectedElement;
|
|
57
|
+
readonly url?: string;
|
|
58
|
+
readonly environment?: CaptureEnvironment;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the capture environment off a window: the viewport size, the browser's
|
|
62
|
+
* user-agent, and the SDK version. A dimension is recorded only when it is a
|
|
63
|
+
* positive number (a headless/zero viewport records nothing rather than an
|
|
64
|
+
* invalid `0`, which the server would reject).
|
|
65
|
+
*/
|
|
66
|
+
export declare function collectEnvironment(win: Window, sdkVersion: string): CaptureEnvironment;
|
|
67
|
+
/**
|
|
68
|
+
* Assemble the ingest content from what the Reporter composed. Empty pieces are
|
|
69
|
+
* dropped rather than sent as blanks: a whitespace-only comment, an absent
|
|
70
|
+
* element, or an empty environment are simply omitted, so the payload carries
|
|
71
|
+
* only what was actually provided (mirroring the server's all-optional content).
|
|
72
|
+
*/
|
|
73
|
+
export declare function assembleContent(draft: ReportDraft): ReportContent;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side masked screenshot capture (spec MVP §E; ticket #54).
|
|
3
|
+
*
|
|
4
|
+
* The capture is **private-by-default**: input values are masked and the SDK's
|
|
5
|
+
* own UI is removed from a *clone* of the view **before** anything is rasterised,
|
|
6
|
+
* so no unmasked text and none of Fixback's chrome ever reaches the image. The
|
|
7
|
+
* approach is dependency-free — the cloned, masked DOM is serialised into an SVG
|
|
8
|
+
* `<foreignObject>` and drawn onto a `<canvas>` — honouring the SDK's "no runtime
|
|
9
|
+
* dependencies / no host-page disturbance" constraints. The raster step is
|
|
10
|
+
* injectable so the pipeline (and the masking-before-capture guarantee) is
|
|
11
|
+
* testable without a real canvas, and it fails quietly: any problem resolves to
|
|
12
|
+
* `null` and the report is simply sent without a screenshot.
|
|
13
|
+
*/
|
|
14
|
+
/** The character private input content is replaced with. */
|
|
15
|
+
export declare const MASK_CHAR = "\u2022";
|
|
16
|
+
/**
|
|
17
|
+
* Mask the private, user-entered content in a subtree: text `input` values,
|
|
18
|
+
* `textarea` content, and `contenteditable` text. Developer-authored text
|
|
19
|
+
* (placeholders, button labels, non-text controls) is left untouched. Operates in
|
|
20
|
+
* place — call it on a *clone* of the view, never the live page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function maskInputs(root: ParentNode): void;
|
|
23
|
+
/** A captured screenshot: the image bytes plus its dimensions and content type. */
|
|
24
|
+
export interface Capture {
|
|
25
|
+
readonly blob: Blob;
|
|
26
|
+
readonly width: number;
|
|
27
|
+
readonly height: number;
|
|
28
|
+
readonly type: string;
|
|
29
|
+
}
|
|
30
|
+
/** Turns a serialised SVG of the view into image bytes (injectable for tests). */
|
|
31
|
+
export type Rasterize = (svg: string, meta: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
type: string;
|
|
35
|
+
}) => Promise<Blob | null>;
|
|
36
|
+
/** Options for {@link captureView}. */
|
|
37
|
+
export interface CaptureOptions {
|
|
38
|
+
/** The element to capture. Defaults to the document element (the full view). */
|
|
39
|
+
readonly target?: Element;
|
|
40
|
+
readonly doc?: Document;
|
|
41
|
+
readonly win?: Window;
|
|
42
|
+
/** Output content type. Defaults to `image/png`. */
|
|
43
|
+
readonly type?: string;
|
|
44
|
+
/** Override the raster step (the default draws via an SVG + `<canvas>`). */
|
|
45
|
+
readonly rasterize?: Rasterize;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Capture the current view as a masked screenshot. Clones the target, removes the
|
|
49
|
+
* SDK's own UI, masks input values — **all before** serialising and rasterising —
|
|
50
|
+
* then returns the image bytes, or `null` if capture wasn't possible.
|
|
51
|
+
*/
|
|
52
|
+
export declare function captureView(options?: CaptureOptions): Promise<Capture | null>;
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launcher's isolated styles.
|
|
3
|
+
*
|
|
4
|
+
* These live inside the launcher's Shadow DOM (see `launcher.ts`), so nothing
|
|
5
|
+
* here can reach the host page and nothing on the host page can reach in. The
|
|
6
|
+
* token values are **vendored** Signal design tokens — copied from
|
|
7
|
+
* `packages/ui/src/tokens.css` rather than imported, because the SDK must not
|
|
8
|
+
* depend on `@fixback/ui` at runtime (ticket #47). Keep them in sync by value.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LAUNCHER_STYLES = "\n:host {\n /* Vendored Signal tokens (packages/ui/src/tokens.css). */\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-text: #0f1720;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\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.4;\n -webkit-font-smoothing: antialiased;\n}\n\n.fb-launcher {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n height: 40px;\n margin: 0;\n padding: 0 16px;\n border: 0;\n border-radius: 999px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n cursor: pointer;\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 1px 2px rgba(15, 23, 32, 0.12);\n transition:\n background-color 120ms ease,\n transform 120ms ease;\n}\n\n.fb-launcher:hover {\n background: var(--fb-color-accent-hover);\n}\n\n.fb-launcher:active {\n transform: translateY(1px);\n}\n\n.fb-launcher:focus-visible {\n outline: 2px solid var(--fb-color-accent);\n outline-offset: 2px;\n}\n\n.fb-launcher__icon {\n display: block;\n flex: none;\n width: 16px;\n height: 16px;\n}\n\n.fb-launcher__label {\n white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fb-launcher {\n transition: none;\n }\n}\n";
|
package/dist/submit.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IdentityInputs, ReporterTier } from "./boot";
|
|
2
|
+
import type { ReportContent } from "./report";
|
|
3
|
+
/** What ingest returns for an accepted submission (vendored server shape). */
|
|
4
|
+
export interface RecordedFeedback {
|
|
5
|
+
readonly feedbackId: string;
|
|
6
|
+
readonly issueId: string;
|
|
7
|
+
readonly reporterId: string;
|
|
8
|
+
readonly tier: ReporterTier;
|
|
9
|
+
}
|
|
10
|
+
/** The captured screenshot to attach, if any. */
|
|
11
|
+
export interface SubmitScreenshot {
|
|
12
|
+
readonly blob: Blob;
|
|
13
|
+
readonly filename?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Everything a single feedback submission carries. */
|
|
16
|
+
export interface SubmitInput {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly identity?: IdentityInputs;
|
|
19
|
+
readonly content: ReportContent;
|
|
20
|
+
readonly screenshot?: SubmitScreenshot | null;
|
|
21
|
+
}
|
|
22
|
+
/** The outcome of a submission — never an exception. */
|
|
23
|
+
export type SubmitResult = {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly feedback: RecordedFeedback | null;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly reason: "unreachable" | "refused";
|
|
29
|
+
readonly status?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Join an API base URL with the feedback path, tolerating a trailing slash. */
|
|
32
|
+
export declare function feedbackEndpoint(apiUrl: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Submit a report to ingest. Assembles the `payload` JSON (key + identity +
|
|
35
|
+
* content) and the optional `screenshot` file into a `FormData`, posts it, and
|
|
36
|
+
* resolves to the recorded Feedback on success or a named failure otherwise. The
|
|
37
|
+
* `fetchImpl` seam exists purely so the call is testable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function submitReport(apiUrl: string, input: SubmitInput, fetchImpl?: typeof fetch): Promise<SubmitResult>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's own version string, reported to ingest as `environment.sdkVersion`
|
|
3
|
+
* (spec MVP §B/§E). Kept as a hand-maintained constant rather than imported from
|
|
4
|
+
* `package.json`, so the bundle stays a single self-contained file with no JSON
|
|
5
|
+
* import — keep it in step with `package.json` and the Changesets bump.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SDK_VERSION = "0.1.0";
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fixback/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Fixback capture SDK — a boot-gated, self-isolating on-page feedback launcher.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/fixback.umd.js",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"unpkg": "./dist/fixback.umd.js",
|
|
17
|
+
"jsdelivr": "./dist/fixback.umd.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.mjs",
|
|
22
|
+
"require": "./dist/fixback.umd.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"fixback",
|
|
28
|
+
"feedback",
|
|
29
|
+
"bug-report",
|
|
30
|
+
"widget",
|
|
31
|
+
"sdk"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/wemuda/fixback.git",
|
|
36
|
+
"directory": "packages/sdk"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/wemuda/fixback/tree/master/packages/sdk#readme",
|
|
39
|
+
"bugs": "https://github.com/wemuda/fixback/issues",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"jsdom": "^30.0.1",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"vite": "^8.2.1",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run"
|
|
54
|
+
}
|
|
55
|
+
}
|