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 +151 -35
- package/dist/embed/index.d.ts +48 -0
- package/dist/embed/index.js +2742 -0
- package/dist/embed/linear-feedback.js +130 -0
- package/dist/react/index.cjs +144 -91
- package/dist/react/index.d.cts +7 -1
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +144 -91
- package/dist/server/index.cjs +94 -49
- package/dist/server/index.d.cts +32 -12
- package/dist/server/index.d.ts +32 -12
- package/dist/server/index.js +92 -49
- package/dist/vite/index.cjs +93 -28
- package/dist/vite/index.d.cts +19 -1
- package/dist/vite/index.d.ts +19 -1
- package/dist/vite/index.js +93 -28
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -2,32 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-linear-feedback) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
**A drop-in
|
|
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
|
|
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 →  -->
|
|
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
|
-
- 🔌 **
|
|
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
|
-
- [
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
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.**
|
|
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
|
-
|
|
75
|
+
### Vite SPA
|
|
66
76
|
|
|
67
|
-
|
|
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
|
-
**
|
|
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).
|
|
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
|
-
**
|
|
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
|
-
|
|
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`
|
|
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
|
-
| `
|
|
179
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
-
- **
|
|
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](#
|
|
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 };
|