react-linear-feedback 0.1.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.
@@ -0,0 +1,627 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var modernScreenshot = require('modern-screenshot');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/react/feedback-gate.tsx
9
+
10
+ // src/react/anchor.ts
11
+ var MAX_DEPTH = 5;
12
+ var OVERLAY_ATTR = "data-feedback-overlay";
13
+ function escapeForSelector(value) {
14
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
15
+ return value.replace(/[^\w-]/g, (c) => `\\${c}`);
16
+ }
17
+ function getStableSelector(el, maxDepth = MAX_DEPTH) {
18
+ const parts = [];
19
+ let cur = el;
20
+ while (cur && parts.length < maxDepth && cur !== document.body && cur !== document.documentElement) {
21
+ const testid = cur.getAttribute("data-testid");
22
+ if (testid) {
23
+ parts.unshift(`[data-testid="${escapeForSelector(testid)}"]`);
24
+ return parts.join(" > ");
25
+ }
26
+ if (cur.id) {
27
+ parts.unshift(`#${escapeForSelector(cur.id)}`);
28
+ return parts.join(" > ");
29
+ }
30
+ let part = cur.tagName.toLowerCase();
31
+ const parent = cur.parentElement;
32
+ if (parent) {
33
+ const sameTag = Array.from(parent.children).filter((s) => s.tagName === cur.tagName);
34
+ if (sameTag.length > 1) {
35
+ part += `:nth-of-type(${sameTag.indexOf(cur) + 1})`;
36
+ }
37
+ }
38
+ parts.unshift(part);
39
+ cur = parent;
40
+ }
41
+ return parts.join(" > ");
42
+ }
43
+ function elementBelowOverlay(clientX, clientY) {
44
+ const stack = document.elementsFromPoint(clientX, clientY);
45
+ return stack.find((el) => !el.closest(`[${OVERLAY_ATTR}]`)) ?? null;
46
+ }
47
+ function describeElementAt(clientX, clientY) {
48
+ const el = elementBelowOverlay(clientX, clientY);
49
+ if (!el) return null;
50
+ return getStableSelector(el) || null;
51
+ }
52
+ var FEEDBACK_OVERLAY_ATTR = OVERLAY_ATTR;
53
+ var MAX_SIDE = 1600;
54
+ async function submitFeedback(annotation, opts = {}) {
55
+ const endpoint = opts.endpoint ?? "/api/feedback";
56
+ const context = collectContext(annotation);
57
+ const screenshot = await captureAnnotatedScreenshot(annotation);
58
+ const payload = { annotation, context, screenshot };
59
+ try {
60
+ const res = await fetch(endpoint, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(payload)
64
+ });
65
+ if (!res.ok) {
66
+ const detail = await res.text().catch(() => "");
67
+ console.error("[feedback] submit failed", res.status, detail);
68
+ return null;
69
+ }
70
+ return await res.json();
71
+ } catch (err) {
72
+ console.error("[feedback] submit error", err);
73
+ return null;
74
+ }
75
+ }
76
+ async function captureAnnotatedScreenshot(annotation) {
77
+ try {
78
+ const docEl = document.documentElement;
79
+ const pageW = docEl.scrollWidth;
80
+ const pageH = docEl.scrollHeight;
81
+ const vw = window.innerWidth;
82
+ const vh = window.innerHeight;
83
+ const scale = Math.min(1, MAX_SIDE / Math.max(vw, vh));
84
+ const full = await modernScreenshot.domToCanvas(document.body, {
85
+ scale,
86
+ backgroundColor: "#ffffff",
87
+ // Exclude the feedback overlay itself (carries data-feedback-overlay → dataset.feedbackOverlay).
88
+ filter: (node) => !(node instanceof HTMLElement && node.dataset.feedbackOverlay !== void 0)
89
+ });
90
+ const ratioX = full.width / pageW;
91
+ const ratioY = full.height / pageH;
92
+ const out = document.createElement("canvas");
93
+ out.width = Math.max(1, Math.round(vw * ratioX));
94
+ out.height = Math.max(1, Math.round(vh * ratioY));
95
+ const ctx = out.getContext("2d");
96
+ if (ctx) {
97
+ ctx.drawImage(full, window.scrollX * ratioX, window.scrollY * ratioY, vw * ratioX, vh * ratioY, 0, 0, out.width, out.height);
98
+ const { x, y, width, height } = annotation.rect;
99
+ const rx = (x - window.scrollX) * ratioX;
100
+ const ry = (y - window.scrollY) * ratioY;
101
+ ctx.lineWidth = Math.max(2, 3 * ratioX);
102
+ ctx.strokeStyle = "#ff0055";
103
+ ctx.fillStyle = "rgba(255, 0, 85, 0.12)";
104
+ ctx.fillRect(rx, ry, width * ratioX, height * ratioY);
105
+ ctx.strokeRect(rx, ry, width * ratioX, height * ratioY);
106
+ }
107
+ return out.toDataURL("image/jpeg", 0.85);
108
+ } catch (err) {
109
+ console.warn("[feedback] screenshot capture failed, sending without image", err);
110
+ return null;
111
+ }
112
+ }
113
+ function collectContext(annotation) {
114
+ const viewportX = annotation.rect.x + annotation.rect.width / 2 - window.scrollX;
115
+ const viewportY = annotation.rect.y + annotation.rect.height / 2 - window.scrollY;
116
+ return {
117
+ url: window.location.href,
118
+ pathname: window.location.pathname,
119
+ title: document.title,
120
+ viewport: { width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio },
121
+ scroll: { x: window.scrollX, y: window.scrollY },
122
+ userAgent: navigator.userAgent,
123
+ referrer: document.referrer,
124
+ elementHint: describeElementAt(viewportX, viewportY),
125
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
126
+ };
127
+ }
128
+ function Svg({ size = 16, children, ...props }) {
129
+ return /* @__PURE__ */ jsxRuntime.jsx(
130
+ "svg",
131
+ {
132
+ width: size,
133
+ height: size,
134
+ viewBox: "0 0 24 24",
135
+ fill: "none",
136
+ stroke: "currentColor",
137
+ strokeWidth: 2,
138
+ strokeLinecap: "round",
139
+ strokeLinejoin: "round",
140
+ "aria-hidden": "true",
141
+ ...props,
142
+ children
143
+ }
144
+ );
145
+ }
146
+ var MessageIcon = (p) => /* @__PURE__ */ jsxRuntime.jsx(Svg, { ...p, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8z" }) });
147
+ var XIcon = (p) => /* @__PURE__ */ jsxRuntime.jsx(Svg, { ...p, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
148
+ var EditIcon = (p) => /* @__PURE__ */ jsxRuntime.jsxs(Svg, { ...p, children: [
149
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 20h9" }),
150
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" })
151
+ ] });
152
+ var CheckCircleIcon = (p) => /* @__PURE__ */ jsxRuntime.jsxs(Svg, { ...p, children: [
153
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
154
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 4 12 14.01l-3-3" })
155
+ ] });
156
+ var AlertTriangleIcon = (p) => /* @__PURE__ */ jsxRuntime.jsxs(Svg, { ...p, children: [
157
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
158
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4" }),
159
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 17h.01" })
160
+ ] });
161
+ var ArrowUpIcon = (p) => /* @__PURE__ */ jsxRuntime.jsx(Svg, { ...p, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
162
+ var DotIcon = (p) => /* @__PURE__ */ jsxRuntime.jsx(Svg, { ...p, strokeWidth: 0, children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "6", fill: "currentColor" }) });
163
+ var TYPE_ICONS = {
164
+ bug: AlertTriangleIcon,
165
+ improvement: ArrowUpIcon,
166
+ dot: DotIcon
167
+ };
168
+
169
+ // src/react/styles.ts
170
+ var STYLE_ID = "lfb-styles";
171
+ var CSS2 = `
172
+ .lfb-doc-layer, .lfb-fixed-layer {
173
+ --lfb-brand: #6366f1;
174
+ --lfb-fg: #181d27;
175
+ --lfb-fg-secondary: #414651;
176
+ --lfb-fg-tertiary: #717680;
177
+ --lfb-surface: #ffffff;
178
+ --lfb-surface-hover: #f5f5f5;
179
+ --lfb-border: #e9eaeb;
180
+ --lfb-radius: 12px;
181
+ --lfb-rect: #ff0055;
182
+ --lfb-z: 2147483640;
183
+ --lfb-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
184
+ }
185
+ .lfb-doc-layer, .lfb-doc-layer *, .lfb-fixed-layer, .lfb-fixed-layer * { box-sizing: border-box; }
186
+
187
+ .lfb-doc-layer { position: absolute; inset-inline: 0; top: 0; pointer-events: none; z-index: var(--lfb-z); }
188
+ .lfb-fixed-layer { position: fixed; inset: 0; pointer-events: none; z-index: calc(var(--lfb-z) + 1); }
189
+
190
+ .lfb-rect { position: absolute; border: 2px solid var(--lfb-rect); background: rgba(255,0,85,0.12); border-radius: 3px; pointer-events: none; }
191
+
192
+ .lfb-anchor { position: absolute; pointer-events: auto; }
193
+
194
+ .lfb-card {
195
+ background: var(--lfb-surface);
196
+ color: var(--lfb-fg);
197
+ border: 1px solid var(--lfb-border);
198
+ border-radius: var(--lfb-radius);
199
+ box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 2px 6px rgba(0,0,0,0.08);
200
+ padding: 12px;
201
+ font-family: var(--lfb-font);
202
+ font-size: 14px;
203
+ line-height: 1.4;
204
+ }
205
+ .lfb-composer { position: absolute; top: 100%; left: 0; margin-top: 8px; width: 320px; max-width: calc(100vw - 32px); }
206
+
207
+ .lfb-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
208
+ .lfb-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--lfb-fg-tertiary); }
209
+ .lfb-sub { margin: 4px 0 0; font-size: 12px; color: var(--lfb-fg-tertiary); }
210
+
211
+ .lfb-iconbtn { display: inline-flex; align-items: center; justify-content: center; padding: 4px; margin: -4px; border: 0; background: none; color: var(--lfb-fg-tertiary); border-radius: 6px; cursor: pointer; }
212
+ .lfb-iconbtn:hover { background: var(--lfb-surface-hover); color: var(--lfb-fg); }
213
+
214
+ .lfb-field-label { font-size: 12px; font-weight: 500; color: var(--lfb-fg-secondary); }
215
+ .lfb-types { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 6px; }
216
+ .lfb-type { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; font-size: 14px; font-weight: 500; font-family: inherit; background: var(--lfb-surface); color: var(--lfb-fg-secondary); border: 1px solid var(--lfb-border); cursor: pointer; transition: background 0.1s, box-shadow 0.1s, border-color 0.1s; }
217
+ .lfb-type:hover { background: var(--lfb-surface-hover); }
218
+ .lfb-type[aria-pressed="true"] { color: var(--lfb-fg); border-color: var(--lfb-brand); box-shadow: inset 0 0 0 1px var(--lfb-brand); }
219
+ .lfb-swatch { width: 20px; height: 20px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; color: #fff; flex-shrink: 0; }
220
+
221
+ .lfb-textarea, .lfb-input {
222
+ width: 100%; margin-top: 12px; border-radius: 8px; background: var(--lfb-surface); color: var(--lfb-fg);
223
+ border: 1px solid var(--lfb-border); padding: 8px 10px; font-size: 14px; font-family: inherit; outline: none;
224
+ }
225
+ .lfb-textarea { resize: none; }
226
+ .lfb-input { margin-top: 8px; font-size: 13px; }
227
+ .lfb-textarea:focus, .lfb-input:focus { border-color: var(--lfb-brand); box-shadow: 0 0 0 2px color-mix(in srgb, var(--lfb-brand) 30%, transparent); }
228
+ .lfb-textarea::placeholder, .lfb-input::placeholder { color: var(--lfb-fg-tertiary); }
229
+ .lfb-textarea:disabled { opacity: 0.5; }
230
+
231
+ .lfb-namerow { margin-top: 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--lfb-fg-tertiary); }
232
+ .lfb-name { color: var(--lfb-fg-secondary); font-weight: 600; }
233
+ .lfb-link { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px; border: 0; background: none; color: var(--lfb-brand); font-weight: 500; font-size: 12px; font-family: inherit; cursor: pointer; border-radius: 4px; }
234
+ .lfb-link:hover { background: var(--lfb-surface-hover); }
235
+
236
+ .lfb-error { margin-top: 8px; font-size: 12px; color: #d92d20; }
237
+ .lfb-actions { margin-top: 12px; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
238
+ .lfb-btn { padding: 6px 12px; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer; border: 0; transition: background 0.1s; }
239
+ .lfb-btn:disabled { opacity: 0.5; cursor: not-allowed; }
240
+ .lfb-btn-ghost { background: none; color: var(--lfb-fg-secondary); }
241
+ .lfb-btn-ghost:hover:not(:disabled) { background: var(--lfb-surface-hover); }
242
+ .lfb-btn-primary { background: var(--lfb-brand); color: #fff; }
243
+ .lfb-btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--lfb-brand) 88%, black); }
244
+
245
+ .lfb-draw { position: absolute; inset: 0; pointer-events: auto; cursor: crosshair; background: rgba(0,0,0,0.04); user-select: none; }
246
+ .lfb-hint { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); background: #181d27; color: #fff; padding: 8px 16px; border-radius: 9999px; font-size: 12px; font-weight: 500; font-family: var(--lfb-font); box-shadow: 0 8px 20px rgba(0,0,0,0.25); white-space: nowrap; }
247
+
248
+ .lfb-stack { position: absolute; bottom: 16px; right: 16px; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; pointer-events: none; }
249
+ .lfb-stack > * { pointer-events: auto; }
250
+ .lfb-stack--bottom-left { right: auto; left: 16px; align-items: flex-start; }
251
+ .lfb-stack--top-right { bottom: auto; top: 16px; }
252
+ .lfb-stack--top-left { bottom: auto; top: 16px; right: auto; left: 16px; align-items: flex-start; }
253
+
254
+ .lfb-fab { display: inline-flex; align-items: center; gap: 8px; border: 0; border-radius: 9999px; padding: 12px 16px; font-size: 14px; font-weight: 600; font-family: var(--lfb-font); cursor: pointer; background: var(--lfb-brand); color: #fff; box-shadow: 0 10px 25px rgba(0,0,0,0.18); transition: transform 0.1s, background 0.1s; }
255
+ .lfb-fab:hover { transform: scale(1.05); background: color-mix(in srgb, var(--lfb-brand) 88%, black); }
256
+ .lfb-fab--active { background: var(--lfb-surface); color: var(--lfb-fg); border: 1px solid var(--lfb-border); }
257
+ .lfb-fab--active:hover { background: var(--lfb-surface-hover); }
258
+
259
+ .lfb-toast { display: flex; align-items: flex-start; gap: 10px; width: 300px; max-width: calc(100vw - 32px); }
260
+ .lfb-toast-icon { color: #17b26a; flex-shrink: 0; margin-top: 1px; }
261
+ .lfb-toast-body { min-width: 0; flex: 1; }
262
+ .lfb-toast-title { font-size: 14px; font-weight: 600; color: var(--lfb-fg); }
263
+ .lfb-toast-text { margin-top: 2px; font-size: 12px; color: var(--lfb-fg-tertiary); }
264
+ .lfb-toast-link { color: var(--lfb-brand); font-weight: 500; text-decoration: none; }
265
+ .lfb-toast-link:hover { text-decoration: underline; }
266
+ `;
267
+ function ensureStyles() {
268
+ if (typeof document === "undefined") return;
269
+ if (document.getElementById(STYLE_ID)) return;
270
+ const style = document.createElement("style");
271
+ style.id = STYLE_ID;
272
+ style.textContent = CSS2;
273
+ document.head.appendChild(style);
274
+ }
275
+ var MIN_DRAG = 12;
276
+ var DEFAULT_BOX = { width: 220, height: 130 };
277
+ var DEFAULT_TYPES = [
278
+ { id: "bug", label: "Bug", color: "#ef4444", icon: "bug" },
279
+ { id: "improvement", label: "Improvement", color: "#22c55e", icon: "improvement" }
280
+ ];
281
+ function FeedbackWidget({
282
+ endpoint,
283
+ brandColor,
284
+ position = "bottom-right",
285
+ types = DEFAULT_TYPES,
286
+ nameRequired = true,
287
+ nameStorageKey = "wh_feedback_name",
288
+ fabLabel = "Give feedback"
289
+ }) {
290
+ const [mode, setMode] = react.useState({ kind: "idle" });
291
+ const [name, setName] = react.useState("");
292
+ const [nameDraft, setNameDraft] = react.useState("");
293
+ const [editingName, setEditingName] = react.useState(false);
294
+ const [text, setText] = react.useState("");
295
+ const [issueType, setIssueType] = react.useState(types[0]?.id ?? "bug");
296
+ const [submitting, setSubmitting] = react.useState(false);
297
+ const [error, setError] = react.useState(null);
298
+ const [result, setResult] = react.useState(null);
299
+ const [drag, setDrag] = react.useState(null);
300
+ const textareaRef = react.useRef(null);
301
+ const nameInputRef = react.useRef(null);
302
+ react.useEffect(() => {
303
+ ensureStyles();
304
+ try {
305
+ const stored = window.localStorage.getItem(nameStorageKey);
306
+ if (stored) setName(stored);
307
+ } catch {
308
+ }
309
+ }, [nameStorageKey]);
310
+ react.useEffect(() => {
311
+ if (mode.kind !== "drawing" && mode.kind !== "naming") return;
312
+ const onKey = (e) => {
313
+ if (e.key === "Escape") {
314
+ setMode({ kind: "idle" });
315
+ setDrag(null);
316
+ }
317
+ };
318
+ document.addEventListener("keydown", onKey);
319
+ return () => document.removeEventListener("keydown", onKey);
320
+ }, [mode.kind]);
321
+ react.useEffect(() => {
322
+ if (mode.kind === "composing") {
323
+ const t = window.setTimeout(() => textareaRef.current?.focus(), 0);
324
+ return () => window.clearTimeout(t);
325
+ }
326
+ if (mode.kind === "naming") {
327
+ const t = window.setTimeout(() => nameInputRef.current?.focus(), 0);
328
+ return () => window.clearTimeout(t);
329
+ }
330
+ }, [mode.kind]);
331
+ const persistName = (value) => {
332
+ try {
333
+ window.localStorage.setItem(nameStorageKey, value);
334
+ } catch {
335
+ }
336
+ };
337
+ const startFlow = () => {
338
+ if (mode.kind === "drawing" || mode.kind === "naming") {
339
+ setMode({ kind: "idle" });
340
+ setDrag(null);
341
+ return;
342
+ }
343
+ setResult(null);
344
+ if (nameRequired && !name.trim()) {
345
+ setNameDraft("");
346
+ setMode({ kind: "naming" });
347
+ } else {
348
+ setMode({ kind: "drawing" });
349
+ }
350
+ };
351
+ const confirmName = (e) => {
352
+ e.preventDefault();
353
+ const t = nameDraft.trim();
354
+ if (!t) return;
355
+ setName(t);
356
+ persistName(t);
357
+ setMode({ kind: "drawing" });
358
+ };
359
+ const onDrawMouseDown = (e) => {
360
+ if (mode.kind !== "drawing") return;
361
+ e.preventDefault();
362
+ const p = { x: e.clientX, y: e.clientY };
363
+ setDrag({ start: p, current: p });
364
+ };
365
+ const onDrawMouseMove = (e) => {
366
+ if (mode.kind !== "drawing" || !drag) return;
367
+ setDrag((d) => d ? { ...d, current: { x: e.clientX, y: e.clientY } } : d);
368
+ };
369
+ const onDrawMouseUp = (e) => {
370
+ if (mode.kind !== "drawing" || !drag) return;
371
+ const start = drag.start;
372
+ const end = { x: e.clientX, y: e.clientY };
373
+ let vx = Math.min(start.x, end.x);
374
+ let vy = Math.min(start.y, end.y);
375
+ let vw = Math.abs(end.x - start.x);
376
+ let vh = Math.abs(end.y - start.y);
377
+ if (vw < MIN_DRAG || vh < MIN_DRAG) {
378
+ vw = Math.max(vw, DEFAULT_BOX.width);
379
+ vh = Math.max(vh, DEFAULT_BOX.height);
380
+ vx = end.x - vw / 2;
381
+ vy = end.y - vh / 2;
382
+ }
383
+ const rect = { x: Math.max(0, vx + window.scrollX), y: Math.max(0, vy + window.scrollY), width: vw, height: vh };
384
+ setDrag(null);
385
+ setText("");
386
+ setIssueType(types[0]?.id ?? "bug");
387
+ setError(null);
388
+ setEditingName(false);
389
+ setMode({ kind: "composing", rect });
390
+ };
391
+ const cancelComposer = () => {
392
+ setMode({ kind: "idle" });
393
+ setText("");
394
+ setError(null);
395
+ setEditingName(false);
396
+ setSubmitting(false);
397
+ };
398
+ const handleSubmit = async (e) => {
399
+ e.preventDefault();
400
+ if (mode.kind !== "composing" || submitting) return;
401
+ const trimmedNote = text.trim();
402
+ if (!trimmedNote) return;
403
+ const trimmedName = name.trim();
404
+ const selected = types.find((t) => t.id === issueType);
405
+ setSubmitting(true);
406
+ setError(null);
407
+ if (trimmedName) persistName(trimmedName);
408
+ const res = await submitFeedback(
409
+ { rect: mode.rect, note: trimmedNote, type: issueType, typeLabel: selected?.label, name: trimmedName || void 0 },
410
+ { endpoint }
411
+ );
412
+ setSubmitting(false);
413
+ if (res) {
414
+ setResult(res);
415
+ setText("");
416
+ setEditingName(false);
417
+ setMode({ kind: "idle" });
418
+ } else {
419
+ setError("Couldn't send \u2014 please try again.");
420
+ }
421
+ };
422
+ const rootStyle = { display: "contents", ...brandColor ? { "--lfb-brand": brandColor } : {} };
423
+ const overlayProps = { [FEEDBACK_OVERLAY_ATTR]: "" };
424
+ const live = drag ? {
425
+ left: Math.min(drag.start.x, drag.current.x),
426
+ top: Math.min(drag.start.y, drag.current.y),
427
+ width: Math.abs(drag.current.x - drag.start.x),
428
+ height: Math.abs(drag.current.y - drag.start.y)
429
+ } : null;
430
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-root", style: rootStyle, children: [
431
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ...overlayProps, className: "lfb-doc-layer", children: mode.kind === "composing" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
432
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-rect", style: { left: mode.rect.x, top: mode.rect.y, width: mode.rect.width, height: mode.rect.height } }),
433
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-anchor", style: { left: mode.rect.x, top: mode.rect.y + mode.rect.height }, children: /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "lfb-card lfb-composer", onSubmit: handleSubmit, children: [
434
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-row", children: [
435
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-eyebrow", children: "New feedback" }),
436
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lfb-iconbtn", "aria-label": "Cancel", onClick: cancelComposer, children: /* @__PURE__ */ jsxRuntime.jsx(XIcon, {}) })
437
+ ] }),
438
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12 }, children: [
439
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-field-label", children: "Issue type" }),
440
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-types", children: types.map((t) => {
441
+ const Icon = TYPE_ICONS[t.icon ?? "dot"];
442
+ return /* @__PURE__ */ jsxRuntime.jsxs(
443
+ "button",
444
+ {
445
+ type: "button",
446
+ className: "lfb-type",
447
+ "aria-pressed": issueType === t.id,
448
+ onClick: () => setIssueType(t.id),
449
+ children: [
450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-swatch", style: { background: t.color }, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 14 }) }),
451
+ t.label
452
+ ]
453
+ },
454
+ t.id
455
+ );
456
+ }) })
457
+ ] }),
458
+ /* @__PURE__ */ jsxRuntime.jsx(
459
+ "textarea",
460
+ {
461
+ ref: textareaRef,
462
+ className: "lfb-textarea",
463
+ placeholder: "What's on your mind?",
464
+ value: text,
465
+ onChange: (e) => setText(e.target.value),
466
+ maxLength: 5e3,
467
+ required: true,
468
+ disabled: submitting,
469
+ rows: 3,
470
+ onKeyDown: (e) => {
471
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
472
+ e.preventDefault();
473
+ handleSubmit(e);
474
+ }
475
+ }
476
+ }
477
+ ),
478
+ editingName ? /* @__PURE__ */ jsxRuntime.jsx(
479
+ "input",
480
+ {
481
+ className: "lfb-input",
482
+ type: "text",
483
+ value: name,
484
+ autoFocus: true,
485
+ maxLength: 80,
486
+ onChange: (e) => setName(e.target.value),
487
+ onBlur: () => {
488
+ const t = name.trim();
489
+ if (t) persistName(t);
490
+ setEditingName(false);
491
+ },
492
+ onKeyDown: (e) => {
493
+ if (e.key === "Enter") {
494
+ e.preventDefault();
495
+ e.target.blur();
496
+ }
497
+ if (e.key === "Escape") {
498
+ e.preventDefault();
499
+ setEditingName(false);
500
+ }
501
+ }
502
+ }
503
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-namerow", children: [
504
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
505
+ "Posting as ",
506
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-name", children: name || "anonymous" })
507
+ ] }),
508
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "lfb-link", onClick: () => setEditingName(true), children: [
509
+ /* @__PURE__ */ jsxRuntime.jsx(EditIcon, { size: 12 }),
510
+ "change"
511
+ ] })
512
+ ] }),
513
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lfb-error", children: error }),
514
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-actions", children: [
515
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lfb-btn lfb-btn-ghost", onClick: cancelComposer, disabled: submitting, children: "Cancel" }),
516
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "lfb-btn lfb-btn-primary", disabled: submitting || !text.trim(), children: submitting ? "Sending\u2026" : "Send to Linear" })
517
+ ] })
518
+ ] }) })
519
+ ] }) }),
520
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ...overlayProps, className: "lfb-fixed-layer", children: [
521
+ mode.kind === "drawing" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-draw", role: "presentation", onMouseDown: onDrawMouseDown, onMouseMove: onDrawMouseMove, onMouseUp: onDrawMouseUp, children: [
522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-hint", children: "Drag to select an area \xB7 Esc to cancel" }),
523
+ live && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-rect", style: live })
524
+ ] }),
525
+ mode.kind !== "composing" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `lfb-stack${position === "bottom-right" ? "" : ` lfb-stack--${position}`}`, children: [
526
+ mode.kind === "naming" && /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "lfb-card", style: { width: 288, maxWidth: "calc(100vw - 32px)" }, onSubmit: confirmName, children: [
527
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-row", children: [
528
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-eyebrow", children: "What's your name?" }),
529
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lfb-iconbtn", "aria-label": "Cancel", onClick: () => setMode({ kind: "idle" }), children: /* @__PURE__ */ jsxRuntime.jsx(XIcon, {}) })
530
+ ] }),
531
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lfb-sub", children: "Saved locally so you don't have to type it again." }),
532
+ /* @__PURE__ */ jsxRuntime.jsx(
533
+ "input",
534
+ {
535
+ ref: nameInputRef,
536
+ className: "lfb-input",
537
+ type: "text",
538
+ placeholder: "Olivia Rhye",
539
+ value: nameDraft,
540
+ maxLength: 80,
541
+ required: true,
542
+ onChange: (e) => setNameDraft(e.target.value)
543
+ }
544
+ ),
545
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-actions", children: [
546
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lfb-btn lfb-btn-ghost", onClick: () => setMode({ kind: "idle" }), children: "Cancel" }),
547
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "lfb-btn lfb-btn-primary", disabled: !nameDraft.trim(), children: "Continue" })
548
+ ] })
549
+ ] }),
550
+ result && mode.kind === "idle" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-card lfb-toast", children: [
551
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lfb-toast-icon", children: /* @__PURE__ */ jsxRuntime.jsx(CheckCircleIcon, { size: 20 }) }),
552
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-toast-body", children: [
553
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lfb-toast-title", children: "Feedback sent" }),
554
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lfb-toast-text", children: [
555
+ "Created ",
556
+ result.identifier ?? "an issue",
557
+ ".",
558
+ result.url && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
559
+ " ",
560
+ /* @__PURE__ */ jsxRuntime.jsx("a", { className: "lfb-toast-link", href: result.url, target: "_blank", rel: "noopener noreferrer", children: "View in Linear" })
561
+ ] })
562
+ ] })
563
+ ] }),
564
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lfb-iconbtn", "aria-label": "Dismiss", onClick: () => setResult(null), children: /* @__PURE__ */ jsxRuntime.jsx(XIcon, {}) })
565
+ ] }),
566
+ /* @__PURE__ */ jsxRuntime.jsx(
567
+ "button",
568
+ {
569
+ type: "button",
570
+ className: `lfb-fab${mode.kind === "idle" ? "" : " lfb-fab--active"}`,
571
+ "aria-label": mode.kind === "idle" ? fabLabel : "Cancel feedback",
572
+ onClick: startFlow,
573
+ children: mode.kind === "idle" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
574
+ /* @__PURE__ */ jsxRuntime.jsx(MessageIcon, { size: 18 }),
575
+ " ",
576
+ fabLabel
577
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
578
+ /* @__PURE__ */ jsxRuntime.jsx(XIcon, { size: 18 }),
579
+ " Cancel"
580
+ ] })
581
+ }
582
+ )
583
+ ] })
584
+ ] })
585
+ ] });
586
+ }
587
+ var DEFAULT_MAX_AGE = 60 * 60 * 24 * 90;
588
+ function writeCookie(name, value, enabled, maxAge) {
589
+ if (typeof document === "undefined") return;
590
+ document.cookie = enabled ? `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax` : `${name}=; path=/; max-age=0; SameSite=Lax`;
591
+ }
592
+ function readCookie(name, value) {
593
+ if (typeof document === "undefined") return false;
594
+ return document.cookie.split(";").map((c) => c.trim()).some((c) => c === `${name}=${value}`);
595
+ }
596
+ function FeedbackGate({
597
+ urlParam = "feedback",
598
+ cookieName = "wh_feedback",
599
+ cookieValue = "1",
600
+ cookieMaxAgeSeconds = DEFAULT_MAX_AGE,
601
+ ...widgetProps
602
+ }) {
603
+ const [enabled, setEnabled] = react.useState(false);
604
+ react.useEffect(() => {
605
+ if (typeof window === "undefined") return;
606
+ const params = new URLSearchParams(window.location.search);
607
+ const param = params.get(urlParam);
608
+ if (param != null) {
609
+ const turnOn = param !== "0";
610
+ writeCookie(cookieName, cookieValue, turnOn, cookieMaxAgeSeconds);
611
+ setEnabled(turnOn);
612
+ params.delete(urlParam);
613
+ const qs = params.toString();
614
+ const url = window.location.pathname + (qs ? `?${qs}` : "") + window.location.hash;
615
+ window.history.replaceState(null, "", url);
616
+ return;
617
+ }
618
+ setEnabled(readCookie(cookieName, cookieValue));
619
+ }, [urlParam, cookieName, cookieValue, cookieMaxAgeSeconds]);
620
+ if (!enabled) return null;
621
+ return /* @__PURE__ */ jsxRuntime.jsx(FeedbackWidget, { ...widgetProps });
622
+ }
623
+
624
+ exports.DEFAULT_TYPES = DEFAULT_TYPES;
625
+ exports.FeedbackGate = FeedbackGate;
626
+ exports.FeedbackWidget = FeedbackWidget;
627
+ exports.submitFeedback = submitFeedback;