feedbackkit-web 1.0.37 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ import type { FeedbackKitConfiguration, FeedbackReport, FeedbackUser, FixUpdate } from "./types";
2
+ /**
3
+ * The reporter's half of the closed loop — web counterpart of the Swift
4
+ * SDK's `FixUpdatesClient` / `FeedbackReporterIdentity`. Talks to the
5
+ * `reporter-updates` Edge Function (supabase/functions/reporter-updates);
6
+ * keep the wire format in sync with it and with
7
+ * Sources/FeedbackKit/Networking/FixUpdatesClient.swift.
8
+ */
9
+ /**
10
+ * Mirrors `compare_builds` (0014_closed_loop.sql) and every other copy:
11
+ * dotted numeric builds compare numerically; anything else is only equal (0)
12
+ * or incomparable (null).
13
+ */
14
+ export declare function compareBuilds(a: string | null | undefined, b: string | null | undefined): -1 | 0 | 1 | null;
15
+ /**
16
+ * The anonymous, random id attached to reports from this browser — what lets
17
+ * a fix get back to the person who reported the bug. Kept in localStorage
18
+ * (per origin); where storage is unavailable (private mode, blocked site
19
+ * data) it lasts for the page's lifetime instead, which just means that
20
+ * browser won't be asked to verify fixes later.
21
+ */
22
+ export declare function reporterId(): string;
23
+ export declare function setUser(user: FeedbackUser | null): void;
24
+ export declare function getUser(): FeedbackUser | null;
25
+ /** `…/functions/v1/ingest-feedback` → `…/functions/v1/reporter-updates`, unless overridden. */
26
+ export declare function reporterUpdatesEndpoint(configuration: FeedbackKitConfiguration): string;
27
+ interface WireUpdate {
28
+ feedback_id: string;
29
+ text?: string;
30
+ created_at?: string;
31
+ screen_name?: string | null;
32
+ fix_stage?: string | null;
33
+ fixed_in_build?: string | null;
34
+ fix_summary?: string | null;
35
+ needs_verification?: boolean;
36
+ open_question?: {
37
+ id: string;
38
+ body: string;
39
+ created_at: string;
40
+ } | null;
41
+ screenshot_url?: string | null;
42
+ messages?: {
43
+ id: string;
44
+ kind: string;
45
+ body: string;
46
+ author: string;
47
+ created_at: string;
48
+ }[];
49
+ }
50
+ export declare function decodeUpdate(w: WireUpdate): FixUpdate;
51
+ export declare function fetchFixUpdates(configuration: FeedbackKitConfiguration): Promise<FixUpdate[]>;
52
+ export type FixAction = {
53
+ type: "verify";
54
+ }
55
+ /** Still broken — optionally with a fresh report (text + annotated screenshot). */
56
+ | {
57
+ type: "reopen";
58
+ report?: FeedbackReport | null;
59
+ text?: string;
60
+ } | {
61
+ type: "reply";
62
+ text: string;
63
+ };
64
+ export declare function encodeFixAction(configuration: FeedbackKitConfiguration, feedbackId: string, action: FixAction): Promise<Record<string, unknown>>;
65
+ export declare function sendFixAction(configuration: FeedbackKitConfiguration, feedbackId: string, action: FixAction): Promise<void>;
66
+ export {};
@@ -18,6 +18,13 @@ declare const _default: {
18
18
  disableKeyboardShortcut(): void;
19
19
  enableLogCapture(options?: import("./logs").LogCaptureOptions | false): void;
20
20
  captureScreenshot(options?: import("./types").CaptureOptions): Promise<Blob>;
21
+ setUser(user: import("./types").FeedbackUser | null): void;
22
+ readonly reporterId: string;
23
+ checkForFixUpdates(): Promise<import("./types").FixUpdate[]>;
24
+ respondToFixUpdate(feedbackId: string, action: import("./fixes").FixAction): Promise<void>;
25
+ enableFixVerification(): void;
26
+ disableFixVerification(): void;
27
+ presentFixUpdatesIfNeeded(): Promise<void>;
21
28
  destroy(): void;
22
29
  } & {
23
30
  renderer: typeof renderer;
@@ -1,11 +1,13 @@
1
1
  import { type LogCaptureOptions } from "./logs";
2
- import type { CaptureOptions, FeedbackKitConfiguration, FeedbackProduct, FeedbackReport, FeedbackTheme } from "./types";
2
+ import { type FixAction } from "./fixes";
3
+ import type { CaptureOptions, FeedbackKitConfiguration, FeedbackProduct, FeedbackReport, FeedbackTheme, FeedbackUser, FixUpdate } from "./types";
3
4
  import { type TriggerOptions } from "./ui/widget";
4
5
  export type * from "./types";
5
6
  export type { TriggerOptions } from "./ui/widget";
6
7
  export type { LogCaptureOptions } from "./logs";
7
8
  export { FeedbackSubmissionError, DEFAULT_ENDPOINT, encodePayload, type IngestPayload } from "./submit";
8
9
  export { parseUserAgent, redactUrl } from "./environment";
10
+ export { compareBuilds, reporterUpdatesEndpoint, type FixAction } from "./fixes";
9
11
  export { drawAnnotation, drawAnnotations, hitTest, readPoint, textBubbleRect, STROKE_WIDTH, type Point, type Size, } from "./renderer";
10
12
  export interface PresentOptions {
11
13
  /** Called with the finished report when the user taps Send. */
@@ -84,6 +86,41 @@ export declare const FeedbackKit: {
84
86
  enableLogCapture(options?: LogCaptureOptions | false): void;
85
87
  /** Captures the current viewport as a PNG, without opening any UI. */
86
88
  captureScreenshot(options?: CaptureOptions): Promise<Blob>;
89
+ /**
90
+ * Optional identity of the person using your site, attached to every
91
+ * report they submit. Not needed for fix verification (that uses an
92
+ * anonymous per-browser id, `reporterId`). Pass `null` on sign-out.
93
+ */
94
+ setUser(user: FeedbackUser | null): void;
95
+ /** The anonymous per-browser id attached to reports — how a fix finds its way back here. */
96
+ readonly reporterId: string;
97
+ /**
98
+ * This browser's reports that need the reporter's attention: shipped
99
+ * fixes to confirm and developer/agent questions. For a custom UI — the
100
+ * built-in one is `enableFixVerification`.
101
+ */
102
+ checkForFixUpdates(): Promise<FixUpdate[]>;
103
+ /** Answers one update directly (custom UIs): verify, reopen, or reply. */
104
+ respondToFixUpdate(feedbackId: string, action: FixAction): Promise<void>;
105
+ /**
106
+ * Closes the loop with the person who reported a bug: once a fix for one
107
+ * of their reports ships (announced with `feedbackkit release`), a small
108
+ * card shows their original screenshot and asks "is it fixed?". "Still
109
+ * broken" reopens the capture dialog so they can show what's wrong now,
110
+ * and the report goes straight back to the developer. Also surfaces
111
+ * questions the developer or their coding agent asked.
112
+ *
113
+ * Checks now and whenever the tab becomes visible (at most once a minute).
114
+ * With `appBuild` configured, only fixes shipped in that build or earlier
115
+ * are shown; without it, a fix counts as live as soon as it's released.
116
+ */
117
+ enableFixVerification(): void;
118
+ disableFixVerification(): void;
119
+ /**
120
+ * Checks right now (ignoring the throttle) and shows the card if there's
121
+ * something. Resolves once the check is done and any card is on screen.
122
+ */
123
+ presentFixUpdatesIfNeeded(): Promise<void>;
87
124
  /** Removes every trace of the SDK from the page (trigger, dialog host, listeners, log hooks). */
88
125
  destroy(): void;
89
126
  };
@@ -1,4 +1,4 @@
1
- import type { FeedbackKitConfiguration, FeedbackProduct, FeedbackReport } from "./types";
1
+ import type { FeedbackKitConfiguration, FeedbackProduct, FeedbackReport, FeedbackUser } from "./types";
2
2
  /** The hosted FeedbackKit dashboard's ingestion endpoint. */
3
3
  export declare const DEFAULT_ENDPOINT = "https://gpucoladcyvijefdjudf.supabase.co/functions/v1/ingest-feedback";
4
4
  export declare class FeedbackSubmissionError extends Error {
@@ -34,9 +34,19 @@ export interface IngestPayload {
34
34
  is_default: boolean;
35
35
  }[];
36
36
  logs?: FeedbackReport["logs"];
37
+ /** Anonymous per-browser id — see fixes.ts `reporterId`. */
38
+ reporter_id?: string;
39
+ /** `FeedbackKit.setUser(...)`, camelCase JSONB like `environment`. */
40
+ reporter?: FeedbackUser;
37
41
  }
38
42
  export declare function blobToBase64(blob: Blob): Promise<string>;
39
- export declare function encodePayload(report: FeedbackReport, projectKey: string): Promise<IngestPayload>;
40
- export declare function submitReport(report: FeedbackReport, configuration: FeedbackKitConfiguration): Promise<void>;
43
+ export declare function encodePayload(report: FeedbackReport, projectKey: string, identity?: {
44
+ reporterId?: string;
45
+ user?: FeedbackUser | null;
46
+ }): Promise<IngestPayload>;
47
+ export declare function submitReport(report: FeedbackReport, configuration: FeedbackKitConfiguration, identity?: {
48
+ reporterId?: string;
49
+ user?: FeedbackUser | null;
50
+ }): Promise<void>;
41
51
  /** `GET <endpoint>?project_key=…` — the products configured for the project in the dashboard. */
42
52
  export declare function fetchProducts(configuration: FeedbackKitConfiguration): Promise<FeedbackProduct[]>;
@@ -135,6 +135,49 @@ export interface FeedbackKitConfiguration {
135
135
  * report). Default `true`; pass options to tune, or `false` to disable.
136
136
  */
137
137
  captureLogs?: boolean | LogCaptureOptions;
138
+ /**
139
+ * Where fix updates for this browser's reports come from (the
140
+ * `reporter-updates` function). Defaults to the sibling of `endpoint`.
141
+ */
142
+ reporterUpdatesEndpoint?: string;
143
+ }
144
+ /** Optional identity of the person using your site, attached to their reports. Mirrors Swift's `FeedbackUser`. */
145
+ export interface FeedbackUser {
146
+ id?: string;
147
+ email?: string;
148
+ name?: string;
149
+ }
150
+ /**
151
+ * Something that happened to a report this browser filed: a fix that
152
+ * shipped ("is it fixed?") and/or a question from the developer or their
153
+ * coding agent. Mirrors Swift's `FixUpdate`.
154
+ */
155
+ export interface FixUpdate {
156
+ feedbackId: string;
157
+ /** What the reporter originally wrote. */
158
+ text: string;
159
+ createdAt: string;
160
+ screenName: string | null;
161
+ fixStage: string | null;
162
+ fixedInBuild: string | null;
163
+ /** One plain-language line on what was fixed, if the developer/agent wrote one. */
164
+ fixSummary: string | null;
165
+ /** A fix shipped (in a build this page is at least, when `appBuild` is configured) and hasn't been confirmed. */
166
+ needsVerification: boolean;
167
+ openQuestion: {
168
+ id: string;
169
+ body: string;
170
+ createdAt: string;
171
+ } | null;
172
+ /** Short-lived signed URL of the original annotated screenshot. */
173
+ screenshotUrl: string | null;
174
+ messages: {
175
+ id: string;
176
+ kind: string;
177
+ body: string;
178
+ author: string;
179
+ createdAt: string;
180
+ }[];
138
181
  }
139
182
  export interface CaptureOptions {
140
183
  /**
@@ -0,0 +1,32 @@
1
+ import type { FixUpdate } from "../types";
2
+ export type FixCardOutcome = {
3
+ type: "verified";
4
+ }
5
+ /** The widget then opens the capture flow in "still broken" mode. */
6
+ | {
7
+ type: "stillBroken";
8
+ } | {
9
+ type: "replied";
10
+ text: string;
11
+ } | {
12
+ type: "later";
13
+ };
14
+ /**
15
+ * "We fixed something you reported — is it fixed?" A small non-modal card in
16
+ * the page's bottom corner (above the trigger), inside the widget's Shadow
17
+ * DOM. Web counterpart of Swift's `FixVerificationView`; same copy on purpose.
18
+ */
19
+ export declare class FixCard {
20
+ private readonly shadow;
21
+ private readonly update;
22
+ private readonly onOutcome;
23
+ private root;
24
+ private settled;
25
+ constructor(shadow: ShadowRoot, update: FixUpdate, onOutcome: (outcome: FixCardOutcome) => void);
26
+ show(position: "bottom-right" | "bottom-left"): void;
27
+ /** Close as "later" (e.g. `FeedbackKit.disableFixVerification()`). */
28
+ dismiss(): void;
29
+ /** Remove without reporting an outcome (the widget is being destroyed). */
30
+ remove(): void;
31
+ private settle;
32
+ }
@@ -3,4 +3,4 @@
3
3
  * styles nor ours leak across. `:host { all: initial }` also blocks inherited
4
4
  * properties (font, color, line-height) from the page.
5
5
  */
6
- export declare const styles = "\n:host {\n all: initial;\n --fk-primary: #007aff;\n --fk-secondary: #6b7280;\n --fk-bg: #ffffff;\n --fk-surface: #f5f5f7;\n --fk-border: rgba(0, 0, 0, 0.1);\n --fk-text: #111114;\n --fk-muted: #6b6b73;\n --fk-danger: #d92d20;\n --fk-backdrop: rgba(10, 10, 14, 0.55);\n --fk-shadow: 0 24px 64px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n font-size: 14px;\n line-height: 1.4;\n color: var(--fk-text);\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([data-color-scheme=\"light\"])) {\n --fk-bg: #1c1c1f;\n --fk-surface: #26262a;\n --fk-border: rgba(255, 255, 255, 0.12);\n --fk-text: #f2f2f5;\n --fk-muted: #a0a0a8;\n --fk-backdrop: rgba(0, 0, 0, 0.65);\n }\n}\n:host([data-color-scheme=\"dark\"]) {\n --fk-bg: #1c1c1f;\n --fk-surface: #26262a;\n --fk-border: rgba(255, 255, 255, 0.12);\n --fk-text: #f2f2f5;\n --fk-muted: #a0a0a8;\n --fk-backdrop: rgba(0, 0, 0, 0.65);\n}\n*, *::before, *::after { box-sizing: border-box; }\nbutton { font: inherit; color: inherit; cursor: pointer; }\nbutton:disabled { cursor: not-allowed; opacity: 0.45; }\n:focus-visible { outline: 2px solid var(--fk-primary); outline-offset: 2px; }\n\n/* ---------- floating trigger ---------- */\n.fk-trigger {\n position: fixed;\n z-index: 2147483000;\n bottom: 20px;\n right: 20px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 44px;\n padding: 0 16px 0 14px;\n border: 0;\n border-radius: 22px;\n background: var(--fk-primary);\n color: #fff;\n font-weight: 600;\n font-size: 14px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22);\n transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;\n}\n.fk-trigger[data-position=\"bottom-left\"] { right: auto; left: 20px; }\n.fk-trigger:hover { transform: translateY(-1px); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.26); }\n.fk-trigger[data-compact=\"true\"] { width: 48px; height: 48px; padding: 0; justify-content: center; border-radius: 24px; }\n.fk-trigger[data-compact=\"true\"] .fk-trigger-label { display: none; }\n.fk-trigger[aria-busy=\"true\"] { opacity: 0.6; pointer-events: none; }\n\n/* ---------- overlay ---------- */\n.fk-overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483001;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--fk-backdrop);\n animation: fk-fade 0.16s ease-out;\n}\n@keyframes fk-fade { from { opacity: 0; } to { opacity: 1; } }\n@keyframes fk-rise { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: none; } }\n.fk-dialog {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 340px;\n width: min(1180px, 100%);\n height: min(780px, 100%);\n overflow: hidden;\n border-radius: 16px;\n background: var(--fk-bg);\n box-shadow: var(--fk-shadow);\n animation: fk-rise 0.2s ease-out;\n}\n.fk-stage {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n background: var(--fk-surface);\n border-right: 1px solid var(--fk-border);\n}\n.fk-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-bottom: 1px solid var(--fk-border);\n}\n.fk-toolbar-sep { width: 1px; height: 22px; margin: 0 6px; background: var(--fk-border); }\n.fk-tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fk-muted);\n}\n.fk-tool:hover:not(:disabled) { background: var(--fk-border); color: var(--fk-text); }\n.fk-tool[aria-pressed=\"true\"] { background: var(--fk-primary); color: #fff; }\n.fk-swatch {\n width: 22px;\n height: 22px;\n margin: 0 2px;\n padding: 0;\n border-radius: 11px;\n border: 2px solid var(--fk-bg);\n box-shadow: 0 0 0 1px var(--fk-border);\n}\n.fk-swatch[aria-pressed=\"true\"] { box-shadow: 0 0 0 2px var(--fk-text); }\n.fk-hint { margin-left: auto; font-size: 12px; color: var(--fk-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.fk-canvas-wrap {\n position: relative;\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: hidden;\n}\n.fk-canvas-box { position: relative; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18); border-radius: 6px; overflow: hidden; }\n.fk-canvas-box canvas { display: block; }\n.fk-canvas-box .fk-ink { position: absolute; inset: 0; touch-action: none; }\n.fk-stage[data-screenshot=\"off\"] .fk-canvas-box { opacity: 0.25; filter: grayscale(1); }\n.fk-stage[data-screenshot=\"off\"] .fk-toolbar button { pointer-events: none; opacity: 0.35; }\n.fk-off-note {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n font-weight: 600;\n color: var(--fk-muted);\n pointer-events: none;\n}\n.fk-stage[data-screenshot=\"off\"] .fk-off-note { display: flex; }\n.fk-text-input {\n position: absolute;\n z-index: 2;\n min-width: 120px;\n padding: 6px 8px;\n border: 2px solid var(--fk-primary);\n border-radius: 8px;\n background: var(--fk-bg);\n color: var(--fk-text);\n font: bold 14px -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n outline: none;\n}\n\n/* ---------- composer ---------- */\n.fk-composer { display: flex; flex-direction: column; min-height: 0; padding: 18px; gap: 14px; overflow-y: auto; }\n.fk-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.fk-title { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -0.01em; }\n.fk-close { display: inline-flex; width: 30px; height: 30px; align-items: center; justify-content: center; border: 0; border-radius: 15px; background: var(--fk-surface); color: var(--fk-muted); }\n.fk-close:hover { color: var(--fk-text); }\n.fk-label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--fk-muted); text-transform: uppercase; letter-spacing: 0.04em; }\n.fk-textarea {\n width: 100%;\n min-height: 140px;\n resize: vertical;\n padding: 10px 12px;\n border: 1px solid var(--fk-border);\n border-radius: 10px;\n background: var(--fk-bg);\n color: var(--fk-text);\n font: inherit;\n line-height: 1.45;\n}\n.fk-textarea:focus { outline: none; border-color: var(--fk-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--fk-primary) 22%, transparent); }\n.fk-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.fk-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n height: 28px;\n padding: 0 11px;\n border: 1px solid var(--fk-border);\n border-radius: 14px;\n background: var(--fk-bg);\n font-size: 13px;\n}\n.fk-chip[aria-pressed=\"true\"] { border-color: var(--fk-primary); background: color-mix(in srgb, var(--fk-primary) 12%, transparent); color: var(--fk-primary); font-weight: 600; }\n.fk-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }\n.fk-switch { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; user-select: none; }\n.fk-switch input { position: absolute; opacity: 0; width: 0; height: 0; }\n.fk-switch-track { position: relative; width: 36px; height: 22px; border-radius: 11px; background: var(--fk-border); transition: background 0.15s; }\n.fk-switch-track::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 9px; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.15s; }\n.fk-switch input:checked + .fk-switch-track { background: var(--fk-primary); }\n.fk-switch input:checked + .fk-switch-track::after { transform: translateX(14px); }\n.fk-switch input:focus-visible + .fk-switch-track { outline: 2px solid var(--fk-primary); outline-offset: 2px; }\n.fk-attach {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 12px;\n border: 1px dashed var(--fk-secondary);\n border-radius: 8px;\n background: transparent;\n color: var(--fk-secondary);\n font-size: 13px;\n}\n.fk-file { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 10px; border-radius: 8px; background: var(--fk-surface); font-size: 12px; }\n.fk-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.fk-file button { display: inline-flex; border: 0; background: transparent; color: var(--fk-muted); padding: 2px; }\n.fk-meta { font-size: 12px; color: var(--fk-muted); }\n.fk-meta details summary { cursor: pointer; }\n.fk-meta pre { max-height: 140px; overflow: auto; margin: 6px 0 0; padding: 8px; border-radius: 8px; background: var(--fk-surface); font-size: 11px; white-space: pre-wrap; word-break: break-word; }\n.fk-spacer { flex: 1; }\n.fk-actions { display: flex; gap: 8px; }\n.fk-btn {\n display: inline-flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n gap: 7px;\n height: 40px;\n border-radius: 10px;\n font-weight: 600;\n}\n.fk-btn-secondary { border: 1px solid var(--fk-border); background: transparent; color: var(--fk-secondary); }\n.fk-btn-primary { border: 0; background: var(--fk-primary); color: #fff; }\n.fk-btn-primary:hover:not(:disabled) { filter: brightness(1.06); }\n.fk-error { margin: 0; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--fk-danger) 12%, transparent); color: var(--fk-danger); font-size: 13px; }\n.fk-spinner { width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.45); border-top-color: #fff; border-radius: 50%; animation: fk-spin 0.7s linear infinite; }\n@keyframes fk-spin { to { transform: rotate(360deg); } }\n.fk-done { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; flex: 1; text-align: center; }\n.fk-done-icon { display: inline-flex; width: 48px; height: 48px; align-items: center; justify-content: center; border-radius: 24px; background: var(--fk-primary); color: #fff; }\n.fk-done-icon svg { width: 26px; height: 26px; }\n.fk-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n\n/* ---------- narrow screens: stack stage over composer ---------- */\n@media (max-width: 760px) {\n .fk-overlay { padding: 0; }\n .fk-dialog { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; width: 100%; height: 100%; border-radius: 0; }\n .fk-stage { border-right: 0; border-bottom: 1px solid var(--fk-border); }\n .fk-hint { display: none; }\n .fk-canvas-wrap { padding: 10px; }\n .fk-composer { max-height: 55vh; padding: 14px; gap: 10px; }\n .fk-textarea { min-height: 84px; }\n}\n@media (prefers-reduced-motion: reduce) {\n .fk-overlay, .fk-dialog { animation: none; }\n}\n";
6
+ export declare const styles = "\n:host {\n all: initial;\n --fk-primary: #007aff;\n --fk-secondary: #6b7280;\n --fk-bg: #ffffff;\n --fk-surface: #f5f5f7;\n --fk-border: rgba(0, 0, 0, 0.1);\n --fk-text: #111114;\n --fk-muted: #6b6b73;\n --fk-danger: #d92d20;\n --fk-backdrop: rgba(10, 10, 14, 0.55);\n --fk-shadow: 0 24px 64px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n font-size: 14px;\n line-height: 1.4;\n color: var(--fk-text);\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([data-color-scheme=\"light\"])) {\n --fk-bg: #1c1c1f;\n --fk-surface: #26262a;\n --fk-border: rgba(255, 255, 255, 0.12);\n --fk-text: #f2f2f5;\n --fk-muted: #a0a0a8;\n --fk-backdrop: rgba(0, 0, 0, 0.65);\n }\n}\n:host([data-color-scheme=\"dark\"]) {\n --fk-bg: #1c1c1f;\n --fk-surface: #26262a;\n --fk-border: rgba(255, 255, 255, 0.12);\n --fk-text: #f2f2f5;\n --fk-muted: #a0a0a8;\n --fk-backdrop: rgba(0, 0, 0, 0.65);\n}\n*, *::before, *::after { box-sizing: border-box; }\nbutton { font: inherit; color: inherit; cursor: pointer; }\nbutton:disabled { cursor: not-allowed; opacity: 0.45; }\n:focus-visible { outline: 2px solid var(--fk-primary); outline-offset: 2px; }\n\n/* ---------- floating trigger ---------- */\n.fk-trigger {\n position: fixed;\n z-index: 2147483000;\n bottom: 20px;\n right: 20px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 44px;\n padding: 0 16px 0 14px;\n border: 0;\n border-radius: 22px;\n background: var(--fk-primary);\n color: #fff;\n font-weight: 600;\n font-size: 14px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22);\n transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;\n}\n.fk-trigger[data-position=\"bottom-left\"] { right: auto; left: 20px; }\n.fk-trigger:hover { transform: translateY(-1px); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.26); }\n.fk-trigger[data-compact=\"true\"] { width: 48px; height: 48px; padding: 0; justify-content: center; border-radius: 24px; }\n.fk-trigger[data-compact=\"true\"] .fk-trigger-label { display: none; }\n.fk-trigger[aria-busy=\"true\"] { opacity: 0.6; pointer-events: none; }\n\n/* ---------- overlay ---------- */\n.fk-overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483001;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--fk-backdrop);\n animation: fk-fade 0.16s ease-out;\n}\n@keyframes fk-fade { from { opacity: 0; } to { opacity: 1; } }\n@keyframes fk-rise { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: none; } }\n.fk-dialog {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 340px;\n width: min(1180px, 100%);\n height: min(780px, 100%);\n overflow: hidden;\n border-radius: 16px;\n background: var(--fk-bg);\n box-shadow: var(--fk-shadow);\n animation: fk-rise 0.2s ease-out;\n}\n.fk-stage {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n background: var(--fk-surface);\n border-right: 1px solid var(--fk-border);\n}\n.fk-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-bottom: 1px solid var(--fk-border);\n}\n.fk-toolbar-sep { width: 1px; height: 22px; margin: 0 6px; background: var(--fk-border); }\n.fk-tool {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fk-muted);\n}\n.fk-tool:hover:not(:disabled) { background: var(--fk-border); color: var(--fk-text); }\n.fk-tool[aria-pressed=\"true\"] { background: var(--fk-primary); color: #fff; }\n.fk-swatch {\n width: 22px;\n height: 22px;\n margin: 0 2px;\n padding: 0;\n border-radius: 11px;\n border: 2px solid var(--fk-bg);\n box-shadow: 0 0 0 1px var(--fk-border);\n}\n.fk-swatch[aria-pressed=\"true\"] { box-shadow: 0 0 0 2px var(--fk-text); }\n.fk-hint { margin-left: auto; font-size: 12px; color: var(--fk-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.fk-canvas-wrap {\n position: relative;\n flex: 1;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: hidden;\n}\n.fk-canvas-box { position: relative; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18); border-radius: 6px; overflow: hidden; }\n.fk-canvas-box canvas { display: block; }\n.fk-canvas-box .fk-ink { position: absolute; inset: 0; touch-action: none; }\n.fk-stage[data-screenshot=\"off\"] .fk-canvas-box { opacity: 0.25; filter: grayscale(1); }\n.fk-stage[data-screenshot=\"off\"] .fk-toolbar button { pointer-events: none; opacity: 0.35; }\n.fk-off-note {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n font-weight: 600;\n color: var(--fk-muted);\n pointer-events: none;\n}\n.fk-stage[data-screenshot=\"off\"] .fk-off-note { display: flex; }\n.fk-text-input {\n position: absolute;\n z-index: 2;\n min-width: 120px;\n padding: 6px 8px;\n border: 2px solid var(--fk-primary);\n border-radius: 8px;\n background: var(--fk-bg);\n color: var(--fk-text);\n font: bold 14px -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n outline: none;\n}\n\n/* ---------- composer ---------- */\n.fk-composer { display: flex; flex-direction: column; min-height: 0; padding: 18px; gap: 14px; overflow-y: auto; }\n.fk-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n.fk-title { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -0.01em; }\n.fk-close { display: inline-flex; width: 30px; height: 30px; align-items: center; justify-content: center; border: 0; border-radius: 15px; background: var(--fk-surface); color: var(--fk-muted); }\n.fk-close:hover { color: var(--fk-text); }\n.fk-label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--fk-muted); text-transform: uppercase; letter-spacing: 0.04em; }\n.fk-textarea {\n width: 100%;\n min-height: 140px;\n resize: vertical;\n padding: 10px 12px;\n border: 1px solid var(--fk-border);\n border-radius: 10px;\n background: var(--fk-bg);\n color: var(--fk-text);\n font: inherit;\n line-height: 1.45;\n}\n.fk-textarea:focus { outline: none; border-color: var(--fk-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--fk-primary) 22%, transparent); }\n.fk-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.fk-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n height: 28px;\n padding: 0 11px;\n border: 1px solid var(--fk-border);\n border-radius: 14px;\n background: var(--fk-bg);\n font-size: 13px;\n}\n.fk-chip[aria-pressed=\"true\"] { border-color: var(--fk-primary); background: color-mix(in srgb, var(--fk-primary) 12%, transparent); color: var(--fk-primary); font-weight: 600; }\n.fk-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }\n.fk-switch { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; user-select: none; }\n.fk-switch input { position: absolute; opacity: 0; width: 0; height: 0; }\n.fk-switch-track { position: relative; width: 36px; height: 22px; border-radius: 11px; background: var(--fk-border); transition: background 0.15s; }\n.fk-switch-track::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 9px; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.15s; }\n.fk-switch input:checked + .fk-switch-track { background: var(--fk-primary); }\n.fk-switch input:checked + .fk-switch-track::after { transform: translateX(14px); }\n.fk-switch input:focus-visible + .fk-switch-track { outline: 2px solid var(--fk-primary); outline-offset: 2px; }\n.fk-attach {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 12px;\n border: 1px dashed var(--fk-secondary);\n border-radius: 8px;\n background: transparent;\n color: var(--fk-secondary);\n font-size: 13px;\n}\n.fk-file { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 10px; border-radius: 8px; background: var(--fk-surface); font-size: 12px; }\n.fk-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.fk-file button { display: inline-flex; border: 0; background: transparent; color: var(--fk-muted); padding: 2px; }\n.fk-meta { font-size: 12px; color: var(--fk-muted); }\n.fk-meta details summary { cursor: pointer; }\n.fk-meta pre { max-height: 140px; overflow: auto; margin: 6px 0 0; padding: 8px; border-radius: 8px; background: var(--fk-surface); font-size: 11px; white-space: pre-wrap; word-break: break-word; }\n.fk-spacer { flex: 1; }\n.fk-actions { display: flex; gap: 8px; }\n.fk-btn {\n display: inline-flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n gap: 7px;\n height: 40px;\n border-radius: 10px;\n font-weight: 600;\n}\n.fk-btn-secondary { border: 1px solid var(--fk-border); background: transparent; color: var(--fk-secondary); }\n.fk-btn-primary { border: 0; background: var(--fk-primary); color: #fff; }\n.fk-btn-primary:hover:not(:disabled) { filter: brightness(1.06); }\n.fk-error { margin: 0; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--fk-danger) 12%, transparent); color: var(--fk-danger); font-size: 13px; }\n.fk-spinner { width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.45); border-top-color: #fff; border-radius: 50%; animation: fk-spin 0.7s linear infinite; }\n@keyframes fk-spin { to { transform: rotate(360deg); } }\n.fk-done { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; flex: 1; text-align: center; }\n.fk-done-icon { display: inline-flex; width: 48px; height: 48px; align-items: center; justify-content: center; border-radius: 24px; background: var(--fk-primary); color: #fff; }\n.fk-done-icon svg { width: 26px; height: 26px; }\n.fk-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n\n/* ---------- \"is it fixed?\" card (fixCard.ts) ---------- */\n.fk-fixcard {\n position: fixed;\n z-index: 2147483000;\n right: 20px;\n bottom: 76px;\n width: min(360px, calc(100vw - 32px));\n max-height: min(640px, calc(100vh - 96px));\n overflow: auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 16px;\n border: 1px solid var(--fk-border);\n border-radius: 16px;\n background: var(--fk-bg);\n box-shadow: var(--fk-shadow);\n animation: fk-rise 0.2s ease-out;\n}\n.fk-fixcard[data-position=\"bottom-left\"] { right: auto; left: 20px; }\n.fk-fixcard > *, .fk-fixcard-actions > * { flex-shrink: 0; }\n.fk-fixcard p { margin: 0; }\n.fk-fixcard-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }\n.fk-fixcard-shot { display: block; width: 100%; max-height: 220px; object-fit: contain; border-radius: 10px; background: var(--fk-surface); }\n.fk-fixcard-quote { font-size: 14px; }\n.fk-fixcard-summary { font-size: 13px; padding: 8px 10px; border-radius: 8px; background: var(--fk-surface); }\n.fk-fixcard-message p { font-size: 13px; }\n.fk-fixcard-question { font-weight: 600; }\n.fk-fixcard-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }\n.fk-fixcard-actions .fk-btn { flex: none; width: 100%; }\n.fk-fixcard-actions .fk-textarea { min-height: 64px; }\n@keyframes fk-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }\n\n/* ---------- narrow screens: stack stage over composer ---------- */\n@media (max-width: 760px) {\n .fk-overlay { padding: 0; }\n .fk-dialog { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; width: 100%; height: 100%; border-radius: 0; }\n .fk-stage { border-right: 0; border-bottom: 1px solid var(--fk-border); }\n .fk-hint { display: none; }\n .fk-canvas-wrap { padding: 10px; }\n .fk-composer { max-height: 55vh; padding: 14px; gap: 10px; }\n .fk-textarea { min-height: 84px; }\n .fk-fixcard, .fk-fixcard[data-position] { right: 16px; left: 16px; width: auto; bottom: 16px; }\n}\n@media (prefers-reduced-motion: reduce) {\n .fk-overlay, .fk-dialog, .fk-fixcard { animation: none; }\n}\n";
@@ -1,4 +1,5 @@
1
- import type { CaptureOptions, FeedbackKitConfiguration, FeedbackLogEntry, FeedbackProduct, FeedbackReport, FeedbackTheme } from "../types";
1
+ import type { CaptureOptions, FeedbackKitConfiguration, FeedbackLogEntry, FeedbackProduct, FeedbackReport, FeedbackTheme, FeedbackUser, FixUpdate } from "../types";
2
+ import { type FixCardOutcome } from "./fixCard";
2
3
  export interface WidgetDeps {
3
4
  configuration(): FeedbackKitConfiguration | null;
4
5
  theme(): FeedbackTheme | null;
@@ -7,6 +8,11 @@ export interface WidgetDeps {
7
8
  logs(): FeedbackLogEntry[];
8
9
  captureOptions(): CaptureOptions;
9
10
  currentScreen(): string | null;
11
+ /** Reporter identity attached to submitted reports (fixes.ts). */
12
+ identity(): {
13
+ reporterId: string;
14
+ user: FeedbackUser | null;
15
+ };
10
16
  }
11
17
  export interface OpenOptions {
12
18
  /** POST the report to the configured endpoint before resolving. */
@@ -15,6 +21,21 @@ export interface OpenOptions {
15
21
  onReport?: (report: FeedbackReport) => void | Promise<void>;
16
22
  /** Called if submitting fails. The dialog stays open so the user can retry. */
17
23
  onError?: (error: unknown) => void;
24
+ /**
25
+ * Deliver the report somewhere other than the ingestion endpoint — used by
26
+ * "still broken", which sends it as a reopen of the original report.
27
+ * Takes precedence over `submit`. Shows the thank-you state like `submit`.
28
+ */
29
+ deliver?: (report: FeedbackReport) => Promise<void>;
30
+ /** Dialog wording overrides (e.g. "What's still wrong?" for a reopen). */
31
+ copy?: DialogCopy;
32
+ }
33
+ export interface DialogCopy {
34
+ title?: string;
35
+ label?: string;
36
+ placeholder?: string;
37
+ thanksTitle?: string;
38
+ thanksBody?: string;
18
39
  }
19
40
  export interface TriggerOptions {
20
41
  position?: "bottom-right" | "bottom-left";
@@ -33,7 +54,15 @@ export declare class Widget {
33
54
  private shadow;
34
55
  private trigger;
35
56
  private pending;
57
+ private fixCard;
36
58
  constructor(deps: WidgetDeps);
59
+ get isShowingFixCard(): boolean;
60
+ /**
61
+ * The non-modal "is it fixed?" card (bottom corner). Resolves with what
62
+ * the reporter chose; the card removes itself.
63
+ */
64
+ showFixCard(update: FixUpdate): Promise<FixCardOutcome>;
65
+ hideFixCard(): void;
37
66
  /** True for the widget's own host element — used to keep it out of screenshots. */
38
67
  isOwnNode: (node: Node) => boolean;
39
68
  get isOpen(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbackkit-web",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "description": "Drop-in in-app feedback for websites: capture a screenshot, annotate it, describe the problem, and get a structured FeedbackKit report.",
5
5
  "type": "module",
6
6
  "main": "./dist/feedbackkit.js",