@marktiderman/genesis-support 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +130 -2
- package/dist/block-fZjTS2sY.d.ts +39 -0
- package/dist/chunk-4VFYVQUF.js +278 -0
- package/dist/chunk-4VFYVQUF.js.map +1 -0
- package/dist/chunk-ZV4KJBUF.js +284 -0
- package/dist/chunk-ZV4KJBUF.js.map +1 -0
- package/dist/copy-CuTArBFg.d.ts +477 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +207 -0
- package/dist/native.js +722 -0
- package/dist/native.js.map +1 -0
- package/dist/web.d.ts +137 -0
- package/dist/web.js +484 -0
- package/dist/web.js.map +1 -0
- package/package.json +252 -3
package/dist/web.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { createContextCollector, cleanRoute, resolveSupportCopy, FEEDBACK_TYPES } from './chunk-ZV4KJBUF.js';
|
|
2
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
3
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent, Label, ToggleGroup, ToggleGroupItem, Textarea, Button, Alert, AlertDescription, Badge, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@marktiderman/genesis-ui';
|
|
4
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
|
|
7
|
+
// src/web/capture.ts
|
|
8
|
+
function canCaptureScreenshot() {
|
|
9
|
+
const devices = globalThis.navigator?.mediaDevices;
|
|
10
|
+
return typeof devices?.getDisplayMedia === "function";
|
|
11
|
+
}
|
|
12
|
+
async function captureScreenshot(doc = document) {
|
|
13
|
+
if (!canCaptureScreenshot()) return null;
|
|
14
|
+
let stream;
|
|
15
|
+
try {
|
|
16
|
+
stream = await navigator.mediaDevices.getDisplayMedia({
|
|
17
|
+
video: { preferCurrentTab: true, frameRate: 1 },
|
|
18
|
+
audio: false,
|
|
19
|
+
preferCurrentTab: true,
|
|
20
|
+
selfBrowserSurface: "include"
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const video = doc.createElement("video");
|
|
27
|
+
video.muted = true;
|
|
28
|
+
video.playsInline = true;
|
|
29
|
+
video.srcObject = stream;
|
|
30
|
+
await video.play();
|
|
31
|
+
await new Promise((resolve) => requestAnimationFrame(() => resolve(void 0)));
|
|
32
|
+
const canvas = doc.createElement("canvas");
|
|
33
|
+
canvas.width = video.videoWidth;
|
|
34
|
+
canvas.height = video.videoHeight;
|
|
35
|
+
const ctx = canvas.getContext("2d");
|
|
36
|
+
if (!ctx || !canvas.width || !canvas.height) return null;
|
|
37
|
+
ctx.drawImage(video, 0, 0);
|
|
38
|
+
return fromCanvas(canvas);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
} finally {
|
|
42
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var MAX_SCREENSHOT_SIDE = 2e3;
|
|
46
|
+
function fromCanvas(source) {
|
|
47
|
+
let canvas = source;
|
|
48
|
+
const scale = Math.min(1, MAX_SCREENSHOT_SIDE / Math.max(source.width, source.height));
|
|
49
|
+
if (scale < 1) {
|
|
50
|
+
canvas = source.ownerDocument.createElement("canvas");
|
|
51
|
+
canvas.width = Math.round(source.width * scale);
|
|
52
|
+
canvas.height = Math.round(source.height * scale);
|
|
53
|
+
canvas.getContext("2d")?.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
54
|
+
}
|
|
55
|
+
const webp = canvas.toDataURL("image/webp", 0.9);
|
|
56
|
+
if (webp.startsWith("data:image/webp")) return { dataUrl: webp, contentType: "image/webp", width: canvas.width, height: canvas.height };
|
|
57
|
+
return { dataUrl: canvas.toDataURL("image/jpeg", 0.88), contentType: "image/jpeg", width: canvas.width, height: canvas.height };
|
|
58
|
+
}
|
|
59
|
+
async function annotateScreenshot(capture, { strokes = [], redactions = [], penColor = "red", redactColor = "black" }, doc = document) {
|
|
60
|
+
const img = doc.createElement("img");
|
|
61
|
+
img.src = capture.dataUrl;
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
63
|
+
img.onload = () => resolve();
|
|
64
|
+
img.onerror = () => reject(new Error("image_load_failed"));
|
|
65
|
+
});
|
|
66
|
+
const canvas = doc.createElement("canvas");
|
|
67
|
+
canvas.width = img.naturalWidth;
|
|
68
|
+
canvas.height = img.naturalHeight;
|
|
69
|
+
const ctx = canvas.getContext("2d");
|
|
70
|
+
if (!ctx) return capture;
|
|
71
|
+
ctx.drawImage(img, 0, 0);
|
|
72
|
+
ctx.fillStyle = redactColor;
|
|
73
|
+
for (const r of redactions) ctx.fillRect(r.x * canvas.width, r.y * canvas.height, r.width * canvas.width, r.height * canvas.height);
|
|
74
|
+
ctx.strokeStyle = penColor;
|
|
75
|
+
ctx.lineWidth = Math.max(3, canvas.width / 300);
|
|
76
|
+
ctx.lineCap = "round";
|
|
77
|
+
ctx.lineJoin = "round";
|
|
78
|
+
for (const stroke of strokes) {
|
|
79
|
+
if (stroke.length < 2) continue;
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.moveTo(stroke[0].x * canvas.width, stroke[0].y * canvas.height);
|
|
82
|
+
for (const p of stroke.slice(1)) ctx.lineTo(p.x * canvas.width, p.y * canvas.height);
|
|
83
|
+
ctx.stroke();
|
|
84
|
+
}
|
|
85
|
+
return fromCanvas(canvas);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/web/pin.ts
|
|
89
|
+
var cssEscape = (value) => typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/[^A-Za-z0-9_-]/g, "\\$&");
|
|
90
|
+
function stableLocator(el) {
|
|
91
|
+
for (let node = el; node && node !== node.ownerDocument.documentElement; node = node.parentElement) {
|
|
92
|
+
if (node.id) return `#${cssEscape(node.id)}`;
|
|
93
|
+
const testId = node.getAttribute("data-testid");
|
|
94
|
+
if (testId) return `[data-testid="${testId.replace(/"/g, '\\"')}"]`;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function pickPin(doc = document) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const veil = doc.createElement("div");
|
|
101
|
+
veil.setAttribute("data-genesis-support-pin-picker", "");
|
|
102
|
+
veil.setAttribute("role", "presentation");
|
|
103
|
+
Object.assign(veil.style, { position: "fixed", inset: "0", zIndex: "2147483000", cursor: "crosshair", background: "hsl(var(--foreground) / 0.06)" });
|
|
104
|
+
const done = (pin) => {
|
|
105
|
+
veil.remove();
|
|
106
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
107
|
+
resolve(pin);
|
|
108
|
+
};
|
|
109
|
+
const onKey = (event) => {
|
|
110
|
+
if (event.key === "Escape") done(null);
|
|
111
|
+
};
|
|
112
|
+
veil.addEventListener("click", (event) => {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
event.stopPropagation();
|
|
115
|
+
veil.style.pointerEvents = "none";
|
|
116
|
+
const target = doc.elementFromPoint?.(event.clientX, event.clientY) ?? null;
|
|
117
|
+
const locator = stableLocator(target);
|
|
118
|
+
const anchor = locator ? doc.querySelector(locator) : null;
|
|
119
|
+
const rect = anchor?.getBoundingClientRect();
|
|
120
|
+
const view = doc.defaultView;
|
|
121
|
+
done({
|
|
122
|
+
locator,
|
|
123
|
+
xPct: rect && rect.width ? Math.round((event.clientX - rect.left) / rect.width * 100) : null,
|
|
124
|
+
yPct: rect && rect.height ? Math.round((event.clientY - rect.top) / rect.height * 100) : null,
|
|
125
|
+
viewportX: event.clientX,
|
|
126
|
+
viewportY: event.clientY,
|
|
127
|
+
scrollX: view?.scrollX ?? 0,
|
|
128
|
+
scrollY: view?.scrollY ?? 0
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
doc.addEventListener("keydown", onKey, true);
|
|
132
|
+
doc.body.appendChild(veil);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function resolvePin(doc, pin) {
|
|
136
|
+
if (pin.locator) {
|
|
137
|
+
try {
|
|
138
|
+
const node = doc.querySelector(pin.locator);
|
|
139
|
+
if (node) {
|
|
140
|
+
const r = node.getBoundingClientRect();
|
|
141
|
+
return { left: r.left + r.width * (pin.xPct ?? 50) / 100, top: r.top + r.height * (pin.yPct ?? 50) / 100 };
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { left: pin.viewportX, top: pin.viewportY };
|
|
147
|
+
}
|
|
148
|
+
function createPinOverlay(options) {
|
|
149
|
+
const doc = options.document ?? document;
|
|
150
|
+
const view = doc.defaultView;
|
|
151
|
+
const host = doc.createElement("div");
|
|
152
|
+
host.setAttribute("data-genesis-support-pins", "");
|
|
153
|
+
host.setAttribute("role", "group");
|
|
154
|
+
host.setAttribute("aria-label", "Feedback pins");
|
|
155
|
+
Object.assign(host.style, { position: "fixed", inset: "0", zIndex: "2147482999", pointerEvents: "none" });
|
|
156
|
+
doc.body.appendChild(host);
|
|
157
|
+
let reports = options.reports.slice();
|
|
158
|
+
const render = () => {
|
|
159
|
+
host.replaceChildren();
|
|
160
|
+
const path = view?.location.pathname ?? "";
|
|
161
|
+
reports.filter((r) => !r.route || r.route === path).forEach((report, i) => {
|
|
162
|
+
const { left, top } = resolvePin(doc, report.pin);
|
|
163
|
+
const pin = doc.createElement("button");
|
|
164
|
+
pin.type = "button";
|
|
165
|
+
pin.textContent = String(i + 1);
|
|
166
|
+
pin.setAttribute("aria-label", `Feedback ${i + 1}: ${report.title}`);
|
|
167
|
+
pin.title = report.title;
|
|
168
|
+
Object.assign(pin.style, {
|
|
169
|
+
position: "fixed",
|
|
170
|
+
left: `${Math.max(12, left)}px`,
|
|
171
|
+
top: `${Math.max(12, top)}px`,
|
|
172
|
+
transform: "translate(-50%, -50%)",
|
|
173
|
+
width: "28px",
|
|
174
|
+
height: "28px",
|
|
175
|
+
borderRadius: "999px",
|
|
176
|
+
border: "2px solid hsl(var(--background))",
|
|
177
|
+
background: "hsl(var(--destructive))",
|
|
178
|
+
color: "hsl(var(--destructive-foreground))",
|
|
179
|
+
font: "600 12px system-ui, sans-serif",
|
|
180
|
+
pointerEvents: "auto",
|
|
181
|
+
cursor: "pointer"
|
|
182
|
+
});
|
|
183
|
+
pin.addEventListener("click", (event) => {
|
|
184
|
+
event.stopPropagation();
|
|
185
|
+
options.onPinClick?.(report);
|
|
186
|
+
});
|
|
187
|
+
host.appendChild(pin);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
view?.addEventListener("popstate", render);
|
|
191
|
+
view?.addEventListener("resize", render);
|
|
192
|
+
view?.addEventListener("scroll", render, { passive: true });
|
|
193
|
+
render();
|
|
194
|
+
return {
|
|
195
|
+
setReports(next) {
|
|
196
|
+
reports = next.slice();
|
|
197
|
+
render();
|
|
198
|
+
},
|
|
199
|
+
destroy() {
|
|
200
|
+
view?.removeEventListener("popstate", render);
|
|
201
|
+
view?.removeEventListener("resize", render);
|
|
202
|
+
view?.removeEventListener("scroll", render);
|
|
203
|
+
host.remove();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function webContextCollector(options = {}) {
|
|
208
|
+
return createContextCollector({
|
|
209
|
+
appVersion: options.appVersion,
|
|
210
|
+
platform: "web",
|
|
211
|
+
route: () => typeof location === "undefined" ? void 0 : cleanRoute(location.pathname),
|
|
212
|
+
device: () => typeof window === "undefined" ? void 0 : { viewport: { width: window.innerWidth, height: window.innerHeight }, pixelRatio: window.devicePixelRatio, language: navigator.language }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function FeedbackPanel({ client, collector, copy: overrides, screenshots, pins = true, defaultType = "bug", onSent, onHide }) {
|
|
216
|
+
const copy = useMemo(() => resolveSupportCopy(overrides), [overrides]);
|
|
217
|
+
const collect = useMemo(() => collector ?? webContextCollector(), [collector]);
|
|
218
|
+
const [type, setType] = useState(defaultType);
|
|
219
|
+
const [message, setMessage] = useState("");
|
|
220
|
+
const [shot, setShot] = useState(null);
|
|
221
|
+
const [marks, setMarks] = useState({ strokes: [], redactions: [] });
|
|
222
|
+
const [pin, setPin] = useState(null);
|
|
223
|
+
const [status, setStatus] = useState({ kind: "idle" });
|
|
224
|
+
const canShoot = screenshots ?? canCaptureScreenshot();
|
|
225
|
+
const takeScreenshot = async () => {
|
|
226
|
+
onHide?.(true);
|
|
227
|
+
try {
|
|
228
|
+
setShot(await captureScreenshot());
|
|
229
|
+
setMarks({ strokes: [], redactions: [] });
|
|
230
|
+
} finally {
|
|
231
|
+
onHide?.(false);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const placePin = async () => {
|
|
235
|
+
onHide?.(true);
|
|
236
|
+
try {
|
|
237
|
+
setPin(await pickPin());
|
|
238
|
+
} finally {
|
|
239
|
+
onHide?.(false);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const send = async () => {
|
|
243
|
+
if (!message.trim()) {
|
|
244
|
+
setStatus({ kind: "error", message: copy.feedback.messageRequired });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
setStatus({ kind: "sending" });
|
|
248
|
+
try {
|
|
249
|
+
const context = await collect.collect();
|
|
250
|
+
if (pin) context.extra = { ...context.extra, pin };
|
|
251
|
+
let image = shot;
|
|
252
|
+
if (shot && (marks.strokes.length || marks.redactions.length)) {
|
|
253
|
+
try {
|
|
254
|
+
image = await annotateScreenshot(shot, marks);
|
|
255
|
+
} catch {
|
|
256
|
+
setStatus({ kind: "error", message: copy.annotate.failed });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const delivery = await client.submit({
|
|
261
|
+
type,
|
|
262
|
+
message: message.trim(),
|
|
263
|
+
context,
|
|
264
|
+
attachments: image ? [{ kind: "screenshot", contentType: image.contentType, base64: image.dataUrl, width: image.width, height: image.height }] : []
|
|
265
|
+
});
|
|
266
|
+
setStatus({ kind: "sent", dropped: delivery.attachmentsDropped });
|
|
267
|
+
setMessage("");
|
|
268
|
+
setShot(null);
|
|
269
|
+
setMarks({ strokes: [], redactions: [] });
|
|
270
|
+
setPin(null);
|
|
271
|
+
onSent?.(delivery);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const limited = error.code === "GS429";
|
|
274
|
+
setStatus({ kind: "error", message: limited ? copy.feedback.rateLimited : copy.feedback.failedBody });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
return /* @__PURE__ */ jsxs(Tabs, { defaultValue: "new", className: "flex flex-col gap-4", "data-testid": "genesis-support-panel", children: [
|
|
278
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [
|
|
279
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "new", children: copy.feedback.title }),
|
|
280
|
+
client.myReports ? /* @__PURE__ */ jsx(TabsTrigger, { value: "mine", children: copy.inbox.sectionTitle }) : null
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "new", className: "flex flex-col gap-4", children: [
|
|
283
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
284
|
+
/* @__PURE__ */ jsx(Label, { children: copy.feedback.typeLabel }),
|
|
285
|
+
/* @__PURE__ */ jsx(ToggleGroup, { type: "single", value: type, onValueChange: (v) => v && setType(v), "aria-label": copy.feedback.typeLabel, children: FEEDBACK_TYPES.map((t) => /* @__PURE__ */ jsx(ToggleGroupItem, { value: t, "data-testid": `genesis-support-type-${t}`, children: copy.feedback.types[t] }, t)) })
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
288
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "genesis-support-message", children: copy.feedback.messageLabel }),
|
|
289
|
+
/* @__PURE__ */ jsx(
|
|
290
|
+
Textarea,
|
|
291
|
+
{
|
|
292
|
+
id: "genesis-support-message",
|
|
293
|
+
"data-testid": "genesis-support-message",
|
|
294
|
+
value: message,
|
|
295
|
+
maxLength: 2e4,
|
|
296
|
+
rows: 5,
|
|
297
|
+
placeholder: copy.feedback.messagePlaceholder,
|
|
298
|
+
onChange: (e) => setMessage(e.target.value)
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
] }),
|
|
302
|
+
canShoot || pins ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
303
|
+
canShoot && !shot ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void takeScreenshot(), "data-testid": "genesis-support-capture", children: copy.feedback.screenshotLabel }) : null,
|
|
304
|
+
pins ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void placePin(), "data-testid": "genesis-support-pin", children: pin ? copy.feedback.pinned : copy.feedback.pin }) : null
|
|
305
|
+
] }) : null,
|
|
306
|
+
shot ? /* @__PURE__ */ jsx(ScreenshotEditor, { shot, marks, onMarks: setMarks, onRemove: () => setShot(null), copy }) : null,
|
|
307
|
+
status.kind === "error" ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", "data-testid": "genesis-support-error", children: /* @__PURE__ */ jsx(AlertDescription, { children: status.message }) }) : null,
|
|
308
|
+
status.kind === "sent" ? /* @__PURE__ */ jsx(Alert, { "data-testid": "genesis-support-sent", children: /* @__PURE__ */ jsx(AlertDescription, { children: status.dropped ? copy.feedback.sentWithoutScreenshot : copy.feedback.sentBody }) }) : null,
|
|
309
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: () => void send(), disabled: status.kind === "sending", "data-testid": "genesis-support-send", children: status.kind === "sending" ? copy.feedback.sending : copy.feedback.send })
|
|
310
|
+
] }),
|
|
311
|
+
client.myReports ? /* @__PURE__ */ jsx(TabsContent, { value: "mine", children: /* @__PURE__ */ jsx(MyReports, { load: client.myReports, copy, refreshKey: status.kind === "sent" ? 1 : 0 }) }) : null
|
|
312
|
+
] });
|
|
313
|
+
}
|
|
314
|
+
function MyReports({ load, copy, refreshKey }) {
|
|
315
|
+
const [state, setState] = useState({ kind: "loading" });
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
let alive = true;
|
|
318
|
+
setState({ kind: "loading" });
|
|
319
|
+
load().then((reports) => alive && setState({ kind: "ok", reports })).catch(() => alive && setState({ kind: "error" }));
|
|
320
|
+
return () => {
|
|
321
|
+
alive = false;
|
|
322
|
+
};
|
|
323
|
+
}, [load, refreshKey]);
|
|
324
|
+
if (state.kind === "loading") return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-testid": "genesis-support-mine-loading", children: "\u2026" });
|
|
325
|
+
if (state.kind === "error")
|
|
326
|
+
return /* @__PURE__ */ jsx(Alert, { variant: "destructive", "data-testid": "genesis-support-mine-error", children: /* @__PURE__ */ jsx(AlertDescription, { children: copy.feedback.failedBody }) });
|
|
327
|
+
if (!state.reports.length)
|
|
328
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": "genesis-support-mine-empty", className: "flex flex-col gap-1", children: [
|
|
329
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: copy.inbox.emptyTitle }),
|
|
330
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: copy.inbox.emptyBody })
|
|
331
|
+
] });
|
|
332
|
+
return /* @__PURE__ */ jsx("ul", { className: "flex flex-col divide-y", "data-testid": "genesis-support-mine", children: state.reports.map((r) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-3 py-2", children: [
|
|
333
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: r.title || r.message }),
|
|
334
|
+
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [
|
|
335
|
+
r.unread ? /* @__PURE__ */ jsx(Badge, { children: copy.inbox.unread }) : null,
|
|
336
|
+
/* @__PURE__ */ jsx(Badge, { variant: "secondary", children: copy.status[r.status] })
|
|
337
|
+
] })
|
|
338
|
+
] }, r.id)) });
|
|
339
|
+
}
|
|
340
|
+
function DraftBox({ drag }) {
|
|
341
|
+
const end = drag.points[drag.points.length - 1] ?? drag.start;
|
|
342
|
+
const x = Math.min(drag.start.x, end.x);
|
|
343
|
+
const y = Math.min(drag.start.y, end.y);
|
|
344
|
+
return /* @__PURE__ */ jsx(
|
|
345
|
+
"div",
|
|
346
|
+
{
|
|
347
|
+
className: "pointer-events-none absolute border-2 border-dashed border-foreground bg-foreground/60",
|
|
348
|
+
style: { left: `${x * 100}%`, top: `${y * 100}%`, width: `${Math.abs(end.x - drag.start.x) * 100}%`, height: `${Math.abs(end.y - drag.start.y) * 100}%` }
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
function undoMark(marks) {
|
|
353
|
+
const order = marks.order ?? [];
|
|
354
|
+
const last = order[order.length - 1] ?? (marks.redactions.length ? "redaction" : "stroke");
|
|
355
|
+
const rest = order.slice(0, -1);
|
|
356
|
+
return last === "redaction" ? { ...marks, redactions: marks.redactions.slice(0, -1), order: rest } : { ...marks, strokes: marks.strokes.slice(0, -1), order: rest };
|
|
357
|
+
}
|
|
358
|
+
function ScreenshotEditor({
|
|
359
|
+
shot,
|
|
360
|
+
marks,
|
|
361
|
+
onMarks,
|
|
362
|
+
onRemove,
|
|
363
|
+
copy
|
|
364
|
+
}) {
|
|
365
|
+
const [tool, setTool] = useState("pen");
|
|
366
|
+
const [drag, setDrag] = useState(null);
|
|
367
|
+
const point = (e) => {
|
|
368
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
369
|
+
return { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)) };
|
|
370
|
+
};
|
|
371
|
+
const finish = (end) => {
|
|
372
|
+
if (!drag) return;
|
|
373
|
+
setDrag(null);
|
|
374
|
+
const order = marks.order ?? [];
|
|
375
|
+
if (tool === "pen") onMarks({ ...marks, strokes: [...marks.strokes, [...drag.points, end]], order: [...order, "stroke"] });
|
|
376
|
+
else
|
|
377
|
+
onMarks({
|
|
378
|
+
...marks,
|
|
379
|
+
order: [...order, "redaction"],
|
|
380
|
+
redactions: [...marks.redactions, { x: Math.min(drag.start.x, end.x), y: Math.min(drag.start.y, end.y), width: Math.abs(end.x - drag.start.x), height: Math.abs(end.y - drag.start.y) }]
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
const pct = (n) => `${n * 100}%`;
|
|
384
|
+
const strokes = drag && tool === "pen" ? [...marks.strokes, drag.points] : marks.strokes;
|
|
385
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-testid": "genesis-support-screenshot", children: [
|
|
386
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
387
|
+
/* @__PURE__ */ jsx(Label, { children: copy.feedback.screenshotLabel }),
|
|
388
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
389
|
+
/* @__PURE__ */ jsxs(ToggleGroup, { type: "single", size: "sm", value: tool, onValueChange: (v) => v && setTool(v), children: [
|
|
390
|
+
/* @__PURE__ */ jsx(ToggleGroupItem, { value: "pen", children: copy.annotate.pen }),
|
|
391
|
+
/* @__PURE__ */ jsx(ToggleGroupItem, { value: "hide", children: copy.annotate.hide })
|
|
392
|
+
] }),
|
|
393
|
+
/* @__PURE__ */ jsx(
|
|
394
|
+
Button,
|
|
395
|
+
{
|
|
396
|
+
type: "button",
|
|
397
|
+
variant: "ghost",
|
|
398
|
+
size: "sm",
|
|
399
|
+
disabled: !marks.strokes.length && !marks.redactions.length,
|
|
400
|
+
onClick: () => onMarks(undoMark(marks)),
|
|
401
|
+
"data-testid": "genesis-support-undo",
|
|
402
|
+
children: copy.annotate.undo
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onRemove, "data-testid": "genesis-support-remove-screenshot", children: copy.feedback.removeScreenshot })
|
|
406
|
+
] })
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsxs(
|
|
409
|
+
"div",
|
|
410
|
+
{
|
|
411
|
+
className: "relative cursor-crosshair touch-none overflow-hidden rounded-md border",
|
|
412
|
+
"data-testid": "genesis-support-screenshot-surface",
|
|
413
|
+
onPointerDown: (e) => {
|
|
414
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
415
|
+
const p = point(e);
|
|
416
|
+
setDrag({ start: p, points: [p] });
|
|
417
|
+
},
|
|
418
|
+
onPointerCancel: () => setDrag(null),
|
|
419
|
+
onPointerMove: (e) => drag && setDrag({ ...drag, points: tool === "pen" ? [...drag.points, point(e)] : [drag.start, point(e)] }),
|
|
420
|
+
onPointerUp: (e) => finish(point(e)),
|
|
421
|
+
children: [
|
|
422
|
+
/* @__PURE__ */ jsx("img", { src: shot.dataUrl, alt: copy.feedback.screenshotLabel, className: "block w-full select-none", draggable: false }),
|
|
423
|
+
/* @__PURE__ */ jsx("svg", { className: "pointer-events-none absolute inset-0 h-full w-full", viewBox: "0 0 1 1", preserveAspectRatio: "none", "aria-hidden": true, children: strokes.map((stroke, i) => /* @__PURE__ */ jsx(
|
|
424
|
+
"polyline",
|
|
425
|
+
{
|
|
426
|
+
points: stroke.map((p) => `${p.x},${p.y}`).join(" "),
|
|
427
|
+
fill: "none",
|
|
428
|
+
stroke: "hsl(var(--destructive))",
|
|
429
|
+
strokeWidth: 6e-3,
|
|
430
|
+
strokeLinecap: "round",
|
|
431
|
+
strokeLinejoin: "round"
|
|
432
|
+
},
|
|
433
|
+
i
|
|
434
|
+
)) }),
|
|
435
|
+
drag && tool === "hide" ? /* @__PURE__ */ jsx(DraftBox, { drag }) : null,
|
|
436
|
+
marks.redactions.map((r, i) => /* @__PURE__ */ jsx(
|
|
437
|
+
"div",
|
|
438
|
+
{
|
|
439
|
+
"data-testid": `genesis-support-redaction-${i}`,
|
|
440
|
+
className: "pointer-events-none absolute bg-foreground",
|
|
441
|
+
style: { left: pct(r.x), top: pct(r.y), width: pct(r.width), height: pct(r.height) }
|
|
442
|
+
},
|
|
443
|
+
i
|
|
444
|
+
))
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
),
|
|
448
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: copy.feedback.screenshotHint })
|
|
449
|
+
] });
|
|
450
|
+
}
|
|
451
|
+
var POSITIONS = {
|
|
452
|
+
"bottom-right": "fixed bottom-4 right-4 z-50",
|
|
453
|
+
"bottom-left": "fixed bottom-4 left-4 z-50"
|
|
454
|
+
};
|
|
455
|
+
function FeedbackLauncher({ position = "bottom-right", label, ...panel }) {
|
|
456
|
+
const copy = useMemo(() => resolveSupportCopy(panel.copy), [panel.copy]);
|
|
457
|
+
const [open, setOpen] = useState(false);
|
|
458
|
+
const [hidden, setHidden] = useState(false);
|
|
459
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
460
|
+
/* @__PURE__ */ jsx(Button, { className: POSITIONS[position], onClick: () => setOpen(true), "data-testid": "genesis-support-launcher", "aria-haspopup": "dialog", children: label ?? copy.settings.sendFeedback }),
|
|
461
|
+
/* @__PURE__ */ jsx(Sheet, { open: open && !hidden, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "flex w-full flex-col gap-4 overflow-y-auto sm:max-w-md", children: [
|
|
462
|
+
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
463
|
+
/* @__PURE__ */ jsx(SheetTitle, { children: copy.feedback.title }),
|
|
464
|
+
/* @__PURE__ */ jsx(SheetDescription, { children: copy.feedback.messagePlaceholder })
|
|
465
|
+
] }),
|
|
466
|
+
/* @__PURE__ */ jsx(FeedbackPanel, { ...panel, onHide: setHidden })
|
|
467
|
+
] }) })
|
|
468
|
+
] });
|
|
469
|
+
}
|
|
470
|
+
function mountSupportWidget(props) {
|
|
471
|
+
const { container, ...launcher } = props;
|
|
472
|
+
const host = container ?? document.body.appendChild(document.createElement("div"));
|
|
473
|
+
host.setAttribute("data-genesis-support", "");
|
|
474
|
+
const root = createRoot(host);
|
|
475
|
+
root.render(/* @__PURE__ */ jsx(FeedbackLauncher, { ...launcher }));
|
|
476
|
+
return () => {
|
|
477
|
+
root.unmount();
|
|
478
|
+
if (!container) host.remove();
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export { FeedbackLauncher, FeedbackPanel, annotateScreenshot, canCaptureScreenshot, captureScreenshot, createPinOverlay, mountSupportWidget, pickPin, resolvePin, stableLocator, undoMark, webContextCollector };
|
|
483
|
+
//# sourceMappingURL=web.js.map
|
|
484
|
+
//# sourceMappingURL=web.js.map
|
package/dist/web.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/web/capture.ts","../src/web/pin.ts","../src/web/feedback-panel.tsx","../src/web/launcher.tsx"],"names":["useMemo","useState","jsxs","jsx","Button"],"mappings":"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","file":"web.js","sourcesContent":["/**\n * Screenshots on the web. The browser only lets a page capture itself with\n * the person's say-so: `getDisplayMedia` with `preferCurrentTab` asks, and a\n * single frame is kept. When the person says no (or the browser can't), there\n * is no screenshot: never a fake one.\n */\n\nexport interface WebCapture {\n /** `data:image/webp;base64,…` (or JPEG where WebP isn't encoded). */\n dataUrl: string;\n contentType: \"image/webp\" | \"image/jpeg\";\n width: number;\n height: number;\n}\n\ntype DisplayMediaDevices = MediaDevices & { getDisplayMedia(constraints: Record<string, unknown>): Promise<MediaStream> };\n\n/** Whether this browser can capture the tab at all. */\nexport function canCaptureScreenshot(): boolean {\n const devices = (globalThis.navigator as Navigator | undefined)?.mediaDevices as Partial<DisplayMediaDevices> | undefined;\n return typeof devices?.getDisplayMedia === \"function\";\n}\n\n/** One frame of the current tab, or `null` when the person declined or the browser can't. */\nexport async function captureScreenshot(doc: Document = document): Promise<WebCapture | null> {\n if (!canCaptureScreenshot()) return null;\n let stream: MediaStream;\n try {\n stream = await (navigator.mediaDevices as DisplayMediaDevices).getDisplayMedia({\n video: { preferCurrentTab: true, frameRate: 1 },\n audio: false,\n preferCurrentTab: true,\n selfBrowserSurface: \"include\",\n });\n } catch {\n return null;\n }\n try {\n const video = doc.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = stream;\n await video.play();\n await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));\n const canvas = doc.createElement(\"canvas\");\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx || !canvas.width || !canvas.height) return null;\n ctx.drawImage(video, 0, 0);\n return fromCanvas(canvas);\n } catch {\n return null;\n } finally {\n stream.getTracks().forEach((track) => track.stop());\n }\n}\n\n/** The longest side a screenshot keeps: enough to read, well under the bucket's 5 MB. */\nexport const MAX_SCREENSHOT_SIDE = 2000;\n\nfunction fromCanvas(source: HTMLCanvasElement): WebCapture {\n let canvas = source;\n const scale = Math.min(1, MAX_SCREENSHOT_SIDE / Math.max(source.width, source.height));\n if (scale < 1) {\n canvas = source.ownerDocument.createElement(\"canvas\");\n canvas.width = Math.round(source.width * scale);\n canvas.height = Math.round(source.height * scale);\n canvas.getContext(\"2d\")?.drawImage(source, 0, 0, canvas.width, canvas.height);\n }\n // WebP where the browser encodes it, else JPEG (never PNG: a full tab as PNG can pass 5 MB).\n const webp = canvas.toDataURL(\"image/webp\", 0.9);\n if (webp.startsWith(\"data:image/webp\")) return { dataUrl: webp, contentType: \"image/webp\", width: canvas.width, height: canvas.height };\n return { dataUrl: canvas.toDataURL(\"image/jpeg\", 0.88), contentType: \"image/jpeg\", width: canvas.width, height: canvas.height };\n}\n\n/** A stroke in fractions of the image (0 to 1), so it survives resizing. */\nexport type Stroke = Array<{ x: number; y: number }>;\n\n/** A rectangle in fractions of the image, painted solid to hide what's under it. */\nexport interface Redaction {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** Draws pen strokes and solid redactions onto a captured image; returns the new capture. */\nexport async function annotateScreenshot(\n capture: WebCapture,\n { strokes = [], redactions = [], penColor = \"red\", redactColor = \"black\" }: { strokes?: Stroke[]; redactions?: Redaction[]; penColor?: string; redactColor?: string },\n doc: Document = document,\n): Promise<WebCapture> {\n const img = doc.createElement(\"img\");\n img.src = capture.dataUrl;\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error(\"image_load_failed\"));\n });\n const canvas = doc.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return capture;\n ctx.drawImage(img, 0, 0);\n ctx.fillStyle = redactColor;\n for (const r of redactions) ctx.fillRect(r.x * canvas.width, r.y * canvas.height, r.width * canvas.width, r.height * canvas.height);\n ctx.strokeStyle = penColor;\n ctx.lineWidth = Math.max(3, canvas.width / 300);\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n for (const stroke of strokes) {\n if (stroke.length < 2) continue;\n ctx.beginPath();\n ctx.moveTo(stroke[0]!.x * canvas.width, stroke[0]!.y * canvas.height);\n for (const p of stroke.slice(1)) ctx.lineTo(p.x * canvas.width, p.y * canvas.height);\n ctx.stroke();\n }\n return fromCanvas(canvas);\n}\n","/**\n * Pins on the live page: the person points at the spot a report is about\n * (`pickPin`), and numbered pins show their open reports on the page they\n * were sent from (`createPinOverlay`). A pin travels in the report's\n * context as `pin`, so it needs no column of its own.\n *\n * Plain DOM on the host document (no iframe, no framework), colored from the\n * Genesis theme variables.\n */\n\nexport interface FeedbackPin {\n /** A CSS selector for the element under the point, when one is stable (an id or a data-testid). */\n locator?: string | null;\n /** Where in that element, in percent. */\n xPct?: number | null;\n yPct?: number | null;\n /** The point in the viewport, as a fallback. */\n viewportX: number;\n viewportY: number;\n scrollX: number;\n scrollY: number;\n}\n\nexport interface PinnedReport {\n id: string;\n title: string;\n /** The route (path) it was sent from. */\n route: string | null;\n pin: FeedbackPin;\n}\n\nconst cssEscape = (value: string) => (typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(value) : value.replace(/[^A-Za-z0-9_-]/g, \"\\\\$&\"));\n\n/** A selector for an element that is likely to survive a re-render: its id, else a data-testid. */\nexport function stableLocator(el: Element | null): string | null {\n for (let node: Element | null = el; node && node !== node.ownerDocument.documentElement; node = node.parentElement) {\n if (node.id) return `#${cssEscape(node.id)}`;\n const testId = node.getAttribute(\"data-testid\");\n if (testId) return `[data-testid=\"${testId.replace(/\"/g, '\\\\\"')}\"]`;\n }\n return null;\n}\n\n/**\n * Waits for one click anywhere on the page and returns the pin, or `null`\n * on Escape. The click doesn't reach the page.\n */\nexport function pickPin(doc: Document = document): Promise<FeedbackPin | null> {\n return new Promise((resolve) => {\n const veil = doc.createElement(\"div\");\n veil.setAttribute(\"data-genesis-support-pin-picker\", \"\");\n veil.setAttribute(\"role\", \"presentation\");\n Object.assign(veil.style, { position: \"fixed\", inset: \"0\", zIndex: \"2147483000\", cursor: \"crosshair\", background: \"hsl(var(--foreground) / 0.06)\" });\n const done = (pin: FeedbackPin | null) => {\n veil.remove();\n doc.removeEventListener(\"keydown\", onKey, true);\n resolve(pin);\n };\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") done(null);\n };\n veil.addEventListener(\"click\", (event) => {\n event.preventDefault();\n event.stopPropagation();\n veil.style.pointerEvents = \"none\";\n const target = doc.elementFromPoint?.(event.clientX, event.clientY) ?? null;\n const locator = stableLocator(target);\n const anchor = locator ? doc.querySelector(locator) : null;\n const rect = anchor?.getBoundingClientRect();\n const view = doc.defaultView;\n done({\n locator,\n xPct: rect && rect.width ? Math.round(((event.clientX - rect.left) / rect.width) * 100) : null,\n yPct: rect && rect.height ? Math.round(((event.clientY - rect.top) / rect.height) * 100) : null,\n viewportX: event.clientX,\n viewportY: event.clientY,\n scrollX: view?.scrollX ?? 0,\n scrollY: view?.scrollY ?? 0,\n });\n });\n doc.addEventListener(\"keydown\", onKey, true);\n doc.body.appendChild(veil);\n });\n}\n\n/** Where a pin sits now: on its element if it is still there, else where it was clicked. */\nexport function resolvePin(doc: Document, pin: FeedbackPin): { left: number; top: number } {\n if (pin.locator) {\n try {\n const node = doc.querySelector(pin.locator);\n if (node) {\n const r = node.getBoundingClientRect();\n return { left: r.left + (r.width * (pin.xPct ?? 50)) / 100, top: r.top + (r.height * (pin.yPct ?? 50)) / 100 };\n }\n } catch {\n // an invalid selector: fall back to the point\n }\n }\n return { left: pin.viewportX, top: pin.viewportY };\n}\n\nexport interface PinOverlay {\n setReports(reports: PinnedReport[]): void;\n destroy(): void;\n}\n\n/** Numbered pins for reports sent from the current path. Re-drawn on navigation, scroll and resize. */\nexport function createPinOverlay(options: { reports: PinnedReport[]; onPinClick?: (report: PinnedReport) => void; document?: Document }): PinOverlay {\n const doc = options.document ?? document;\n const view = doc.defaultView;\n const host = doc.createElement(\"div\");\n host.setAttribute(\"data-genesis-support-pins\", \"\");\n host.setAttribute(\"role\", \"group\");\n host.setAttribute(\"aria-label\", \"Feedback pins\");\n Object.assign(host.style, { position: \"fixed\", inset: \"0\", zIndex: \"2147482999\", pointerEvents: \"none\" });\n doc.body.appendChild(host);\n let reports = options.reports.slice();\n\n const render = () => {\n host.replaceChildren();\n const path = view?.location.pathname ?? \"\";\n reports\n .filter((r) => !r.route || r.route === path)\n .forEach((report, i) => {\n const { left, top } = resolvePin(doc, report.pin);\n const pin = doc.createElement(\"button\");\n pin.type = \"button\";\n pin.textContent = String(i + 1);\n pin.setAttribute(\"aria-label\", `Feedback ${i + 1}: ${report.title}`);\n pin.title = report.title;\n Object.assign(pin.style, {\n position: \"fixed\",\n left: `${Math.max(12, left)}px`,\n top: `${Math.max(12, top)}px`,\n transform: \"translate(-50%, -50%)\",\n width: \"28px\",\n height: \"28px\",\n borderRadius: \"999px\",\n border: \"2px solid hsl(var(--background))\",\n background: \"hsl(var(--destructive))\",\n color: \"hsl(var(--destructive-foreground))\",\n font: \"600 12px system-ui, sans-serif\",\n pointerEvents: \"auto\",\n cursor: \"pointer\",\n });\n pin.addEventListener(\"click\", (event) => {\n event.stopPropagation();\n options.onPinClick?.(report);\n });\n host.appendChild(pin);\n });\n };\n\n view?.addEventListener(\"popstate\", render);\n view?.addEventListener(\"resize\", render);\n view?.addEventListener(\"scroll\", render, { passive: true });\n render();\n return {\n setReports(next) {\n reports = next.slice();\n render();\n },\n destroy() {\n view?.removeEventListener(\"popstate\", render);\n view?.removeEventListener(\"resize\", render);\n view?.removeEventListener(\"scroll\", render);\n host.remove();\n },\n };\n}\n","/**\n * The web feedback panel, built from Genesis UI: a \"New\" tab (type, message,\n * an optional screenshot of the tab with pen marks and hidden areas, an\n * optional pin on the page) and a \"My reports\" tab (the person's reports and\n * their status). Loading, empty and failed reads are told apart.\n *\n * Put it anywhere (a page, a Sheet); `FeedbackLauncher` puts it in a Sheet\n * behind a floating button.\n */\nimport { useEffect, useMemo, useState, type PointerEvent } from \"react\";\nimport {\n Alert,\n AlertDescription,\n Badge,\n Button,\n Label,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n Textarea,\n ToggleGroup,\n ToggleGroupItem,\n} from \"@marktiderman/genesis-ui\";\nimport type { SupportClient, SupportReport } from \"../client\";\nimport { resolveSupportCopy, type SupportCopyOverrides } from \"../copy\";\nimport { cleanRoute } from \"../contract\";\nimport { createContextCollector, FEEDBACK_TYPES, type ContextCollector, type FeedbackDelivery, type FeedbackSink, type FeedbackType } from \"../record\";\nimport { annotateScreenshot, canCaptureScreenshot, captureScreenshot, type Redaction, type Stroke, type WebCapture } from \"./capture\";\nimport { pickPin, type FeedbackPin } from \"./pin\";\n\n/** The default web collector: the path (never the query string), `web`, and the viewport and language. */\nexport function webContextCollector(options: { appVersion?: string } = {}): ContextCollector {\n return createContextCollector({\n appVersion: options.appVersion,\n platform: \"web\",\n route: () => (typeof location === \"undefined\" ? undefined : cleanRoute(location.pathname)),\n device: () =>\n typeof window === \"undefined\"\n ? undefined\n : { viewport: { width: window.innerWidth, height: window.innerHeight }, pixelRatio: window.devicePixelRatio, language: navigator.language },\n });\n}\n\nexport interface FeedbackPanelProps {\n /** Where feedback goes. A support client also fills \"My reports\". */\n client: FeedbackSink & Partial<Pick<SupportClient, \"myReports\">>;\n collector?: ContextCollector;\n copy?: SupportCopyOverrides;\n /** Offer a screenshot of the tab. Default: when the browser can capture. */\n screenshots?: boolean;\n /** Offer a pin on the page. Default on. */\n pins?: boolean;\n defaultType?: FeedbackType;\n onSent?: (delivery: FeedbackDelivery) => void;\n /** Hidden while the person picks a pin or the browser asks to capture, so the page shows. */\n onHide?: (hidden: boolean) => void;\n}\n\ntype Status = { kind: \"idle\" } | { kind: \"sending\" } | { kind: \"sent\"; dropped: number } | { kind: \"error\"; message: string };\n\nexport function FeedbackPanel({ client, collector, copy: overrides, screenshots, pins = true, defaultType = \"bug\", onSent, onHide }: FeedbackPanelProps) {\n const copy = useMemo(() => resolveSupportCopy(overrides), [overrides]);\n const collect = useMemo(() => collector ?? webContextCollector(), [collector]);\n const [type, setType] = useState<FeedbackType>(defaultType);\n const [message, setMessage] = useState(\"\");\n const [shot, setShot] = useState<WebCapture | null>(null);\n // Marks stay a list until Send, then are drawn onto the image once: a quick second mark can't\n // drop the first, and nothing is sent half-marked.\n const [marks, setMarks] = useState<Marks>({ strokes: [], redactions: [] });\n const [pin, setPin] = useState<FeedbackPin | null>(null);\n const [status, setStatus] = useState<Status>({ kind: \"idle\" });\n const canShoot = screenshots ?? canCaptureScreenshot();\n\n const takeScreenshot = async () => {\n onHide?.(true);\n try {\n setShot(await captureScreenshot());\n setMarks({ strokes: [], redactions: [] });\n } finally {\n onHide?.(false);\n }\n };\n\n const placePin = async () => {\n onHide?.(true);\n try {\n setPin(await pickPin());\n } finally {\n onHide?.(false);\n }\n };\n\n const send = async () => {\n if (!message.trim()) {\n setStatus({ kind: \"error\", message: copy.feedback.messageRequired });\n return;\n }\n setStatus({ kind: \"sending\" });\n try {\n const context = await collect.collect();\n if (pin) context.extra = { ...context.extra, pin };\n let image = shot;\n if (shot && (marks.strokes.length || marks.redactions.length)) {\n try {\n image = await annotateScreenshot(shot, marks);\n } catch {\n // Never send the screenshot without the parts the person hid.\n setStatus({ kind: \"error\", message: copy.annotate.failed });\n return;\n }\n }\n const delivery = await client.submit({\n type,\n message: message.trim(),\n context,\n attachments: image ? [{ kind: \"screenshot\", contentType: image.contentType, base64: image.dataUrl, width: image.width, height: image.height }] : [],\n });\n setStatus({ kind: \"sent\", dropped: delivery.attachmentsDropped });\n setMessage(\"\");\n setShot(null);\n setMarks({ strokes: [], redactions: [] });\n setPin(null);\n onSent?.(delivery);\n } catch (error) {\n const limited = (error as { code?: string }).code === \"GS429\";\n setStatus({ kind: \"error\", message: limited ? copy.feedback.rateLimited : copy.feedback.failedBody });\n }\n };\n\n return (\n <Tabs defaultValue=\"new\" className=\"flex flex-col gap-4\" data-testid=\"genesis-support-panel\">\n <TabsList>\n <TabsTrigger value=\"new\">{copy.feedback.title}</TabsTrigger>\n {client.myReports ? <TabsTrigger value=\"mine\">{copy.inbox.sectionTitle}</TabsTrigger> : null}\n </TabsList>\n <TabsContent value=\"new\" className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n <Label>{copy.feedback.typeLabel}</Label>\n <ToggleGroup type=\"single\" value={type} onValueChange={(v: string) => v && setType(v as FeedbackType)} aria-label={copy.feedback.typeLabel}>\n {FEEDBACK_TYPES.map((t) => (\n <ToggleGroupItem key={t} value={t} data-testid={`genesis-support-type-${t}`}>\n {copy.feedback.types[t]}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </div>\n <div className=\"flex flex-col gap-2\">\n <Label htmlFor=\"genesis-support-message\">{copy.feedback.messageLabel}</Label>\n <Textarea\n id=\"genesis-support-message\"\n data-testid=\"genesis-support-message\"\n value={message}\n maxLength={20000}\n rows={5}\n placeholder={copy.feedback.messagePlaceholder}\n onChange={(e) => setMessage(e.target.value)}\n />\n </div>\n {canShoot || pins ? (\n <div className=\"flex flex-wrap gap-2\">\n {canShoot && !shot ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void takeScreenshot()} data-testid=\"genesis-support-capture\">\n {copy.feedback.screenshotLabel}\n </Button>\n ) : null}\n {pins ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void placePin()} data-testid=\"genesis-support-pin\">\n {pin ? copy.feedback.pinned : copy.feedback.pin}\n </Button>\n ) : null}\n </div>\n ) : null}\n {shot ? <ScreenshotEditor shot={shot} marks={marks} onMarks={setMarks} onRemove={() => setShot(null)} copy={copy} /> : null}\n {status.kind === \"error\" ? (\n <Alert variant=\"destructive\" data-testid=\"genesis-support-error\">\n <AlertDescription>{status.message}</AlertDescription>\n </Alert>\n ) : null}\n {status.kind === \"sent\" ? (\n <Alert data-testid=\"genesis-support-sent\">\n <AlertDescription>{status.dropped ? copy.feedback.sentWithoutScreenshot : copy.feedback.sentBody}</AlertDescription>\n </Alert>\n ) : null}\n <Button type=\"button\" onClick={() => void send()} disabled={status.kind === \"sending\"} data-testid=\"genesis-support-send\">\n {status.kind === \"sending\" ? copy.feedback.sending : copy.feedback.send}\n </Button>\n </TabsContent>\n {client.myReports ? (\n <TabsContent value=\"mine\">\n <MyReports load={client.myReports} copy={copy} refreshKey={status.kind === \"sent\" ? 1 : 0} />\n </TabsContent>\n ) : null}\n </Tabs>\n );\n}\n\nfunction MyReports({ load, copy, refreshKey }: { load: NonNullable<SupportClient[\"myReports\"]>; copy: ReturnType<typeof resolveSupportCopy>; refreshKey: number }) {\n const [state, setState] = useState<{ kind: \"loading\" } | { kind: \"error\" } | { kind: \"ok\"; reports: SupportReport[] }>({ kind: \"loading\" });\n useEffect(() => {\n let alive = true;\n setState({ kind: \"loading\" });\n load()\n .then((reports) => alive && setState({ kind: \"ok\", reports }))\n .catch(() => alive && setState({ kind: \"error\" }));\n return () => {\n alive = false;\n };\n }, [load, refreshKey]);\n if (state.kind === \"loading\") return <p className=\"text-sm text-muted-foreground\" data-testid=\"genesis-support-mine-loading\">…</p>;\n if (state.kind === \"error\")\n return (\n <Alert variant=\"destructive\" data-testid=\"genesis-support-mine-error\">\n <AlertDescription>{copy.feedback.failedBody}</AlertDescription>\n </Alert>\n );\n if (!state.reports.length)\n return (\n <div data-testid=\"genesis-support-mine-empty\" className=\"flex flex-col gap-1\">\n <p className=\"font-medium\">{copy.inbox.emptyTitle}</p>\n <p className=\"text-sm text-muted-foreground\">{copy.inbox.emptyBody}</p>\n </div>\n );\n return (\n <ul className=\"flex flex-col divide-y\" data-testid=\"genesis-support-mine\">\n {state.reports.map((r) => (\n <li key={r.id} className=\"flex items-center justify-between gap-3 py-2\">\n <span className=\"truncate\">{r.title || r.message}</span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {r.unread ? <Badge>{copy.inbox.unread}</Badge> : null}\n <Badge variant=\"secondary\">{copy.status[r.status]}</Badge>\n </span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** The box being dragged, while it is dragged. */\nfunction DraftBox({ drag }: { drag: { start: { x: number; y: number }; points: Stroke } }) {\n const end = drag.points[drag.points.length - 1] ?? drag.start;\n const x = Math.min(drag.start.x, end.x);\n const y = Math.min(drag.start.y, end.y);\n return (\n <div\n className=\"pointer-events-none absolute border-2 border-dashed border-foreground bg-foreground/60\"\n style={{ left: `${x * 100}%`, top: `${y * 100}%`, width: `${Math.abs(end.x - drag.start.x) * 100}%`, height: `${Math.abs(end.y - drag.start.y) * 100}%` }}\n />\n );\n}\n\n/** Pen strokes and hidden areas, in fractions of the image. */\nexport interface Marks {\n strokes: Stroke[];\n redactions: Redaction[];\n /** The kinds in the order they were drawn, for Undo. */\n order?: Array<\"stroke\" | \"redaction\">;\n}\n\n/** Marks without the newest one (by `order`; without it, the last redaction, then the last stroke). */\nexport function undoMark(marks: Marks): Marks {\n const order = marks.order ?? [];\n const last = order[order.length - 1] ?? (marks.redactions.length ? \"redaction\" : \"stroke\");\n const rest = order.slice(0, -1);\n return last === \"redaction\"\n ? { ...marks, redactions: marks.redactions.slice(0, -1), order: rest }\n : { ...marks, strokes: marks.strokes.slice(0, -1), order: rest };\n}\n\n/** The captured tab, with pen strokes and hidden (solid) areas shown over it; drawn into the image at Send. */\nfunction ScreenshotEditor({\n shot,\n marks,\n onMarks,\n onRemove,\n copy,\n}: {\n shot: WebCapture;\n marks: Marks;\n onMarks: (next: Marks) => void;\n onRemove: () => void;\n copy: ReturnType<typeof resolveSupportCopy>;\n}) {\n const [tool, setTool] = useState<\"pen\" | \"hide\">(\"pen\");\n const [drag, setDrag] = useState<{ start: { x: number; y: number }; points: Stroke } | null>(null);\n const point = (e: PointerEvent<HTMLDivElement>) => {\n const r = e.currentTarget.getBoundingClientRect();\n return { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)) };\n };\n const finish = (end: { x: number; y: number }) => {\n if (!drag) return;\n setDrag(null);\n const order = marks.order ?? [];\n if (tool === \"pen\") onMarks({ ...marks, strokes: [...marks.strokes, [...drag.points, end]], order: [...order, \"stroke\"] });\n else\n onMarks({\n ...marks,\n order: [...order, \"redaction\"],\n redactions: [...marks.redactions, { x: Math.min(drag.start.x, end.x), y: Math.min(drag.start.y, end.y), width: Math.abs(end.x - drag.start.x), height: Math.abs(end.y - drag.start.y) }],\n });\n };\n const pct = (n: number) => `${n * 100}%`;\n const strokes = drag && tool === \"pen\" ? [...marks.strokes, drag.points] : marks.strokes;\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"genesis-support-screenshot\">\n <div className=\"flex items-center justify-between gap-2\">\n <Label>{copy.feedback.screenshotLabel}</Label>\n <div className=\"flex gap-2\">\n <ToggleGroup type=\"single\" size=\"sm\" value={tool} onValueChange={(v: string) => v && setTool(v as \"pen\" | \"hide\")}>\n <ToggleGroupItem value=\"pen\">{copy.annotate.pen}</ToggleGroupItem>\n <ToggleGroupItem value=\"hide\">{copy.annotate.hide}</ToggleGroupItem>\n </ToggleGroup>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={!marks.strokes.length && !marks.redactions.length}\n onClick={() => onMarks(undoMark(marks))}\n data-testid=\"genesis-support-undo\"\n >\n {copy.annotate.undo}\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onRemove} data-testid=\"genesis-support-remove-screenshot\">\n {copy.feedback.removeScreenshot}\n </Button>\n </div>\n </div>\n <div\n className=\"relative cursor-crosshair touch-none overflow-hidden rounded-md border\"\n data-testid=\"genesis-support-screenshot-surface\"\n onPointerDown={(e) => {\n // Keep the gesture when the pointer leaves the image, so a release outside still ends it.\n e.currentTarget.setPointerCapture?.(e.pointerId);\n const p = point(e);\n setDrag({ start: p, points: [p] });\n }}\n onPointerCancel={() => setDrag(null)}\n onPointerMove={(e) => drag && setDrag({ ...drag, points: tool === \"pen\" ? [...drag.points, point(e)] : [drag.start, point(e)] })}\n onPointerUp={(e) => finish(point(e))}\n >\n <img src={shot.dataUrl} alt={copy.feedback.screenshotLabel} className=\"block w-full select-none\" draggable={false} />\n <svg className=\"pointer-events-none absolute inset-0 h-full w-full\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\" aria-hidden>\n {strokes.map((stroke, i) => (\n <polyline\n key={i}\n points={stroke.map((p) => `${p.x},${p.y}`).join(\" \")}\n fill=\"none\"\n stroke=\"hsl(var(--destructive))\"\n strokeWidth={0.006}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n ))}\n </svg>\n {drag && tool === \"hide\" ? <DraftBox drag={drag} /> : null}\n {marks.redactions.map((r, i) => (\n <div\n key={i}\n data-testid={`genesis-support-redaction-${i}`}\n className=\"pointer-events-none absolute bg-foreground\"\n style={{ left: pct(r.x), top: pct(r.y), width: pct(r.width), height: pct(r.height) }}\n />\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">{copy.feedback.screenshotHint}</p>\n </div>\n );\n}\n","/**\n * The feedback launcher: a floating button that opens `FeedbackPanel` in a\n * Genesis UI Sheet, and `mountSupportWidget`, the loader that puts it on any\n * page with one call (for a page that isn't a React app of its own).\n */\nimport { useMemo, useState } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from \"@marktiderman/genesis-ui\";\nimport { resolveSupportCopy } from \"../copy\";\nimport { FeedbackPanel, type FeedbackPanelProps } from \"./feedback-panel\";\n\nexport type LauncherPosition = \"bottom-right\" | \"bottom-left\";\n\nexport interface FeedbackLauncherProps extends FeedbackPanelProps {\n position?: LauncherPosition;\n /** The button's text. Default the copy's \"Send feedback\". */\n label?: string;\n}\n\nconst POSITIONS: Record<LauncherPosition, string> = {\n \"bottom-right\": \"fixed bottom-4 right-4 z-50\",\n \"bottom-left\": \"fixed bottom-4 left-4 z-50\",\n};\n\nexport function FeedbackLauncher({ position = \"bottom-right\", label, ...panel }: FeedbackLauncherProps) {\n const copy = useMemo(() => resolveSupportCopy(panel.copy), [panel.copy]);\n const [open, setOpen] = useState(false);\n // While the person picks a pin or the browser asks to capture, the sheet steps aside.\n const [hidden, setHidden] = useState(false);\n return (\n <>\n <Button className={POSITIONS[position]} onClick={() => setOpen(true)} data-testid=\"genesis-support-launcher\" aria-haspopup=\"dialog\">\n {label ?? copy.settings.sendFeedback}\n </Button>\n <Sheet open={open && !hidden} onOpenChange={setOpen}>\n <SheetContent side=\"right\" className=\"flex w-full flex-col gap-4 overflow-y-auto sm:max-w-md\">\n <SheetHeader>\n <SheetTitle>{copy.feedback.title}</SheetTitle>\n <SheetDescription>{copy.feedback.messagePlaceholder}</SheetDescription>\n </SheetHeader>\n <FeedbackPanel {...panel} onHide={setHidden} />\n </SheetContent>\n </Sheet>\n </>\n );\n}\n\n/**\n * Mount the launcher into the page: into `container`, or a new element at the\n * end of `<body>`. Returns the unmount. The page needs Genesis UI's styles.\n */\nexport function mountSupportWidget(props: FeedbackLauncherProps & { container?: HTMLElement }): () => void {\n const { container, ...launcher } = props;\n const host = container ?? document.body.appendChild(document.createElement(\"div\"));\n host.setAttribute(\"data-genesis-support\", \"\");\n const root = createRoot(host);\n root.render(<FeedbackLauncher {...launcher} />);\n return () => {\n root.unmount();\n if (!container) host.remove();\n };\n}\n"]}
|