@tra-bilisim/report-issue 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.
package/dist/index.js ADDED
@@ -0,0 +1,687 @@
1
+ import { createVideoRecorder, CONSENT_VERSION } from './chunk-5S66KGBW.js';
2
+ export { CONSENT_VERSION } from './chunk-5S66KGBW.js';
3
+ import { cn, useReportIssueConfig, Button, useReportIssue } from './chunk-JMQUG5Q7.js';
4
+ export { ReportIssueProvider, useReportIssue, useReportIssueConfig } from './chunk-JMQUG5Q7.js';
5
+ import { getConsoleLogs, getNetworkLogs } from './chunk-EXDFVVYA.js';
6
+ export { getConsoleLogs, getNetworkLogs, patchConsole } from './chunk-EXDFVVYA.js';
7
+ import { toggleVideoMask } from './chunk-ZYF6UFBB.js';
8
+ import { createPortal } from 'react-dom';
9
+ import { X, ShieldCheck, Camera, Video, Film, FileText, Circle, Square, MessageSquareWarning, Check, ChevronDown } from 'lucide-react';
10
+ import { forwardRef, lazy, useCallback, useState, useRef, useEffect, useMemo, Suspense } from 'react';
11
+ import * as RadixDialog from '@radix-ui/react-dialog';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
13
+ import * as RadixCheckbox from '@radix-ui/react-checkbox';
14
+ import * as Popover from '@radix-ui/react-popover';
15
+
16
+ var Dialog = RadixDialog.Root;
17
+ var DialogContent = forwardRef(
18
+ ({ size = "md", showClose = true, className, children, ...rest }, ref) => /* @__PURE__ */ jsxs(RadixDialog.Portal, { children: [
19
+ /* @__PURE__ */ jsx(RadixDialog.Overlay, { className: "rpi-dialog__overlay", "data-slot": "dialog-overlay" }),
20
+ /* @__PURE__ */ jsxs(
21
+ RadixDialog.Content,
22
+ {
23
+ ref,
24
+ className: cn(
25
+ "rpi-root",
26
+ "rpi-dialog__content",
27
+ size === "lg" && "rpi-dialog__content--lg",
28
+ size === "xl" && "rpi-dialog__content--xl",
29
+ className
30
+ ),
31
+ ...rest,
32
+ children: [
33
+ children,
34
+ showClose && /* @__PURE__ */ jsx(RadixDialog.Close, { className: "rpi-dialog__close", "aria-label": "Close", children: /* @__PURE__ */ jsx(X, { size: 16 }) })
35
+ ]
36
+ }
37
+ )
38
+ ] })
39
+ );
40
+ DialogContent.displayName = "RpiDialogContent";
41
+ var DialogHeader = ({ children }) => /* @__PURE__ */ jsx("div", { className: "rpi-dialog__header", children });
42
+ var DialogTitle = ({ children, className }) => /* @__PURE__ */ jsx(RadixDialog.Title, { className: cn("rpi-dialog__title", className), children });
43
+ var DialogDescription = ({ children }) => /* @__PURE__ */ jsx(RadixDialog.Description, { className: "rpi-dialog__desc", children });
44
+ var DialogFooter = ({ children }) => /* @__PURE__ */ jsx("div", { className: "rpi-dialog__footer", children });
45
+ var Input = forwardRef(
46
+ ({ textarea, className, rows, ...rest }, ref) => {
47
+ if (textarea) {
48
+ return /* @__PURE__ */ jsx(
49
+ "textarea",
50
+ {
51
+ ref,
52
+ rows,
53
+ className: cn("rpi-textarea", className),
54
+ ...rest
55
+ }
56
+ );
57
+ }
58
+ return /* @__PURE__ */ jsx(
59
+ "input",
60
+ {
61
+ ref,
62
+ className: cn("rpi-input", className),
63
+ ...rest
64
+ }
65
+ );
66
+ }
67
+ );
68
+ Input.displayName = "RpiInput";
69
+ var Checkbox = ({ id, checked, onCheckedChange, label }) => /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "rpi-check", children: [
70
+ /* @__PURE__ */ jsx(
71
+ RadixCheckbox.Root,
72
+ {
73
+ id,
74
+ checked,
75
+ onCheckedChange: (value) => onCheckedChange?.(value === true),
76
+ className: "rpi-check__box",
77
+ children: /* @__PURE__ */ jsx(RadixCheckbox.Indicator, { children: /* @__PURE__ */ jsx(Check, { size: 13, strokeWidth: 3 }) })
78
+ }
79
+ ),
80
+ label != null && /* @__PURE__ */ jsx("span", { className: "rpi-check__label", children: label })
81
+ ] });
82
+ var Select = ({
83
+ options,
84
+ value,
85
+ onChange,
86
+ placeholder,
87
+ isSearchable = false,
88
+ disabled = false,
89
+ t = (k) => k
90
+ }) => {
91
+ const [open, setOpen] = useState(false);
92
+ const [query, setQuery] = useState("");
93
+ const searchRef = useRef(null);
94
+ const selected = options.find((o) => o.value === value);
95
+ const filtered = useMemo(() => {
96
+ if (!isSearchable || !query.trim()) return options;
97
+ const q = query.toLowerCase();
98
+ return options.filter((o) => o.label.toLowerCase().includes(q));
99
+ }, [options, query, isSearchable]);
100
+ const commit = (next) => {
101
+ onChange(next === value ? void 0 : next);
102
+ setOpen(false);
103
+ setQuery("");
104
+ };
105
+ return /* @__PURE__ */ jsxs(
106
+ Popover.Root,
107
+ {
108
+ open,
109
+ onOpenChange: (next) => {
110
+ if (disabled) return;
111
+ setOpen(next);
112
+ if (!next) setQuery("");
113
+ },
114
+ children: [
115
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
116
+ "button",
117
+ {
118
+ type: "button",
119
+ disabled,
120
+ "data-placeholder": !selected,
121
+ className: "rpi-select__trigger",
122
+ children: [
123
+ /* @__PURE__ */ jsx("span", { children: selected ? selected.label : placeholder ?? "" }),
124
+ /* @__PURE__ */ jsx(ChevronDown, { size: 16 })
125
+ ]
126
+ }
127
+ ) }),
128
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
129
+ Popover.Content,
130
+ {
131
+ className: "rpi-root rpi-select__panel",
132
+ align: "start",
133
+ sideOffset: 4,
134
+ onOpenAutoFocus: (e) => {
135
+ if (isSearchable) {
136
+ e.preventDefault();
137
+ searchRef.current?.focus();
138
+ }
139
+ },
140
+ children: [
141
+ isSearchable && /* @__PURE__ */ jsx(
142
+ "input",
143
+ {
144
+ ref: searchRef,
145
+ value: query,
146
+ onChange: (e) => setQuery(e.target.value),
147
+ placeholder: t("Search"),
148
+ className: "rpi-select__search"
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx("div", { className: "rpi-select__list", children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rpi-select__empty", children: t("No results") }) : filtered.map((o) => /* @__PURE__ */ jsx(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ "data-selected": o.value === value,
156
+ className: cn("rpi-select__option"),
157
+ onClick: () => commit(o.value),
158
+ children: o.label
159
+ },
160
+ String(o.value)
161
+ )) })
162
+ ]
163
+ }
164
+ ) })
165
+ ]
166
+ }
167
+ );
168
+ };
169
+ function useReportIssueCapture() {
170
+ const collectConsoleLogs = useCallback(() => getConsoleLogs(), []);
171
+ const collectNetworkLogs = useCallback(() => getNetworkLogs(), []);
172
+ return { collectConsoleLogs, collectNetworkLogs };
173
+ }
174
+ function useVideoRecorder({ onComplete, onRecordingChange, maxDurationMs }) {
175
+ const [isRecording, setIsRecording] = useState(false);
176
+ const [elapsed, setElapsed] = useState(0);
177
+ const controllerRef = useRef(null);
178
+ const onCompleteRef = useRef(onComplete);
179
+ const onRecordingChangeRef = useRef(onRecordingChange);
180
+ useEffect(() => {
181
+ onCompleteRef.current = onComplete;
182
+ }, [onComplete]);
183
+ useEffect(() => {
184
+ onRecordingChangeRef.current = onRecordingChange;
185
+ }, [onRecordingChange]);
186
+ if (!controllerRef.current) {
187
+ controllerRef.current = createVideoRecorder(
188
+ {
189
+ onComplete: (file) => onCompleteRef.current(file),
190
+ onStateChange: (next) => {
191
+ setIsRecording(next);
192
+ onRecordingChangeRef.current?.(next);
193
+ },
194
+ onTick: (seconds) => setElapsed(seconds)
195
+ },
196
+ { maxDurationMs }
197
+ );
198
+ }
199
+ useEffect(() => () => controllerRef.current?.dispose(), []);
200
+ const start = useCallback(
201
+ () => controllerRef.current.start(),
202
+ []
203
+ );
204
+ const stop = useCallback(() => controllerRef.current.stop(), []);
205
+ return {
206
+ isRecording,
207
+ elapsed,
208
+ maxDuration: controllerRef.current.maxDurationSeconds,
209
+ start,
210
+ stop
211
+ };
212
+ }
213
+ var RecordingConsentDialog = ({ open, captureType = "video", onOpenChange, onAccept }) => {
214
+ const { t } = useReportIssueConfig();
215
+ const [checked, setChecked] = useState(false);
216
+ const isScreenshot = captureType === "screenshot";
217
+ useEffect(() => {
218
+ if (open) setChecked(false);
219
+ }, [open]);
220
+ const handleAccept = () => {
221
+ if (!checked) return;
222
+ onAccept({ accepted: true, version: CONSENT_VERSION, acceptedAt: (/* @__PURE__ */ new Date()).toISOString(), captureType });
223
+ };
224
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { "data-report-issue-dialog": true, "data-report-ignore-capture": true, size: "lg", children: [
225
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
226
+ /* @__PURE__ */ jsxs(DialogTitle, { children: [
227
+ /* @__PURE__ */ jsx(ShieldCheck, { size: 20, className: "rpi-icon-primary" }),
228
+ isScreenshot ? t("Screenshot Consent") : t("Screen Recording Consent")
229
+ ] }),
230
+ /* @__PURE__ */ jsx(DialogDescription, { children: isScreenshot ? t("Before the screenshot is taken, please review which data is collected and give your consent.") : t("Before the recording starts, please review which data is collected and give your consent.") })
231
+ ] }),
232
+ /* @__PURE__ */ jsxs("div", { className: "rpi-consent__body", children: [
233
+ /* @__PURE__ */ jsx("p", { children: isScreenshot ? t("To diagnose the issue you reported, a screenshot of this application screen will be captured. Sensitive on-screen data is masked before capture.") : t("To diagnose the issue you reported, a short screen recording of only this tab will be captured. All on-screen data is masked during recording; you may temporarily reveal it.") }),
234
+ /* @__PURE__ */ jsxs("div", { className: "rpi-consent__group", children: [
235
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: t("Data that will be collected:") }),
236
+ /* @__PURE__ */ jsxs("ul", { className: "rpi-consent__list", children: [
237
+ /* @__PURE__ */ jsx("li", { children: isScreenshot ? t("A masked screenshot of this application screen") : t("A masked screen recording limited to this application tab") }),
238
+ /* @__PURE__ */ jsx("li", { children: t("Console and network logs of this session") }),
239
+ /* @__PURE__ */ jsx("li", { children: t("Session metadata (user, roles, browser, page)") })
240
+ ] })
241
+ ] }),
242
+ /* @__PURE__ */ jsx("p", { className: "rpi-muted", children: t("This data is used solely to diagnose and resolve the reported issue and is shared only with the relevant technical team. You may withdraw your consent by not submitting the report.") })
243
+ ] }),
244
+ /* @__PURE__ */ jsx(
245
+ Checkbox,
246
+ {
247
+ id: "rpi-recording-consent",
248
+ checked,
249
+ onCheckedChange: setChecked,
250
+ label: t("I have read the above and give my explicit consent to the processing and sharing of this data.")
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
254
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: t("Cancel") }),
255
+ /* @__PURE__ */ jsx(Button, { disabled: !checked, onClick: handleAccept, children: isScreenshot ? t("Take Screenshot") : t("Start Recording") })
256
+ ] })
257
+ ] }) });
258
+ };
259
+ var RecordingConsentDialog_default = RecordingConsentDialog;
260
+ var AnnotationEditor = lazy(() => import('./AnnotationEditor-ILMYBTOG.js'));
261
+ var getAppOrigin = () => typeof window !== "undefined" ? window.location.origin : "";
262
+ var toAbsoluteUrl = (value) => {
263
+ if (!value) return "";
264
+ try {
265
+ return new URL(value, getAppOrigin()).href;
266
+ } catch {
267
+ return value;
268
+ }
269
+ };
270
+ var ReportIssueDialog = ({ open, onOpenChange }) => {
271
+ const {
272
+ config,
273
+ setIsCapturing,
274
+ isRecording,
275
+ setIsRecording,
276
+ setIsVideoMaskEnabled
277
+ } = useReportIssue();
278
+ const {
279
+ adapter,
280
+ t,
281
+ locale,
282
+ toast,
283
+ environmentResolver,
284
+ getCurrentUrl,
285
+ maxFiles,
286
+ maxFileSizeBytes,
287
+ maxRecordingSeconds
288
+ } = config;
289
+ const { collectConsoleLogs, collectNetworkLogs } = useReportIssueCapture();
290
+ const [view, setView] = useState("form");
291
+ const [editorImage, setEditorImage] = useState(null);
292
+ const [consentOpen, setConsentOpen] = useState(false);
293
+ const consentRef = useRef(null);
294
+ const [screenshotConsentOpen, setScreenshotConsentOpen] = useState(false);
295
+ const screenshotConsentRef = useRef(null);
296
+ const [attachments, setAttachments] = useState([]);
297
+ const [useCurrentPage, setUseCurrentPage] = useState(true);
298
+ const [customPage, setCustomPage] = useState("");
299
+ const [category, setCategory] = useState(null);
300
+ const [categoryOptions, setCategoryOptions] = useState([]);
301
+ const [description, setDescription] = useState("");
302
+ const [submitting, setSubmitting] = useState(false);
303
+ const fileInputRef = useRef(null);
304
+ const currentUrl = getCurrentUrl();
305
+ useEffect(() => {
306
+ if (!open || !adapter.getCategories) return;
307
+ let active = true;
308
+ Promise.resolve(adapter.getCategories()).then((list) => {
309
+ if (active) setCategoryOptions(Array.isArray(list) ? list : []);
310
+ }).catch(() => void 0);
311
+ return () => {
312
+ active = false;
313
+ };
314
+ }, [open, adapter]);
315
+ const localizedCategoryOptions = useMemo(
316
+ () => [...categoryOptions].map((o) => ({ label: t(o.label), value: o.value })).sort((a, b) => a.label.localeCompare(b.label, locale, { sensitivity: "base" })),
317
+ [categoryOptions, locale, t]
318
+ );
319
+ const pageOptions = useMemo(() => {
320
+ const raw = typeof adapter.pageOptions === "function" ? adapter.pageOptions() : adapter.pageOptions;
321
+ return (raw ?? []).slice().sort((a, b) => a.label.localeCompare(b.label));
322
+ }, [adapter]);
323
+ const detectKind = (file) => {
324
+ if (file.type.startsWith("image/")) return "image";
325
+ if (file.type.startsWith("video/")) return "video";
326
+ return "other";
327
+ };
328
+ const addFiles = useCallback((files, origin = "upload") => {
329
+ setAttachments((prev) => {
330
+ const next = [...prev];
331
+ let limitHit = false;
332
+ files.forEach((file) => {
333
+ if (next.length >= maxFiles) {
334
+ limitHit = true;
335
+ return;
336
+ }
337
+ if (file.size > maxFileSizeBytes) {
338
+ toast.error(t("{name} exceeds the size limit.").replace("{name}", file.name));
339
+ return;
340
+ }
341
+ next.push({ id: `${file.name}-${Date.now()}-${Math.random()}`, file, url: URL.createObjectURL(file), kind: detectKind(file), origin });
342
+ });
343
+ if (limitHit) toast.error(t("You can attach at most {count} files.").replace("{count}", String(maxFiles)));
344
+ return next;
345
+ });
346
+ }, [maxFiles, maxFileSizeBytes, t, toast]);
347
+ const removeAttachment = (id) => {
348
+ setAttachments((prev) => {
349
+ const target = prev.find((a) => a.id === id);
350
+ if (target) URL.revokeObjectURL(target.url);
351
+ return prev.filter((a) => a.id !== id);
352
+ });
353
+ };
354
+ const captureScreenshot = useCallback(async () => {
355
+ setIsCapturing(true);
356
+ try {
357
+ const { captureMaskedScreenshot } = await import('./screenshot-BQPXCSLD.js');
358
+ const dataUrl = await captureMaskedScreenshot();
359
+ setEditorImage(dataUrl);
360
+ await new Promise((resolve) => {
361
+ requestAnimationFrame(() => {
362
+ requestAnimationFrame(() => resolve());
363
+ });
364
+ });
365
+ setView("editor");
366
+ } catch {
367
+ toast.error(t("Failed to capture screenshot."));
368
+ } finally {
369
+ setIsCapturing(false);
370
+ }
371
+ }, [setIsCapturing, t, toast]);
372
+ const handleScreenshotConsentAccept = useCallback((consent) => {
373
+ screenshotConsentRef.current = consent;
374
+ setScreenshotConsentOpen(false);
375
+ captureScreenshot();
376
+ }, [captureScreenshot]);
377
+ const handleEditorConfirm = useCallback((file) => {
378
+ addFiles([file], "screenshot");
379
+ setEditorImage(null);
380
+ setView("form");
381
+ }, [addFiles]);
382
+ const handleEditorCancel = useCallback(() => {
383
+ setEditorImage(null);
384
+ setView("form");
385
+ }, []);
386
+ const handleRecordingChange = useCallback((next) => {
387
+ setIsRecording(next);
388
+ if (!next) {
389
+ setIsVideoMaskEnabled(false);
390
+ toggleVideoMask(false);
391
+ }
392
+ }, [setIsRecording, setIsVideoMaskEnabled]);
393
+ const { isRecording: recording, elapsed, maxDuration, start, stop } = useVideoRecorder({
394
+ onComplete: (file) => {
395
+ addFiles([file], "recording");
396
+ onOpenChange(true);
397
+ },
398
+ onRecordingChange: handleRecordingChange,
399
+ maxDurationMs: maxRecordingSeconds * 1e3
400
+ });
401
+ const handleStartRecording = useCallback(async () => {
402
+ setIsVideoMaskEnabled(true);
403
+ toggleVideoMask(true);
404
+ onOpenChange(false);
405
+ const result = await start();
406
+ if (result !== "started") {
407
+ handleRecordingChange(false);
408
+ onOpenChange(true);
409
+ const message = result === "unsupported" ? t("Screen recording is only supported on Chrome and Edge browsers.") : result === "wrong-surface" ? t("Only this application tab can be recorded. Please share this tab.") : t("Screen recording permission was denied.");
410
+ toast.error(message);
411
+ }
412
+ }, [start, setIsVideoMaskEnabled, handleRecordingChange, onOpenChange, t, toast]);
413
+ const handleConsentAccept = useCallback((consent) => {
414
+ consentRef.current = consent;
415
+ setConsentOpen(false);
416
+ handleStartRecording();
417
+ }, [handleStartRecording]);
418
+ const resetForm = useCallback(() => {
419
+ setAttachments((prev) => {
420
+ prev.forEach((a) => URL.revokeObjectURL(a.url));
421
+ return [];
422
+ });
423
+ setUseCurrentPage(true);
424
+ setCustomPage("");
425
+ setCategory(null);
426
+ setDescription("");
427
+ setView("form");
428
+ setEditorImage(null);
429
+ consentRef.current = null;
430
+ screenshotConsentRef.current = null;
431
+ }, []);
432
+ const handleSubmit = useCallback(async () => {
433
+ const page = useCurrentPage ? currentUrl : toAbsoluteUrl(customPage.trim());
434
+ if (!page) {
435
+ toast.error(t("Please specify the page."));
436
+ return;
437
+ }
438
+ setSubmitting(true);
439
+ try {
440
+ const consoleLogs = collectConsoleLogs();
441
+ const networkLogs = collectNetworkLogs();
442
+ const environment = environmentResolver(page);
443
+ const hasRecording = attachments.some((a) => a.origin === "recording");
444
+ const hasScreenshot = attachments.some((a) => a.origin === "screenshot");
445
+ const recordingConsent = hasRecording ? consentRef.current : null;
446
+ const screenshotConsent = hasScreenshot ? screenshotConsentRef.current : null;
447
+ const baseMetadata = {
448
+ userAgent: navigator.userAgent,
449
+ screenResolution: `${window.screen.width}x${window.screen.height}`,
450
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
451
+ };
452
+ const provided = adapter.getMetadata ? await adapter.getMetadata() : {};
453
+ const metadata = { ...baseMetadata, ...provided };
454
+ const payload = {
455
+ files: attachments.map((a) => a.file),
456
+ page,
457
+ environment,
458
+ categoryId: category,
459
+ description: description.trim() || null,
460
+ consoleLogs,
461
+ networkLogs,
462
+ recordingConsent,
463
+ screenshotConsent,
464
+ metadata
465
+ };
466
+ const formData = new FormData();
467
+ attachments.forEach((a) => formData.append("files", a.file, a.file.name));
468
+ formData.append("page", page);
469
+ formData.append("environment", environment);
470
+ if (category) formData.append("categoryId", String(category));
471
+ if (description.trim()) formData.append("description", description.trim());
472
+ formData.append("consoleLogs", JSON.stringify(consoleLogs));
473
+ formData.append("networkLogs", JSON.stringify(networkLogs));
474
+ if (recordingConsent) formData.append("recordingConsent", JSON.stringify(recordingConsent));
475
+ if (screenshotConsent) formData.append("screenshotConsent", JSON.stringify(screenshotConsent));
476
+ formData.append("metadata", JSON.stringify(metadata));
477
+ const result = await adapter.submit(payload, formData);
478
+ if (result.ok) {
479
+ toast.success(t(result.message ?? "Your report has been submitted. Thank you!"));
480
+ resetForm();
481
+ onOpenChange(false);
482
+ } else {
483
+ toast.error(t(result.message ?? "An error occurred!"));
484
+ }
485
+ } catch {
486
+ toast.error(t("An error occurred!"));
487
+ } finally {
488
+ setSubmitting(false);
489
+ }
490
+ }, [
491
+ attachments,
492
+ useCurrentPage,
493
+ customPage,
494
+ currentUrl,
495
+ category,
496
+ description,
497
+ adapter,
498
+ environmentResolver,
499
+ collectConsoleLogs,
500
+ collectNetworkLogs,
501
+ onOpenChange,
502
+ resetForm,
503
+ t,
504
+ toast
505
+ ]);
506
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
507
+ /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (val) => {
508
+ if (!recording) onOpenChange(val);
509
+ }, children: /* @__PURE__ */ jsx(
510
+ DialogContent,
511
+ {
512
+ "data-report-issue-dialog": true,
513
+ "data-report-ignore-capture": true,
514
+ showClose: false,
515
+ className: view === "editor" ? "rpi-dialog__content--editor" : void 0,
516
+ children: view === "form" ? /* @__PURE__ */ jsxs(Fragment, { children: [
517
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
518
+ /* @__PURE__ */ jsx(DialogTitle, { children: t("Report an Issue") }),
519
+ /* @__PURE__ */ jsx(DialogDescription, { children: t("Describe the problem and attach screenshots or a screen recording.") })
520
+ ] }),
521
+ /* @__PURE__ */ jsxs("div", { className: "rpi-form-body", children: [
522
+ /* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
523
+ /* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: t("Attachments") }),
524
+ /* @__PURE__ */ jsxs("div", { className: "rpi-row-wrap", children: [
525
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => setScreenshotConsentOpen(true), children: [
526
+ /* @__PURE__ */ jsx(Camera, { size: 16 }),
527
+ t("Take a Screenshot")
528
+ ] }),
529
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => setConsentOpen(true), children: [
530
+ /* @__PURE__ */ jsx(Video, { size: 16 }),
531
+ t("Record Video")
532
+ ] }),
533
+ /* @__PURE__ */ jsx(
534
+ "input",
535
+ {
536
+ ref: fileInputRef,
537
+ type: "file",
538
+ multiple: true,
539
+ accept: "image/*,video/*",
540
+ style: { display: "none" },
541
+ onChange: (e) => {
542
+ if (e.target.files) addFiles(Array.from(e.target.files));
543
+ e.target.value = "";
544
+ }
545
+ }
546
+ )
547
+ ] }),
548
+ attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: "rpi-row-wrap", style: { marginTop: 4 }, children: attachments.map((a) => /* @__PURE__ */ jsxs("div", { className: "rpi-attach", children: [
549
+ a.kind === "image" && /* @__PURE__ */ jsx("img", { src: a.url, alt: a.file.name }),
550
+ a.kind === "video" && /* @__PURE__ */ jsx("video", { src: a.url }),
551
+ a.kind === "video" && /* @__PURE__ */ jsx(Film, { size: 24, className: "rpi-attach__badge" }),
552
+ a.kind === "other" && /* @__PURE__ */ jsx(FileText, { size: 28, className: "rpi-muted" }),
553
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeAttachment(a.id), className: "rpi-attach__remove", children: /* @__PURE__ */ jsx(X, { size: 12 }) })
554
+ ] }, a.id)) })
555
+ ] }),
556
+ /* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
557
+ /* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: t("The page where the issue occurred") }),
558
+ /* @__PURE__ */ jsx(
559
+ Checkbox,
560
+ {
561
+ id: "rpi-use-current-page",
562
+ checked: useCurrentPage,
563
+ onCheckedChange: (val) => {
564
+ setUseCurrentPage(val);
565
+ if (val) setCustomPage("");
566
+ },
567
+ label: `${t("Current page")}: ${currentUrl}`
568
+ }
569
+ ),
570
+ pageOptions.length > 0 && /* @__PURE__ */ jsx(
571
+ Select,
572
+ {
573
+ placeholder: t("Enter the page where the issue occurred"),
574
+ options: pageOptions,
575
+ value: customPage || void 0,
576
+ onChange: (val) => setCustomPage(typeof val === "string" ? val : ""),
577
+ isSearchable: true,
578
+ disabled: useCurrentPage,
579
+ t
580
+ }
581
+ )
582
+ ] }),
583
+ localizedCategoryOptions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
584
+ /* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: `${t("Category")} (${t("optional")})` }),
585
+ /* @__PURE__ */ jsx(
586
+ Select,
587
+ {
588
+ placeholder: t("Select"),
589
+ options: localizedCategoryOptions,
590
+ value: category ?? void 0,
591
+ onChange: (val) => setCategory(typeof val === "number" ? val : null),
592
+ t
593
+ }
594
+ )
595
+ ] }),
596
+ /* @__PURE__ */ jsxs("div", { className: "rpi-col", children: [
597
+ /* @__PURE__ */ jsx("span", { className: "rpi-field-label", children: `${t("Description")} (${t("optional")})` }),
598
+ /* @__PURE__ */ jsx(
599
+ Input,
600
+ {
601
+ textarea: true,
602
+ rows: 3,
603
+ placeholder: t("Describe what happened..."),
604
+ value: description,
605
+ onChange: (e) => setDescription(e.target.value),
606
+ "data-report-mask-ignore": true
607
+ }
608
+ )
609
+ ] })
610
+ ] }),
611
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
612
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: t("Cancel") }),
613
+ /* @__PURE__ */ jsx(Button, { loading: submitting, onClick: handleSubmit, children: t("Submit") })
614
+ ] })
615
+ ] }) : editorImage && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
616
+ AnnotationEditor,
617
+ {
618
+ imageDataUrl: editorImage,
619
+ onConfirm: handleEditorConfirm,
620
+ onCancel: handleEditorCancel
621
+ }
622
+ ) })
623
+ }
624
+ ) }),
625
+ /* @__PURE__ */ jsx(
626
+ RecordingConsentDialog_default,
627
+ {
628
+ open: consentOpen,
629
+ captureType: "video",
630
+ onOpenChange: setConsentOpen,
631
+ onAccept: handleConsentAccept
632
+ }
633
+ ),
634
+ /* @__PURE__ */ jsx(
635
+ RecordingConsentDialog_default,
636
+ {
637
+ open: screenshotConsentOpen,
638
+ captureType: "screenshot",
639
+ onOpenChange: setScreenshotConsentOpen,
640
+ onAccept: handleScreenshotConsentAccept
641
+ }
642
+ ),
643
+ (isRecording || recording) && createPortal(
644
+ /* @__PURE__ */ jsxs("div", { "data-report-mask-ignore": true, "data-report-ignore-capture": true, className: "rpi-root rpi-rec", children: [
645
+ /* @__PURE__ */ jsxs("div", { className: "rpi-rec__row", children: [
646
+ /* @__PURE__ */ jsxs("span", { className: "rpi-rec__label", children: [
647
+ /* @__PURE__ */ jsx(Circle, { size: 12, className: "rpi-rec__dot" }),
648
+ "REC"
649
+ ] }),
650
+ /* @__PURE__ */ jsx("span", { className: "rpi-rec__time", children: `00:${String(elapsed).padStart(2, "0")} / 00:${String(maxDuration).padStart(2, "0")}` })
651
+ ] }),
652
+ /* @__PURE__ */ jsxs(Button, { color: "error", size: "sm", onClick: stop, children: [
653
+ /* @__PURE__ */ jsx(Square, { size: 16 }),
654
+ t("Stop")
655
+ ] })
656
+ ] }),
657
+ document.body
658
+ )
659
+ ] });
660
+ };
661
+ var ReportIssueDialog_default = ReportIssueDialog;
662
+ var FloatingReportButton = () => {
663
+ const { isReportOpen, setIsReportOpen, isRecording, config } = useReportIssue();
664
+ const { t } = config;
665
+ return createPortal(
666
+ /* @__PURE__ */ jsxs(Fragment, { children: [
667
+ !isRecording && !isReportOpen && /* @__PURE__ */ jsx("div", { "data-report-ignore-capture": true, className: "rpi-root rpi-fab", children: /* @__PURE__ */ jsxs(
668
+ Button,
669
+ {
670
+ color: "error",
671
+ onClick: () => setIsReportOpen(true),
672
+ title: t("Report an Issue"),
673
+ children: [
674
+ t("Report an Issue"),
675
+ /* @__PURE__ */ jsx(MessageSquareWarning, { size: 18 }),
676
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "rpi-fab__dot" })
677
+ ]
678
+ }
679
+ ) }),
680
+ /* @__PURE__ */ jsx(ReportIssueDialog_default, { open: isReportOpen, onOpenChange: setIsReportOpen })
681
+ ] }),
682
+ document.body
683
+ );
684
+ };
685
+ var FloatingReportButton_default = FloatingReportButton;
686
+
687
+ export { FloatingReportButton_default as FloatingReportButton, RecordingConsentDialog_default as RecordingConsentDialog, ReportIssueDialog_default as ReportIssueDialog, useReportIssueCapture, useVideoRecorder };
@@ -0,0 +1,2 @@
1
+ export { captureMaskedScreenshot } from './chunk-KY2IRP36.js';
2
+ import './chunk-ZYF6UFBB.js';