react-linear-feedback 0.3.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 +4 -3
- package/dist/embed/index.js +134 -87
- package/dist/embed/linear-feedback.js +11 -8
- package/dist/react/index.cjs +134 -87
- package/dist/react/index.js +134 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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 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.
|
|
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._
|
|
@@ -147,7 +147,7 @@ No npm, no build step, no React on the page — the embed bundle (~23KB gz, serv
|
|
|
147
147
|
|
|
148
148
|
```html
|
|
149
149
|
<script
|
|
150
|
-
src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.
|
|
150
|
+
src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.4.0/dist/embed/linear-feedback.js"
|
|
151
151
|
data-endpoint="https://your-app.example.com/api/feedback"
|
|
152
152
|
data-brand-color="#7f56d9"
|
|
153
153
|
data-token="your-embed-token"
|
|
@@ -186,7 +186,7 @@ Options that aren't expressible as data attributes (like custom `types`) go thro
|
|
|
186
186
|
],
|
|
187
187
|
};
|
|
188
188
|
</script>
|
|
189
|
-
<script src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.
|
|
189
|
+
<script src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.4.0/dist/embed/linear-feedback.js" data-endpoint="..." defer></script>
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
```js
|
|
@@ -338,6 +338,7 @@ Submissions never throw in the UI — failures are logged to the browser console
|
|
|
338
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`.
|
|
339
339
|
- `runtime = "nodejs"` is set on the Next.js route (Edge has no `Buffer`).
|
|
340
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.)
|
|
341
342
|
- The expected Linear **labels exist** (otherwise the issue is created without a label, with a warning).
|
|
342
343
|
|
|
343
344
|
## License
|
package/dist/embed/index.js
CHANGED
|
@@ -2215,6 +2215,8 @@ var CSS2 = `
|
|
|
2215
2215
|
|
|
2216
2216
|
.lfb-rect { position: absolute; border: 2px solid var(--lfb-rect); background: rgba(255,0,85,0.12); border-radius: 3px; pointer-events: none; }
|
|
2217
2217
|
|
|
2218
|
+
.lfb-shield { position: fixed; inset: 0; pointer-events: auto; cursor: default; }
|
|
2219
|
+
|
|
2218
2220
|
.lfb-anchor { position: absolute; pointer-events: auto; }
|
|
2219
2221
|
|
|
2220
2222
|
.lfb-card {
|
|
@@ -2229,6 +2231,7 @@ var CSS2 = `
|
|
|
2229
2231
|
line-height: 1.4;
|
|
2230
2232
|
}
|
|
2231
2233
|
.lfb-composer { position: absolute; top: 100%; left: 0; margin-top: 8px; width: 320px; max-width: calc(100vw - 32px); }
|
|
2234
|
+
.lfb-composer--above { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 8px; }
|
|
2232
2235
|
|
|
2233
2236
|
.lfb-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
2234
2237
|
.lfb-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--lfb-fg-tertiary); }
|
|
@@ -2314,6 +2317,8 @@ function ensureStyles() {
|
|
|
2314
2317
|
// src/react/feedback-widget.tsx
|
|
2315
2318
|
var MIN_DRAG = 12;
|
|
2316
2319
|
var DEFAULT_BOX = { width: 220, height: 130 };
|
|
2320
|
+
var COMPOSER_WIDTH = 320;
|
|
2321
|
+
var COMPOSER_EST_HEIGHT = 320;
|
|
2317
2322
|
var DEFAULT_TYPES = [
|
|
2318
2323
|
{ id: "bug", label: "Bug", color: "#ef4444", icon: "bug" },
|
|
2319
2324
|
{ id: "improvement", label: "Improvement", color: "#22c55e", icon: "improvement" }
|
|
@@ -2342,6 +2347,11 @@ function FeedbackWidget({
|
|
|
2342
2347
|
const [drag, setDrag] = d2(null);
|
|
2343
2348
|
const textareaRef = A2(null);
|
|
2344
2349
|
const nameInputRef = A2(null);
|
|
2350
|
+
const nameEditRef = A2(null);
|
|
2351
|
+
const submittingRef = A2(false);
|
|
2352
|
+
submittingRef.current = submitting;
|
|
2353
|
+
const editingNameRef = A2(false);
|
|
2354
|
+
editingNameRef.current = editingName;
|
|
2345
2355
|
y2(() => {
|
|
2346
2356
|
ensureStyles();
|
|
2347
2357
|
try {
|
|
@@ -2351,12 +2361,19 @@ function FeedbackWidget({
|
|
|
2351
2361
|
}
|
|
2352
2362
|
}, [nameStorageKey]);
|
|
2353
2363
|
y2(() => {
|
|
2354
|
-
if (mode.kind
|
|
2364
|
+
if (mode.kind === "idle") return;
|
|
2355
2365
|
const onKey = (e3) => {
|
|
2356
|
-
if (e3.key
|
|
2357
|
-
|
|
2358
|
-
|
|
2366
|
+
if (e3.key !== "Escape" || submittingRef.current) return;
|
|
2367
|
+
if (editingNameRef.current) {
|
|
2368
|
+
setEditingName(false);
|
|
2369
|
+
return;
|
|
2359
2370
|
}
|
|
2371
|
+
setMode({ kind: "idle" });
|
|
2372
|
+
setDrag(null);
|
|
2373
|
+
setText("");
|
|
2374
|
+
setError(null);
|
|
2375
|
+
setSubmitting(false);
|
|
2376
|
+
setEditingName(false);
|
|
2360
2377
|
};
|
|
2361
2378
|
document.addEventListener("keydown", onKey);
|
|
2362
2379
|
return () => document.removeEventListener("keydown", onKey);
|
|
@@ -2371,6 +2388,22 @@ function FeedbackWidget({
|
|
|
2371
2388
|
return () => window.clearTimeout(t3);
|
|
2372
2389
|
}
|
|
2373
2390
|
}, [mode.kind]);
|
|
2391
|
+
y2(() => {
|
|
2392
|
+
if (editingName) nameEditRef.current?.focus();
|
|
2393
|
+
}, [editingName]);
|
|
2394
|
+
y2(() => {
|
|
2395
|
+
if (mode.kind !== "drawing" && mode.kind !== "composing") return;
|
|
2396
|
+
const el = document.documentElement;
|
|
2397
|
+
const prevOverflow = el.style.overflow;
|
|
2398
|
+
const prevPaddingRight = el.style.paddingRight;
|
|
2399
|
+
const scrollbar = window.innerWidth - el.clientWidth;
|
|
2400
|
+
el.style.overflow = "hidden";
|
|
2401
|
+
if (scrollbar > 0) el.style.paddingRight = `${scrollbar}px`;
|
|
2402
|
+
return () => {
|
|
2403
|
+
el.style.overflow = prevOverflow;
|
|
2404
|
+
el.style.paddingRight = prevPaddingRight;
|
|
2405
|
+
};
|
|
2406
|
+
}, [mode.kind]);
|
|
2374
2407
|
const persistName = (value) => {
|
|
2375
2408
|
try {
|
|
2376
2409
|
window.localStorage.setItem(nameStorageKey, value);
|
|
@@ -2424,12 +2457,17 @@ function FeedbackWidget({
|
|
|
2424
2457
|
vy = end.y - vh / 2;
|
|
2425
2458
|
}
|
|
2426
2459
|
const rect = { x: Math.max(0, vx + window.scrollX), y: Math.max(0, vy + window.scrollY), width: vw, height: vh };
|
|
2460
|
+
const fitsBelow = vy + vh + COMPOSER_EST_HEIGHT <= window.innerHeight;
|
|
2461
|
+
const fitsAbove = vy >= COMPOSER_EST_HEIGHT;
|
|
2462
|
+
const placement = !fitsBelow && fitsAbove ? "above" : "below";
|
|
2463
|
+
const maxAnchorX = window.scrollX + Math.max(16, window.innerWidth - COMPOSER_WIDTH - 16);
|
|
2464
|
+
const anchorX = Math.min(rect.x, maxAnchorX);
|
|
2427
2465
|
setDrag(null);
|
|
2428
2466
|
setText("");
|
|
2429
2467
|
setIssueType(types[0]?.id ?? "bug");
|
|
2430
2468
|
setError(null);
|
|
2431
2469
|
setEditingName(false);
|
|
2432
|
-
setMode({ kind: "composing", rect });
|
|
2470
|
+
setMode({ kind: "composing", rect, placement, anchorX });
|
|
2433
2471
|
};
|
|
2434
2472
|
const cancelComposer = () => {
|
|
2435
2473
|
setMode({ kind: "idle" });
|
|
@@ -2476,93 +2514,102 @@ function FeedbackWidget({
|
|
|
2476
2514
|
} : null;
|
|
2477
2515
|
return /* @__PURE__ */ u3("div", { className: "lfb-root", style: rootStyle, children: [
|
|
2478
2516
|
/* @__PURE__ */ u3("div", { ...overlayProps, className: "lfb-doc-layer", children: mode.kind === "composing" && /* @__PURE__ */ u3(S, { children: [
|
|
2517
|
+
/* @__PURE__ */ u3("div", { className: "lfb-shield", "aria-hidden": "true" }),
|
|
2479
2518
|
/* @__PURE__ */ u3("div", { className: "lfb-rect", style: { left: mode.rect.x, top: mode.rect.y, width: mode.rect.width, height: mode.rect.height } }),
|
|
2480
|
-
/* @__PURE__ */ u3(
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2519
|
+
/* @__PURE__ */ u3(
|
|
2520
|
+
"div",
|
|
2521
|
+
{
|
|
2522
|
+
className: "lfb-anchor",
|
|
2523
|
+
style: { left: mode.anchorX, top: mode.placement === "below" ? mode.rect.y + mode.rect.height : mode.rect.y },
|
|
2524
|
+
children: /* @__PURE__ */ u3("form", { className: `lfb-card lfb-composer${mode.placement === "above" ? " lfb-composer--above" : ""}`, onSubmit: handleSubmit, children: [
|
|
2525
|
+
/* @__PURE__ */ u3("div", { className: "lfb-row", children: [
|
|
2526
|
+
/* @__PURE__ */ u3("span", { className: "lfb-eyebrow", children: "New feedback" }),
|
|
2527
|
+
/* @__PURE__ */ u3("button", { type: "button", className: "lfb-iconbtn", "aria-label": "Cancel", onClick: cancelComposer, children: /* @__PURE__ */ u3(XIcon, {}) })
|
|
2528
|
+
] }),
|
|
2529
|
+
/* @__PURE__ */ u3("div", { style: { marginTop: 12 }, children: [
|
|
2530
|
+
/* @__PURE__ */ u3("span", { className: "lfb-field-label", children: "Issue type" }),
|
|
2531
|
+
/* @__PURE__ */ u3("div", { className: "lfb-types", children: types.map((t3) => {
|
|
2532
|
+
const Icon = TYPE_ICONS[t3.icon ?? "dot"];
|
|
2533
|
+
return /* @__PURE__ */ u3(
|
|
2534
|
+
"button",
|
|
2535
|
+
{
|
|
2536
|
+
type: "button",
|
|
2537
|
+
className: "lfb-type",
|
|
2538
|
+
"aria-pressed": issueType === t3.id,
|
|
2539
|
+
onClick: () => setIssueType(t3.id),
|
|
2540
|
+
children: [
|
|
2541
|
+
/* @__PURE__ */ u3("span", { className: "lfb-swatch", style: { background: t3.color }, children: /* @__PURE__ */ u3(Icon, { size: 14 }) }),
|
|
2542
|
+
t3.label
|
|
2543
|
+
]
|
|
2544
|
+
},
|
|
2545
|
+
t3.id
|
|
2546
|
+
);
|
|
2547
|
+
}) })
|
|
2548
|
+
] }),
|
|
2549
|
+
/* @__PURE__ */ u3(
|
|
2550
|
+
"textarea",
|
|
2491
2551
|
{
|
|
2492
|
-
|
|
2493
|
-
className: "lfb-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
{
|
|
2508
|
-
ref: textareaRef,
|
|
2509
|
-
className: "lfb-textarea",
|
|
2510
|
-
placeholder: "What's on your mind?",
|
|
2511
|
-
value: text,
|
|
2512
|
-
onChange: (e3) => setText(e3.target.value),
|
|
2513
|
-
maxLength: 5e3,
|
|
2514
|
-
required: true,
|
|
2515
|
-
disabled: submitting,
|
|
2516
|
-
rows: 3,
|
|
2517
|
-
onKeyDown: (e3) => {
|
|
2518
|
-
if (e3.key === "Enter" && (e3.metaKey || e3.ctrlKey)) {
|
|
2519
|
-
e3.preventDefault();
|
|
2520
|
-
handleSubmit(e3);
|
|
2552
|
+
ref: textareaRef,
|
|
2553
|
+
className: "lfb-textarea",
|
|
2554
|
+
placeholder: "What's on your mind?",
|
|
2555
|
+
value: text,
|
|
2556
|
+
onChange: (e3) => setText(e3.target.value),
|
|
2557
|
+
maxLength: 5e3,
|
|
2558
|
+
required: true,
|
|
2559
|
+
disabled: submitting,
|
|
2560
|
+
rows: 3,
|
|
2561
|
+
onKeyDown: (e3) => {
|
|
2562
|
+
if (e3.key === "Enter" && (e3.metaKey || e3.ctrlKey)) {
|
|
2563
|
+
e3.preventDefault();
|
|
2564
|
+
handleSubmit(e3);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2521
2567
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2568
|
+
),
|
|
2569
|
+
editingName ? /* @__PURE__ */ u3(
|
|
2570
|
+
"input",
|
|
2571
|
+
{
|
|
2572
|
+
ref: nameEditRef,
|
|
2573
|
+
className: "lfb-input",
|
|
2574
|
+
type: "text",
|
|
2575
|
+
value: name,
|
|
2576
|
+
maxLength: 80,
|
|
2577
|
+
onChange: (e3) => setName(e3.target.value),
|
|
2578
|
+
onBlur: () => {
|
|
2579
|
+
const t3 = name.trim();
|
|
2580
|
+
if (t3) persistName(t3);
|
|
2581
|
+
setEditingName(false);
|
|
2582
|
+
},
|
|
2583
|
+
onKeyDown: (e3) => {
|
|
2584
|
+
if (e3.key === "Enter") {
|
|
2585
|
+
e3.preventDefault();
|
|
2586
|
+
e3.target.blur();
|
|
2587
|
+
}
|
|
2588
|
+
if (e3.key === "Escape") {
|
|
2589
|
+
e3.preventDefault();
|
|
2590
|
+
e3.stopPropagation();
|
|
2591
|
+
setEditingName(false);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2547
2594
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2595
|
+
) : /* @__PURE__ */ u3("div", { className: "lfb-namerow", children: [
|
|
2596
|
+
/* @__PURE__ */ u3("span", { children: [
|
|
2597
|
+
"Posting as ",
|
|
2598
|
+
/* @__PURE__ */ u3("span", { className: "lfb-name", children: name || "anonymous" })
|
|
2599
|
+
] }),
|
|
2600
|
+
/* @__PURE__ */ u3("button", { type: "button", className: "lfb-link", onClick: () => setEditingName(true), children: [
|
|
2601
|
+
/* @__PURE__ */ u3(EditIcon, { size: 12 }),
|
|
2602
|
+
"change"
|
|
2603
|
+
] })
|
|
2604
|
+
] }),
|
|
2605
|
+
error && /* @__PURE__ */ u3("p", { className: "lfb-error", children: error }),
|
|
2606
|
+
/* @__PURE__ */ u3("div", { className: "lfb-actions", children: [
|
|
2607
|
+
/* @__PURE__ */ u3("button", { type: "button", className: "lfb-btn lfb-btn-ghost", onClick: cancelComposer, disabled: submitting, children: "Cancel" }),
|
|
2608
|
+
/* @__PURE__ */ u3("button", { type: "submit", className: "lfb-btn lfb-btn-primary", disabled: submitting || !text.trim(), children: submitting ? "Sending\u2026" : "Send to Linear" })
|
|
2609
|
+
] })
|
|
2558
2610
|
] })
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
/* @__PURE__ */ u3("div", { className: "lfb-actions", children: [
|
|
2562
|
-
/* @__PURE__ */ u3("button", { type: "button", className: "lfb-btn lfb-btn-ghost", onClick: cancelComposer, disabled: submitting, children: "Cancel" }),
|
|
2563
|
-
/* @__PURE__ */ u3("button", { type: "submit", className: "lfb-btn lfb-btn-primary", disabled: submitting || !text.trim(), children: submitting ? "Sending\u2026" : "Send to Linear" })
|
|
2564
|
-
] })
|
|
2565
|
-
] }) })
|
|
2611
|
+
}
|
|
2612
|
+
)
|
|
2566
2613
|
] }) }),
|
|
2567
2614
|
/* @__PURE__ */ u3("div", { ...overlayProps, className: "lfb-fixed-layer", children: [
|
|
2568
2615
|
mode.kind === "drawing" && /* @__PURE__ */ u3("div", { className: "lfb-draw", role: "presentation", onMouseDown: onDrawMouseDown, onMouseMove: onDrawMouseMove, onMouseUp: onDrawMouseUp, children: [
|