feedbackkit-web 1.0.37

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,64 @@
1
+ import type { FeedbackAnnotation, NormalizedPoint } from "./types";
2
+ /**
3
+ * Canvas port of the Swift SDK's `AnnotationRenderer`
4
+ * (Sources/FeedbackKit/Annotation/AnnotationRenderer.swift). Same stroke
5
+ * width, arrowhead geometry, text-bubble metrics, scale/rotation math and
6
+ * hit-testing, so a report annotated in a browser renders the same way a
7
+ * native one does — and the dashboard can use this one file to redraw
8
+ * annotations from any platform on top of the raw screenshot.
9
+ *
10
+ * All sizes are in the *target's* coordinate space (CSS pixels on the web,
11
+ * points on iOS/macOS); callers scale the context for device pixels.
12
+ */
13
+ export interface Point {
14
+ x: number;
15
+ y: number;
16
+ }
17
+ export interface Size {
18
+ width: number;
19
+ height: number;
20
+ }
21
+ /** Anything with `measureText` — lets hit-testing run without a real canvas in tests. */
22
+ export interface TextMeasurer {
23
+ font: string;
24
+ measureText(text: string): {
25
+ width: number;
26
+ };
27
+ }
28
+ export declare const STROKE_WIDTH = 4;
29
+ export declare const HIT_TEST_TOLERANCE = 16;
30
+ export declare function textFont(scale?: number): string;
31
+ /** Accepts both the `[x, y]` wire shape and a legacy/defensive `{x, y}` object. */
32
+ export declare function readPoint(p: NormalizedPoint | Point | unknown): Point;
33
+ export declare function denormalize(annotation: FeedbackAnnotation, size: Size): Point[];
34
+ export declare function normalizeColor(hex: string | undefined): string;
35
+ export declare function drawAnnotations(ctx: CanvasRenderingContext2D, annotations: readonly FeedbackAnnotation[], size: Size): void;
36
+ export declare function drawAnnotation(ctx: CanvasRenderingContext2D, annotation: FeedbackAnnotation, size: Size): void;
37
+ export declare function arrowGeometry(rawStart: Point, rawEnd: Point, scale?: number, rotation?: number): {
38
+ start: Point;
39
+ end: Point;
40
+ left: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ right: {
45
+ x: number;
46
+ y: number;
47
+ };
48
+ };
49
+ export declare function textBubbleRect(measurer: TextMeasurer, text: string, point: Point, scale?: number): {
50
+ x: number;
51
+ y: number;
52
+ width: number;
53
+ height: number;
54
+ };
55
+ export declare function rectangleCorners(center: Point, halfWidth: number, halfHeight: number, rotation: number): Point[];
56
+ /**
57
+ * Whether a pointer at `point` grabs `annotation` for a drag. Freehand
58
+ * strokes are intentionally not draggable, same as native.
59
+ */
60
+ export declare function hitTest(annotation: FeedbackAnnotation, point: Point, size: Size, measurer: TextMeasurer): boolean;
61
+ export declare function midpoint(a: Point, b: Point): Point;
62
+ export declare function rotate(p: Point, angle: number, center: Point): Point;
63
+ export declare function scaled(p: Point, scale: number, center: Point): Point;
64
+ export declare function distanceToSegment(p: Point, a: Point, b: Point): number;
@@ -0,0 +1,42 @@
1
+ import type { FeedbackKitConfiguration, FeedbackProduct, FeedbackReport } from "./types";
2
+ /** The hosted FeedbackKit dashboard's ingestion endpoint. */
3
+ export declare const DEFAULT_ENDPOINT = "https://gpucoladcyvijefdjudf.supabase.co/functions/v1/ingest-feedback";
4
+ export declare class FeedbackSubmissionError extends Error {
5
+ readonly kind: "not_configured" | "network" | "server";
6
+ readonly status?: number | undefined;
7
+ constructor(message: string, kind: "not_configured" | "network" | "server", status?: number | undefined);
8
+ }
9
+ /**
10
+ * Wire format sent to the ingestion endpoint — the exact shape the Swift
11
+ * SDK's private `IngestPayload` produces (Sources/FeedbackKit/Networking/FeedbackSubmitter.swift)
12
+ * and `supabase/functions/ingest-feedback` accepts. snake_case at the top
13
+ * level; `environment`/`annotations`/`logs` pass through untouched (camelCase
14
+ * JSONB). Optional fields are omitted rather than sent as null, matching
15
+ * Swift's `encodeIfPresent`.
16
+ */
17
+ export interface IngestPayload {
18
+ project_key: string;
19
+ id: string;
20
+ created_at: string;
21
+ text: string;
22
+ screenshot_raw_png_base64?: string;
23
+ screenshot_annotated_png_base64?: string;
24
+ annotations: FeedbackReport["annotations"];
25
+ environment: FeedbackReport["environment"];
26
+ attachment_filename?: string;
27
+ attachment_mime_type?: string;
28
+ attachment_data_base64?: string;
29
+ product_keys?: string[];
30
+ products?: {
31
+ key: string;
32
+ name: string;
33
+ description: string;
34
+ is_default: boolean;
35
+ }[];
36
+ logs?: FeedbackReport["logs"];
37
+ }
38
+ 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>;
41
+ /** `GET <endpoint>?project_key=…` — the products configured for the project in the dashboard. */
42
+ export declare function fetchProducts(configuration: FeedbackKitConfiguration): Promise<FeedbackProduct[]>;
@@ -0,0 +1,149 @@
1
+ import type { LogCaptureOptions } from "./logs";
2
+ /**
3
+ * Public data model. Mirrors the Swift SDK's `FeedbackReport` family
4
+ * (Sources/FeedbackKit/Model/FeedbackReport.swift) so a report captured on
5
+ * the web is interchangeable with one captured on iOS/macOS/watchOS.
6
+ *
7
+ * Like the Swift SDK, the public API is camelCase and the wire format
8
+ * (see `submit.ts`) is snake_case at the top level — except `environment`,
9
+ * `annotations`, and `logs`, which are stored as opaque JSONB and read
10
+ * directly by the dashboard, the Developer Portal, and the CLI, so their
11
+ * property names are part of the cross-platform contract and must stay in
12
+ * sync with `web/src/lib/types.ts` and the Swift model by hand.
13
+ */
14
+ /**
15
+ * A point normalized to 0...1 relative to the screenshot's width/height.
16
+ * Encoded as a `[x, y]` tuple because that's exactly how Swift's `CGPoint`
17
+ * encodes through `Codable` — the Developer Portal decodes web reports with
18
+ * the Swift SDK's own `FeedbackAnnotation`, so any other shape would fail.
19
+ */
20
+ export type NormalizedPoint = [x: number, y: number];
21
+ export type AnnotationKind = "rectangle" | "arrow" | "freehand" | "text";
22
+ export interface FeedbackAnnotation {
23
+ kind: AnnotationKind;
24
+ /** Freehand: every point on the stroke. Rectangle/arrow: exactly two (start, end). Text: one (top-left). */
25
+ points: NormalizedPoint[];
26
+ colorHex: string;
27
+ /** Only set for `text` annotations. */
28
+ label?: string;
29
+ /** Uniform scale around the shape's center (rectangle/arrow) or font size (text). */
30
+ scale: number;
31
+ /** Rotation in radians around the shape's center (rectangle/arrow only). */
32
+ rotation: number;
33
+ }
34
+ /**
35
+ * Device/app/page context. Every field the Swift `FeedbackEnvironment` has
36
+ * is required here too (the Developer Portal can't decode a report missing
37
+ * any of them); the trailing web-only fields are optional additions that
38
+ * Swift's `Codable` simply ignores.
39
+ */
40
+ export interface FeedbackEnvironment {
41
+ /** The real OS the browser runs on, e.g. "macOS", "Windows", "iOS", "Android". */
42
+ osName: string;
43
+ osVersion: string;
44
+ /** Browser name and major version, e.g. "Chrome 141". */
45
+ deviceModel: string;
46
+ appVersion: string;
47
+ appBuild: string;
48
+ /** The page's host, e.g. "app.example.com". */
49
+ bundleIdentifier: string;
50
+ /** Developer-set `FeedbackKit.currentScreen`, else `location.pathname`. */
51
+ screenName: string | null;
52
+ locale: string;
53
+ /** Viewport size in CSS pixels. */
54
+ screenWidthPoints: number;
55
+ screenHeightPoints: number;
56
+ /** `devicePixelRatio`. */
57
+ screenScale: number;
58
+ /** Always `"web"` for reports from this SDK. Native SDKs leave it unset. */
59
+ platform?: "web";
60
+ /** Full page URL at capture time, with the query string's sensitive values redacted. */
61
+ pageUrl?: string;
62
+ userAgent?: string;
63
+ /** Browser name alone, e.g. "Chrome". */
64
+ browserName?: string;
65
+ /** Full browser version, e.g. "141.0.7390.54". */
66
+ browserVersion?: string;
67
+ }
68
+ export type LogLevel = "log" | "info" | "warn" | "error" | "debug" | "network";
69
+ /** One console message, uncaught error, or failed network request from before the report was made. */
70
+ export interface FeedbackLogEntry {
71
+ level: LogLevel;
72
+ message: string;
73
+ /** ISO-8601 timestamp. */
74
+ timestamp: string;
75
+ }
76
+ export interface FeedbackAttachment {
77
+ filename: string;
78
+ mimeType: string;
79
+ data: Blob;
80
+ }
81
+ export interface FeedbackProduct {
82
+ key: string;
83
+ name: string;
84
+ description?: string;
85
+ isDefault?: boolean;
86
+ }
87
+ /**
88
+ * The complete report handed back to the developer on submit. FeedbackKit's
89
+ * job ends at producing this value — what happens to it (logging it,
90
+ * POSTing it to your own backend, or `FeedbackKit.submit` to the hosted
91
+ * dashboard) is up to you.
92
+ */
93
+ export interface FeedbackReport {
94
+ id: string;
95
+ createdAt: Date;
96
+ text: string;
97
+ /** Raw viewport capture. Null if the user turned the screenshot off. */
98
+ screenshotRaw: Blob | null;
99
+ /** Same capture with annotations flattened in. Null under the same conditions. */
100
+ screenshotAnnotated: Blob | null;
101
+ /** Empty when there's no screenshot. */
102
+ annotations: FeedbackAnnotation[];
103
+ environment: FeedbackEnvironment;
104
+ attachment: FeedbackAttachment | null;
105
+ products: FeedbackProduct[];
106
+ /** Recent console output / errors / failed requests (web only). Empty if log capture is off. */
107
+ logs: FeedbackLogEntry[];
108
+ }
109
+ /** Brand colors for the widget, as hex strings (`#RRGGBB`). Mirrors Swift's `FeedbackTheme`. */
110
+ export interface FeedbackTheme {
111
+ /** Send button, selected tool, screenshot toggle, trigger button. */
112
+ primaryColorHex?: string;
113
+ /** Cancel and attach buttons. */
114
+ secondaryColorHex?: string;
115
+ }
116
+ export type CaptureMode = "dom" | "display";
117
+ export interface FeedbackKitConfiguration {
118
+ /** The project key from the dashboard's project settings. A routing key, not a secret. */
119
+ projectKey: string;
120
+ /**
121
+ * The ingestion endpoint, e.g. `https://<ref>.supabase.co/functions/v1/ingest-feedback`.
122
+ * Defaults to the hosted FeedbackKit dashboard.
123
+ */
124
+ endpoint?: string;
125
+ /** Static product list. When empty, products are fetched from the backend using `projectKey`. */
126
+ products?: FeedbackProduct[];
127
+ /** Product preselected in the composer (e.g. "web"). */
128
+ defaultProductKey?: string;
129
+ /** Your app's version/build, since a browser can't know them. */
130
+ appVersion?: string;
131
+ appBuild?: string;
132
+ /**
133
+ * Record recent console warnings/errors, uncaught exceptions, and failed
134
+ * network requests to attach to reports (the user can untick them per
135
+ * report). Default `true`; pass options to tune, or `false` to disable.
136
+ */
137
+ captureLogs?: boolean | LogCaptureOptions;
138
+ }
139
+ export interface CaptureOptions {
140
+ /**
141
+ * `"dom"` (default): re-render the page into an image — silent, no
142
+ * permission prompt. `"display"`: the browser's Screen Capture API —
143
+ * pixel-exact but shows a picker every time; falls back to `"dom"` where
144
+ * unsupported or declined.
145
+ */
146
+ mode?: CaptureMode;
147
+ /** Upper bound on the capture's pixel ratio, to keep uploads small on dense displays. Default 2. */
148
+ maxPixelRatio?: number;
149
+ }
@@ -0,0 +1,60 @@
1
+ import type { CapturedScreenshot } from "../capture";
2
+ import type { FeedbackAnnotation } from "../types";
3
+ export type Tool = "pen" | "rectangle" | "arrow" | "text" | "move";
4
+ /** Same palette as the native toolbar: systemRed, systemYellow, systemGreen, systemBlue, label. */
5
+ export declare const COLORS: readonly ["#FF3B30", "#FFCC00", "#34C759", "#007AFF", "#000000"];
6
+ /**
7
+ * The annotation canvas: the screenshot, plus an "ink" canvas on top that
8
+ * the user draws on. Everything is stored in the capture's own coordinate
9
+ * space (viewport CSS pixels) and normalized to 0...1 on the way out, so the
10
+ * on-screen display size never leaks into the report — same contract as
11
+ * the native `AnnotationCanvasView`.
12
+ */
13
+ export declare class AnnotationEditor {
14
+ private readonly shot;
15
+ readonly element: HTMLDivElement;
16
+ annotations: FeedbackAnnotation[];
17
+ tool: Tool;
18
+ color: string;
19
+ onChange: () => void;
20
+ private readonly base;
21
+ private readonly ink;
22
+ private readonly inkCtx;
23
+ private history;
24
+ private draft;
25
+ private draftStart;
26
+ private pointers;
27
+ private drag;
28
+ private gesture;
29
+ private selected;
30
+ private textInput;
31
+ private displayScale;
32
+ constructor(shot: CapturedScreenshot);
33
+ /** Fits the screenshot inside `maxWidth × maxHeight` (CSS px) keeping its aspect ratio. */
34
+ layout(maxWidth: number, maxHeight: number): void;
35
+ setTool(tool: Tool): void;
36
+ undo(): void;
37
+ clear(): void;
38
+ get canUndo(): boolean;
39
+ /** Deletes the annotation last moved/selected with the move tool. */
40
+ deleteSelected(): boolean;
41
+ /** The screenshot at full capture resolution with every annotation burned in. */
42
+ flatten(): HTMLCanvasElement;
43
+ commitTextInput(): void;
44
+ cancelTextInput(): void;
45
+ render(): void;
46
+ private changed;
47
+ private snapshot;
48
+ /** Pointer position in capture coordinates (viewport CSS px of the original page). */
49
+ private toCapture;
50
+ private normalize;
51
+ private hitIndex;
52
+ private onPointerDown;
53
+ private onPointerMove;
54
+ private onPointerUp;
55
+ /** Mouse/trackpad stand-in for pinch/twist: wheel scales, Shift/Alt+wheel rotates. */
56
+ private onWheel;
57
+ private startGesture;
58
+ private updateGesture;
59
+ private openTextInput;
60
+ }
@@ -0,0 +1,15 @@
1
+ export declare const icons: {
2
+ feedback: string;
3
+ pen: string;
4
+ rectangle: string;
5
+ arrow: string;
6
+ text: string;
7
+ move: string;
8
+ undo: string;
9
+ trash: string;
10
+ close: string;
11
+ plus: string;
12
+ paperclip: string;
13
+ send: string;
14
+ check: string;
15
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Widget CSS, scoped inside the host's Shadow DOM so neither the page's
3
+ * styles nor ours leak across. `:host { all: initial }` also blocks inherited
4
+ * properties (font, color, line-height) from the page.
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";
@@ -0,0 +1,48 @@
1
+ import type { CaptureOptions, FeedbackKitConfiguration, FeedbackLogEntry, FeedbackProduct, FeedbackReport, FeedbackTheme } from "../types";
2
+ export interface WidgetDeps {
3
+ configuration(): FeedbackKitConfiguration | null;
4
+ theme(): FeedbackTheme | null;
5
+ products(): Promise<FeedbackProduct[]>;
6
+ defaultProductKey(): string | undefined;
7
+ logs(): FeedbackLogEntry[];
8
+ captureOptions(): CaptureOptions;
9
+ currentScreen(): string | null;
10
+ }
11
+ export interface OpenOptions {
12
+ /** POST the report to the configured endpoint before resolving. */
13
+ submit: boolean;
14
+ /** Called with the finished report (after a successful submit, when `submit` is true). */
15
+ onReport?: (report: FeedbackReport) => void | Promise<void>;
16
+ /** Called if submitting fails. The dialog stays open so the user can retry. */
17
+ onError?: (error: unknown) => void;
18
+ }
19
+ export interface TriggerOptions {
20
+ position?: "bottom-right" | "bottom-left";
21
+ /** Button text. Default "Feedback". */
22
+ label?: string;
23
+ /** Icon-only round button. */
24
+ compact?: boolean;
25
+ }
26
+ /**
27
+ * Owns the widget's Shadow DOM host: the floating trigger button and the
28
+ * capture → annotate → describe → send dialog. One per page.
29
+ */
30
+ export declare class Widget {
31
+ private readonly deps;
32
+ private host;
33
+ private shadow;
34
+ private trigger;
35
+ private pending;
36
+ constructor(deps: WidgetDeps);
37
+ /** True for the widget's own host element — used to keep it out of screenshots. */
38
+ isOwnNode: (node: Node) => boolean;
39
+ get isOpen(): boolean;
40
+ showTrigger(options: TriggerOptions, onClick: () => void): void;
41
+ hideTrigger(): void;
42
+ destroy(): void;
43
+ open(options: OpenOptions): Promise<FeedbackReport | null>;
44
+ private mount;
45
+ private applyTheme;
46
+ private run;
47
+ }
48
+ export declare function uuid(): string;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "feedbackkit-web",
3
+ "version": "1.0.37",
4
+ "description": "Drop-in in-app feedback for websites: capture a screenshot, annotate it, describe the problem, and get a structured FeedbackKit report.",
5
+ "type": "module",
6
+ "main": "./dist/feedbackkit.js",
7
+ "module": "./dist/feedbackkit.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "unpkg": "./dist/feedbackkit.iife.js",
10
+ "jsdelivr": "./dist/feedbackkit.iife.js",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/index.d.ts",
14
+ "import": "./dist/feedbackkit.js"
15
+ },
16
+ "./iife": "./dist/feedbackkit.iife.js",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "vite build && FEEDBACKKIT_IIFE=1 vite build && tsc -p tsconfig.build.json",
27
+ "prepublishOnly": "npm run build",
28
+ "dev": "vite build --watch",
29
+ "lint": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:e2e": "npm run build && node e2e/run.mjs"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tianhaoz95/feedback-kit.git",
36
+ "directory": "web-sdk"
37
+ },
38
+ "homepage": "https://github.com/tianhaoz95/feedback-kit/tree/main/web-sdk#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/tianhaoz95/feedback-kit/issues"
41
+ },
42
+ "keywords": [
43
+ "feedback",
44
+ "bug-report",
45
+ "screenshot",
46
+ "annotation",
47
+ "widget",
48
+ "feedbackkit"
49
+ ],
50
+ "license": "SEE LICENSE IN LICENSE",
51
+ "dependencies": {
52
+ "modern-screenshot": "^4.7.0"
53
+ },
54
+ "devDependencies": {
55
+ "playwright": "^1.63.0",
56
+ "typescript": "^5",
57
+ "vite": "^7",
58
+ "vitest": "^4"
59
+ },
60
+ "engines": {
61
+ "node": ">=18"
62
+ }
63
+ }