react-linear-feedback 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,32 +2,42 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/react-linear-feedback.svg)](https://www.npmjs.com/package/react-linear-feedback) [![license](https://img.shields.io/npm/l/react-linear-feedback.svg)](./LICENSE)
4
4
 
5
- **A drop-in React feedback widget that turns a drawn box + note into a Linear issue with an annotated screenshot.**
5
+ **A drop-in feedback widget that turns a drawn box + note into a Linear issue with an annotated screenshot.** Use it as a React component, or embed it on **any site** — Framer, Webflow, plain HTML — with a single script tag.
6
6
 
7
- A trusted user opens it with `?feedback`, **drags a box** over the page, picks a type (**Bug / Improvement**), writes a note — and it captures a screenshot and files a Linear issue. Framework-agnostic, self-contained styling, no design system required.
7
+ A user opens the widget, **drags a box** over the page, picks a type (**Bug / Improvement**), writes a note — and it captures a screenshot and files a Linear issue. Self-contained styling, no design system required. While a region is being selected or the composer is open, the page behind is frozen — scrolling is locked and clicks outside the composer are blocked; Esc cancels.
8
8
 
9
9
  <!-- Demo: record a short GIF and drop it here → ![demo](docs/demo.gif) -->
10
10
  > _Draw a box anywhere → type a note → it lands in Linear with the annotated screenshot and page context._
11
11
 
12
+ - 🌍 **Three ways in** — a React component, a framework-agnostic `init()` import for any bundler app, or a ~23KB script tag for sites with no build step
12
13
  - 🪶 **Themeable in one prop** — no CSS import, no Tailwind, no design system; styles are injected at runtime
13
- - 🌍 **Works in any React app** — Next.js, Vite, Remix, CRA… (no `next` dependency; `"use client"` is built in)
14
14
  - 🖼️ **Annotated screenshots** via [`modern-screenshot`](https://github.com/qq15725/modern-screenshot) (handles Tailwind v4 / `oklch()`)
15
- - 🏷️ **Labels by name, self-healing** — resolved at request time, so recoloring/recreating a label in Linear won't break it; applied best-effort
16
- - 🔌 **Tiny server core** + Next.js, Node/Express, and Vite-dev adapters
15
+ - 🏷️ **Labels by name, self-healing** — resolved at request time (and cached), so recoloring/recreating a label in Linear won't break it; applied best-effort
16
+ - 🔌 **One Web-standard server handler** + Next.js, Node/Express, and Vite-dev adapters, with built-in CORS for cross-origin embeds
17
17
 
18
18
  ## Contents
19
19
 
20
- - [Install](#install) · [Quick start (Next.js)](#quick-start-nextjs) · [Vite / any backend](#use-with-vite--any-react-app) · [Linear setup](#linear-setup) · [Configuration](#configuration) · [Theming](#theming) · [Custom types](#custom-types) · [Security](#security) · [Troubleshooting](#troubleshooting) · [License](#license)
20
+ - [How it fits together](#how-it-fits-together) · [React apps (npm)](#react-apps-npm) · [Any framework (npm import)](#any-framework-npm-import) · [Any site (script tag)](#any-site-script-tag) · [The server handler](#the-server-handler) · [Linear setup](#linear-setup) · [Configuration](#configuration) · [Theming](#theming) · [Custom types](#custom-types) · [Security](#security) · [Troubleshooting](#troubleshooting) · [License](#license)
21
21
 
22
- ## Install
22
+ ## How it fits together
23
+
24
+ The widget (React component or script-tag embed) never talks to Linear directly — it POSTs to **your** endpoint, which holds the API key server-side and creates the issue:
25
+
26
+ ```
27
+ widget (browser) ──POST──▶ your endpoint (createWebHandler) ──▶ Linear API
28
+ ```
29
+
30
+ So every setup is two steps: mount the widget, and deploy the handler ([The server handler](#the-server-handler)).
31
+
32
+ ## React apps (npm)
23
33
 
24
34
  ```bash
25
35
  npm i react-linear-feedback
26
36
  ```
27
37
 
28
- `react` / `react-dom` are peer dependencies. `@linear/sdk` ships as a dependency — it's imported only by the server entry, so it's tree-shaken out of client bundles. Do [Linear setup](#linear-setup) first to get your API key, team, and labels.
38
+ `react` / `react-dom` are peer dependencies. `@linear/sdk` ships as a dependency — it's imported only by the server entry, so it's tree-shaken out of client bundles.
29
39
 
30
- ## Quick start (Next.js, App Router)
40
+ ### Next.js (App Router)
31
41
 
32
42
  **1. Server route** — `app/api/feedback/route.ts`:
33
43
 
@@ -60,13 +70,11 @@ export default function RootLayout({ children }) {
60
70
 
61
71
  No `"use client"` needed — the package ships it, so you can mount `<FeedbackGate>` straight from a Server Component layout.
62
72
 
63
- **3. Turn it on.** The widget is **hidden by default**. Visit any page with **`?feedback`** (or `?feedback=1`) to enable it; a cookie remembers the choice for 90 days. `?feedback=0` turns it off.
73
+ **3. Turn it on.** `<FeedbackGate>` is **hidden by default**. Visit any page with **`?feedback`** (or `?feedback=1`) to enable it; a cookie remembers the choice for 90 days. `?feedback=0` turns it off. (Use `<FeedbackWidget>` instead for an always-visible button.)
64
74
 
65
- ## Use with Vite / any React app
75
+ ### Vite SPA
66
76
 
67
- Unlike Next.js, a Vite SPA has **no server of its own** — so the handler runs as a serverless function (e.g. on Vercel) in production, and as a dev-server plugin locally. Same widget either way.
68
-
69
- **1. Mount the widget** once, in your root component:
77
+ Mount the widget the same way; the handler runs as a serverless function in production and as a dev-server plugin locally:
70
78
 
71
79
  ```tsx
72
80
  import { FeedbackGate } from "react-linear-feedback/react";
@@ -74,12 +82,11 @@ import { FeedbackGate } from "react-linear-feedback/react";
74
82
  <FeedbackGate brandColor="#7f56d9" />; // endpoint defaults to /api/feedback (same origin)
75
83
  ```
76
84
 
77
- **2. Production — a Vercel serverless function** at `api/feedback.ts`:
85
+ **Production — a Vercel serverless function** at `api/feedback.ts`:
78
86
 
79
87
  ```ts
80
88
  import { createNodeHandler, cookieGate } from "react-linear-feedback/server";
81
89
 
82
- // Node runtime (the default for /api functions) — Edge has no Buffer for the screenshot upload.
83
90
  export default createNodeHandler({
84
91
  apiKey: process.env.LINEAR_API_KEY!,
85
92
  teamId: process.env.LINEAR_TEAM_ID!,
@@ -87,9 +94,9 @@ export default createNodeHandler({
87
94
  });
88
95
  ```
89
96
 
90
- Set `LINEAR_API_KEY` / `LINEAR_TEAM_ID` in your Vercel project's env (server-side — **not** `VITE_`-prefixed, so they never reach the bundle). The function is same-origin as the SPA, so no CORS needed.
97
+ Set `LINEAR_API_KEY` / `LINEAR_TEAM_ID` in your Vercel project's env (server-side — **not** `VITE_`-prefixed, so they never reach the bundle).
91
98
 
92
- **3. Local dev — the Vite plugin**, so `vite dev` serves the same endpoint (without it, `POST /api/feedback` 404s locally):
99
+ **Local dev — the Vite plugin**, so `vite dev` serves the same endpoint (without it, `POST /api/feedback` 404s locally):
93
100
 
94
101
  ```ts
95
102
  // vite.config.ts
@@ -113,9 +120,105 @@ export default defineConfig(({ mode }) => {
113
120
 
114
121
  The plugin is dev-only (`apply: "serve"`) — it has no effect on the production build.
115
122
 
116
- ### Other Node servers (Express, Hono, …)
123
+ ## Any framework (npm import)
124
+
125
+ Not a React app? The same self-mounting widget is available as a plain ESM import — Vue, Svelte, Astro, Solid, vanilla TS, anything with a bundler. **Zero peer dependencies** (a compact React-compatible runtime is bundled inside, ~23KB gz):
126
+
127
+ ```ts
128
+ import { init, destroy } from "react-linear-feedback/embed";
129
+
130
+ init({
131
+ endpoint: "/api/feedback",
132
+ brandColor: "#7f56d9",
133
+ // mode: "gated", // ?feedback URL-param gate, like <FeedbackGate>
134
+ // token: "...", // for headerGate'd cross-origin endpoints
135
+ });
136
+
137
+ // destroy() unmounts it (e.g. SPA route teardown / HMR)
138
+ ```
139
+
140
+ `init()` takes the same options as the React component plus `mode` and `token` (see [Configuration](#configuration)). Calling it again replaces the existing instance.
141
+
142
+ React apps should prefer the [`<FeedbackGate>` component](#react-apps-npm) — it shares the page's React instead of shipping a second renderer.
143
+
144
+ ## Any site (script tag)
145
+
146
+ No npm, no build step, no React on the page — the embed bundle (~23KB gz, served from a CDN off the npm package) mounts the same widget on any site: Framer, Webflow, WordPress, plain HTML.
147
+
148
+ ```html
149
+ <script
150
+ src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.4.0/dist/embed/linear-feedback.js"
151
+ data-endpoint="https://your-app.example.com/api/feedback"
152
+ data-brand-color="#7f56d9"
153
+ data-token="your-embed-token"
154
+ defer
155
+ ></script>
156
+ ```
157
+
158
+ Pin the version in the URL (as above). Because the page and the endpoint are usually on **different origins**, the handler needs `allowedOrigins` set — see [The server handler](#the-server-handler).
159
+
160
+ It must be a classic `<script src>` tag (not `type="module"`, not injected via `innerHTML`) so the embed can read its own config; if you need dynamic injection, use `data-manual` + `init()` below.
161
+
162
+ ### Script-tag options
163
+
164
+ | Attribute | Maps to | Notes |
165
+ | --- | --- | --- |
166
+ | `data-endpoint` | `endpoint` | **Required in practice** — your deployed handler URL |
167
+ | `data-brand-color` | `brandColor` | Any CSS color |
168
+ | `data-position` | `position` | `bottom-right` (default) \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` |
169
+ | `data-fab-label` | `fabLabel` | Button text |
170
+ | `data-mode` | `mode` | `open` (default, always visible) \| `gated` (`?feedback` URL param + cookie, like `<FeedbackGate>`) |
171
+ | `data-token` | `token` | Sent as the `x-feedback-token` header — pair with `headerGate` on the server |
172
+ | `data-z-index` | `zIndex` | Override the widget's stacking context |
173
+ | `data-name-required` | `nameRequired` | `"false"` to skip the name prompt |
174
+ | `data-manual` | — | `"true"` disables auto-init; call `LinearFeedback.init()` yourself |
175
+
176
+ ### JS API
177
+
178
+ Options that aren't expressible as data attributes (like custom `types`) go through `window.lfbConfig` (set **before** the script tag; merged over the data attributes) or the imperative API:
179
+
180
+ ```html
181
+ <script>
182
+ window.lfbConfig = {
183
+ types: [
184
+ { id: "bug", label: "Bug", color: "#ef4444", icon: "bug" },
185
+ { id: "idea", label: "Idea", color: "#22c55e", icon: "improvement" },
186
+ ],
187
+ };
188
+ </script>
189
+ <script src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.4.0/dist/embed/linear-feedback.js" data-endpoint="..." defer></script>
190
+ ```
191
+
192
+ ```js
193
+ // With data-manual="true":
194
+ LinearFeedback.init({ endpoint: "https://...", brandColor: "#7f56d9", token: "..." });
195
+ LinearFeedback.destroy();
196
+ ```
197
+
198
+ A runnable demo lives in [`examples/embed.html`](./examples/embed.html) + [`examples/dev-server.mjs`](./examples/dev-server.mjs).
199
+
200
+ ## The server handler
201
+
202
+ `createWebHandler` is the canonical handler — a Web-standard `(Request) => Promise<Response>` function. `createNextRoute` is the same function under its Next.js name, and `createNodeHandler` wraps it for `(req, res)`-style servers (Express, plain `node:http`, Vercel Node functions). All of them run on the **Node runtime** (the screenshot upload uses `Buffer`; Edge isn't supported).
203
+
204
+ ```ts
205
+ import { createWebHandler, headerGate } from "react-linear-feedback/server";
206
+
207
+ const handler = createWebHandler({
208
+ apiKey: process.env.LINEAR_API_KEY!,
209
+ teamId: process.env.LINEAR_TEAM_ID!,
210
+ // Required for script-tag embeds on other domains — enables CORS (preflight + headers):
211
+ allowedOrigins: ["https://your-marketing-site.com", "https://www.your-marketing-site.com"],
212
+ // Cookie gates can't cross origins — gate embeds with the shared token instead:
213
+ authorize: headerGate("your-embed-token"),
214
+ });
215
+ ```
216
+
217
+ Cross-origin example end-to-end: the script tag on `your-marketing-site.com` has `data-endpoint="https://your-app.example.com/api/feedback"` and `data-token="your-embed-token"`; the handler at that endpoint lists the marketing origin in `allowedOrigins` and gates with `headerGate("your-embed-token")`. Same-origin setups (widget and handler on one domain) need neither.
218
+
219
+ ### Express / other Node servers
117
220
 
118
- `createNodeHandler` is a plain `(req, res)` handler that reads the raw body itself (works with or without `express.json()`):
221
+ `createNodeHandler` works with or without `express.json()`:
119
222
 
120
223
  ```ts
121
224
  import express from "express";
@@ -130,8 +233,6 @@ app.post("/api/feedback", createNodeHandler({
130
233
  app.listen(8787);
131
234
  ```
132
235
 
133
- If the SPA and API are on **different origins**, set `allowedOrigin: "https://your-site.com"` and enable CORS (`credentials: true` so the cookie is sent).
134
-
135
236
  ## Linear setup
136
237
 
137
238
  You need three things from Linear:
@@ -148,7 +249,7 @@ LINEAR_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
148
249
 
149
250
  ## Configuration
150
251
 
151
- ### Widget props (`<FeedbackGate>` / `<FeedbackWidget>`)
252
+ ### Widget props (`<FeedbackGate>` / `<FeedbackWidget>` / `LinearFeedback.init`)
152
253
 
153
254
  Most apps only set `brandColor` and maybe `endpoint`.
154
255
 
@@ -156,32 +257,35 @@ Most apps only set `brandColor` and maybe `endpoint`.
156
257
  | --- | --- | --- |
157
258
  | `endpoint` | `/api/feedback` | Where the widget POSTs |
158
259
  | `brandColor` | `#6366f1` | FAB / active / focus color (sets the `--lfb-brand` CSS var) |
159
- | `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` |
260
+ | `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` (edge tabs: compact icon-only launcher) |
160
261
  | `types` | Bug, Improvement | `{ id, label, color, icon? }[]` shown in the composer |
161
262
  | `nameRequired` | `true` | Ask for a reporter name before the first submission (saved to localStorage, included in the issue) |
162
263
  | `nameStorageKey` | `wh_feedback_name` | localStorage key for the remembered name |
163
264
  | `fabLabel` | `Give feedback` | Floating button text |
265
+ | `zIndex` | `2147483640` | Stacking context for the widget's layers (sugar for `--lfb-z`, see [Theming](#theming)) |
266
+ | `requestHeaders` | — | Extra headers sent with the submission (the embed's `token` uses this) |
164
267
  | `urlParam` ¹ | `feedback` | Toggle param |
165
268
  | `cookieName` ¹ | `wh_feedback` | Enabled-state cookie name |
166
269
  | `cookieValue` ¹ | `1` | Cookie value when enabled |
167
270
  | `cookieMaxAgeSeconds` ¹ | `7776000` (90d) | Cookie lifetime |
168
271
 
169
- ¹ `<FeedbackGate>` only.
272
+ ¹ `<FeedbackGate>` only (and the embed's `mode: "gated"`).
170
273
 
171
- ### Server config (`createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
274
+ ### Server config (`createWebHandler` / `createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
172
275
 
173
276
  | Field | Description |
174
277
  | --- | --- |
175
278
  | `apiKey` | Linear personal API key (**server-side only**) |
176
279
  | `teamId` | Target team UUID |
177
280
  | `labels` | Optional `{ [typeId]: labelName }` map. Default: the type id **is** the label name (so type `bug` → label `bug`) |
178
- | `allowedOrigin` | Optional single-origin allowlist (adapters) |
179
- | `authorize(req)` | Optional gate; return `false` to reject. `cookieGate(name, value="1")` is provided |
281
+ | `labelCacheTtlMs` | How long label name→ID lookups are cached (default 10 min; `0` disables). Saves a Linear API round-trip per submission |
282
+ | `allowedOrigins` | Origins allowed to call the endpoint cross-origin (exact origins or `"*"`); enables CORS. Required for script-tag embeds on other domains |
283
+ | `authorize(req)` | Optional gate; return `false` to reject. `cookieGate(name, value="1")` and `headerGate(token, headerName="x-feedback-token")` are provided |
180
284
 
181
285
  Remap type ids to differently-named Linear labels:
182
286
 
183
287
  ```ts
184
- createNextRoute({
288
+ createWebHandler({
185
289
  apiKey, teamId,
186
290
  labels: { bug: "bug", idea: "feature-request" },
187
291
  });
@@ -192,6 +296,12 @@ createNextRoute({
192
296
  Set `brandColor`, or override any CSS variable on `.lfb-root`:
193
297
  `--lfb-brand`, `--lfb-fg`, `--lfb-surface`, `--lfb-border`, `--lfb-radius`, `--lfb-rect`, `--lfb-z`, `--lfb-font`.
194
298
 
299
+ The widget defaults to a very high `--lfb-z` (`2147483640`) so it sits above app chrome. If it lands on top of your own modals or toasts, lower it — either with the `zIndex` prop / `data-z-index` attribute, or in CSS:
300
+
301
+ ```css
302
+ .lfb-root { --lfb-z: 40; }
303
+ ```
304
+
195
305
  ## Custom types
196
306
 
197
307
  ```tsx
@@ -203,26 +313,32 @@ Set `brandColor`, or override any CSS variable on `.lfb-root`:
203
313
  />
204
314
  ```
205
315
 
206
- Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`.
316
+ Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`. For the script-tag embed, pass `types` via `window.lfbConfig` or `LinearFeedback.init()`.
207
317
 
208
318
  ## Security
209
319
 
210
320
  ⚠️ **The endpoint creates Linear issues**, so it's effectively write access to your tracker, and it's **open by default**. Before shipping on a public page:
211
321
 
212
- - **Gate it** with `authorize` (e.g. `cookieGate`, or your own session check).
213
- - **Restrict origin** with `allowedOrigin: "https://your-site.com"`.
322
+ - **Gate it** with `authorize` (e.g. `cookieGate`, `headerGate`, or your own session check).
323
+ - **Restrict origins** with `allowedOrigins: ["https://your-site.com"]`.
214
324
  - **Rate-limit** if the page is public (e.g. per-IP).
215
325
 
326
+ ### Gating an embedded (cross-origin) widget
327
+
328
+ `cookieGate` only works **same-origin**: the gate cookie is written on the page's origin, so the browser never sends it with a cross-origin submission. For script-tag embeds, use the shared token instead — `data-token="..."` on the script tag plus `authorize: headerGate("...")` on the handler. The token is visible in the page source, so treat it as a **tripwire against drive-by spam, not authentication**; combine it with `allowedOrigins` and rate limiting.
329
+
216
330
  Your `LINEAR_API_KEY` stays server-side; the package never ships it to the browser. Screenshots are uploaded to Linear's **private** asset storage — they render inside the issue, but the URL needs a fresh signed token to fetch elsewhere (it can't be hot-linked). If a screenshot upload fails, the issue is still created without it.
217
331
 
218
332
  ## Troubleshooting
219
333
 
220
334
  Submissions never throw in the UI — failures are logged to the browser console with a `[feedback]` prefix, and server errors return a JSON `message`. If issues aren't being created, check:
221
335
 
222
- - `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set.
223
- - **CORS** is configured when the app and API are on different origins.
336
+ - `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set (the handler logs a `[feedback]` warning at startup when they're missing).
337
+ - A rejected `authorize` returns **404 by design** — the endpoint is hidden, not just forbidden. If you're debugging your gate and seeing 404s, it's not a routing problem.
338
+ - **CORS**: cross-origin pages (script-tag embeds) need their origin in `allowedOrigins`, and the browser must reach the endpoint with both `OPTIONS` and `POST`.
224
339
  - `runtime = "nodejs"` is set on the Next.js route (Edge has no `Buffer`).
225
- - On a **Vite SPA**, `POST /api/feedback` 404s under `vite dev` unless you add the [`linearFeedback` Vite plugin](#use-with-vite--any-react-app) (or run `vercel dev`). In production it's served by your deployed function.
340
+ - On a **Vite SPA**, `POST /api/feedback` 404s under `vite dev` unless you add the [`linearFeedback` Vite plugin](#vite-spa) (or run `vercel dev`). In production it's served by your deployed function.
341
+ - **`FUNCTION_INVOCATION_FAILED` on Vercel but local dev works?** Your function file itself may not parse — the Vite plugin serves the dev endpoint without ever loading it, so syntax errors there only surface when deployed. Check it directly: `node -e "import('./api/feedback.mjs')"`. (One real-world culprit: a `*/` sequence inside a block comment — e.g. a `**/*` glob — terminates the comment early.)
226
342
  - The expected Linear **labels exist** (otherwise the issue is created without a label, with a warning).
227
343
 
228
344
  ## License
@@ -0,0 +1,48 @@
1
+ /** Built-in inline icon keys usable on a type option. */
2
+ type FeedbackIconName = "bug" | "improvement" | "dot";
3
+ /** A selectable issue type shown in the composer's segmented control. */
4
+ type FeedbackTypeOption = {
5
+ /** Stable id sent to the server and mapped to a Linear label name. */
6
+ id: string;
7
+ /** Human label shown in the UI and used in the issue title (e.g. "Bug"). */
8
+ label: string;
9
+ /** Swatch background color, any CSS color. */
10
+ color: string;
11
+ /** Optional built-in icon for the swatch. */
12
+ icon?: FeedbackIconName;
13
+ };
14
+
15
+ type FeedbackPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left"
16
+ /** Edge tabs: compact, icon-only launcher flush to the side, vertically centered. */
17
+ | "right" | "left";
18
+ type FeedbackWidgetProps = {
19
+ /** Endpoint that runs the server handler (default "/api/feedback"). */
20
+ endpoint?: string;
21
+ /** Brand color for the FAB, active states and focus rings (any CSS color). */
22
+ brandColor?: string;
23
+ /** Corner for the floating button (default "bottom-right"). */
24
+ position?: FeedbackPosition;
25
+ /** Selectable issue types (default Bug / Improvement). */
26
+ types?: FeedbackTypeOption[];
27
+ /** Ask for a reporter name before the first submission (default true). */
28
+ nameRequired?: boolean;
29
+ /** localStorage key for the remembered name. */
30
+ nameStorageKey?: string;
31
+ /** Label on the floating button (default "Give feedback"). */
32
+ fabLabel?: string;
33
+ /** Stacking context for the widget's layers — sugar for the `--lfb-z` CSS variable. */
34
+ zIndex?: number;
35
+ /** Extra headers sent with the submission (e.g. x-feedback-token for `headerGate`). */
36
+ requestHeaders?: Record<string, string>;
37
+ };
38
+
39
+ type LinearFeedbackConfig = FeedbackWidgetProps & {
40
+ /** "open" (default): widget always visible. "gated": enabled via ?feedback URL param + cookie. */
41
+ mode?: "open" | "gated";
42
+ /** Shared secret sent as the x-feedback-token header — pair with `headerGate` on the server. */
43
+ token?: string;
44
+ };
45
+ declare function init(config?: LinearFeedbackConfig): void;
46
+ declare function destroy(): void;
47
+
48
+ export { type LinearFeedbackConfig, destroy, init };