spaps-issue-reporting-react 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,933 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FloatingIssueReportButton: () => FloatingIssueReportButton,
34
+ IssueReportingProvider: () => IssueReportingProvider,
35
+ ReportModeContext: () => ReportModeContext,
36
+ ReportableSection: () => ReportableSection,
37
+ defaultIssueReportingCopy: () => defaultIssueReportingCopy,
38
+ filterIssueReports: () => filterIssueReports,
39
+ getEntryPointClassName: () => getEntryPointClassName,
40
+ getEntryPointState: () => getEntryPointState,
41
+ getIssueNoteLengthMessage: () => getIssueNoteLengthMessage,
42
+ getIssueStatusBadgeLabel: () => getIssueStatusBadgeLabel,
43
+ getIssueStatusClassName: () => getIssueStatusClassName,
44
+ isClosedIssueStatus: () => isClosedIssueStatus,
45
+ isOpenIssueStatus: () => isOpenIssueStatus,
46
+ issueReportingKeys: () => issueReportingKeys,
47
+ useIssueReporting: () => useIssueReporting,
48
+ useIssueReportingHistory: () => useIssueReportingHistory,
49
+ useIssueReportingMutations: () => useIssueReportingMutations,
50
+ useIssueReportingStatus: () => useIssueReportingStatus,
51
+ useReportMode: () => useReportMode
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/components.tsx
56
+ var Dialog = __toESM(require("@radix-ui/react-dialog"));
57
+ var Popover = __toESM(require("@radix-ui/react-popover"));
58
+ var import_react3 = require("@phosphor-icons/react");
59
+ var import_date_fns = require("date-fns");
60
+ var import_react4 = require("react");
61
+
62
+ // src/provider.tsx
63
+ var import_react2 = require("react");
64
+ var import_react_query = require("@tanstack/react-query");
65
+
66
+ // src/report-mode.ts
67
+ var import_react = require("react");
68
+ var ReportModeContext = (0, import_react.createContext)(
69
+ null
70
+ );
71
+ function useReportMode() {
72
+ return (0, import_react.useContext)(ReportModeContext);
73
+ }
74
+
75
+ // src/provider.tsx
76
+ var import_jsx_runtime = require("react/jsx-runtime");
77
+ var LIST_LIMIT = 200;
78
+ var NOTE_MIN_LENGTH = 10;
79
+ var NOTE_MAX_LENGTH = 2e3;
80
+ var initialModalState = {
81
+ isOpen: false,
82
+ mode: "create",
83
+ issueReportId: null,
84
+ issue: null,
85
+ target: null,
86
+ error: null,
87
+ isHydrating: false
88
+ };
89
+ var IssueReportingContext = (0, import_react2.createContext)(
90
+ void 0
91
+ );
92
+ var defaultIssueReportingCopy = {
93
+ entryAriaLabel: "Report issue",
94
+ popoverTitle: "Issue Reports",
95
+ reportNewAction: "Report New Issue",
96
+ filtersAll: "All",
97
+ filtersOpen: "Open",
98
+ filtersClosed: "Closed",
99
+ historyHelpText: "We read every report. If something is broken or could work better, report it here and it goes straight to the people building the app.",
100
+ historyLoading: "Loading issues...",
101
+ historyLoadFailed: "Failed to load issues",
102
+ emptyAll: "No issues reported yet",
103
+ emptyOpen: "No open issues",
104
+ emptyClosed: "No resolved or ignored issues",
105
+ reportModeTitle: "Report mode is active",
106
+ reportModeDescription: "Click a highlighted section to capture the broken surface.",
107
+ reportModeCancelAction: "Cancel",
108
+ createTitlePrefix: "Report Issue",
109
+ editTitlePrefix: "Edit Issue",
110
+ replyTitlePrefix: "Reply to",
111
+ createDescriptionPrefix: "Reporting issue for page:",
112
+ editDescription: "Update the issue description below.",
113
+ replyDescription: "The original issue was marked resolved or ignored. Describe why it still needs attention.",
114
+ notePlaceholder: "Describe the issue in detail (min 10 characters)...",
115
+ noteMinimumSuffix: "more characters needed",
116
+ keyboardShortcutHint: "Cmd/Ctrl + Enter to submit",
117
+ originalIssueLabel: "Original issue",
118
+ cancelAction: "Cancel",
119
+ submitAction: "Submit",
120
+ submittingAction: "Submitting...",
121
+ editAction: "Edit",
122
+ replyAction: "Reply",
123
+ hydrateLoading: "Loading the latest issue details...",
124
+ hydrateFailed: "Failed to load the latest issue details.",
125
+ retryAction: "Retry"
126
+ };
127
+ var issueReportingKeys = {
128
+ all: ["spaps-issue-reporting"],
129
+ status: () => [...issueReportingKeys.all, "status"],
130
+ history: () => [...issueReportingKeys.all, "history"],
131
+ detail: (issueReportId) => [...issueReportingKeys.all, "detail", issueReportId]
132
+ };
133
+ function resolvePageUrl(getPageUrl) {
134
+ if (getPageUrl) {
135
+ return getPageUrl();
136
+ }
137
+ if (typeof window === "undefined") {
138
+ return "/";
139
+ }
140
+ return `${window.location.pathname}${window.location.search}${window.location.hash}`;
141
+ }
142
+ function normalizeTarget(target, getPageUrl) {
143
+ if (typeof target === "string") {
144
+ const normalized = target.trim();
145
+ return {
146
+ component_key: normalized,
147
+ component_label: normalized,
148
+ page_url: resolvePageUrl(getPageUrl),
149
+ surface_ref: null,
150
+ metadata: {}
151
+ };
152
+ }
153
+ const key = target.componentKey.trim();
154
+ const label = (target.componentLabel ?? key).trim();
155
+ return {
156
+ component_key: key,
157
+ component_label: label,
158
+ page_url: resolvePageUrl(getPageUrl),
159
+ surface_ref: target.surfaceRef ?? null,
160
+ metadata: target.metadata ?? {}
161
+ };
162
+ }
163
+ function normalizeIssueTarget(issue) {
164
+ return {
165
+ component_key: issue.target.component_key,
166
+ component_label: issue.target.component_label,
167
+ page_url: issue.target.page_url,
168
+ surface_ref: issue.target.surface_ref ?? null,
169
+ metadata: issue.target.metadata ?? {}
170
+ };
171
+ }
172
+ function toErrorMessage(error, fallback) {
173
+ if (error instanceof Error && error.message.trim()) {
174
+ return error.message;
175
+ }
176
+ return fallback;
177
+ }
178
+ function isOpenIssueStatus(status) {
179
+ return status === "open" || status === "in_progress";
180
+ }
181
+ function isClosedIssueStatus(status) {
182
+ return status === "resolved" || status === "ignored";
183
+ }
184
+ function filterIssueReports(issues, filter) {
185
+ if (filter === "open") {
186
+ return issues.filter((issue) => isOpenIssueStatus(issue.status));
187
+ }
188
+ if (filter === "closed") {
189
+ return issues.filter((issue) => isClosedIssueStatus(issue.status));
190
+ }
191
+ return issues;
192
+ }
193
+ function getIssueStatusBadgeLabel(status) {
194
+ switch (status) {
195
+ case "in_progress":
196
+ return "In Progress";
197
+ case "resolved":
198
+ return "Resolved";
199
+ case "ignored":
200
+ return "Ignored";
201
+ default:
202
+ return "Open";
203
+ }
204
+ }
205
+ function getIssueStatusClassName(status) {
206
+ switch (status) {
207
+ case "in_progress":
208
+ return "bg-amber-100 text-amber-700";
209
+ case "resolved":
210
+ return "bg-emerald-100 text-emerald-700";
211
+ case "ignored":
212
+ return "bg-slate-200 text-slate-700";
213
+ default:
214
+ return "bg-rose-100 text-rose-700";
215
+ }
216
+ }
217
+ function getEntryPointState(status) {
218
+ if (!status) {
219
+ return "neutral";
220
+ }
221
+ if (status.has_open) {
222
+ return "open";
223
+ }
224
+ if (status.has_recent_resolved) {
225
+ return "recent_resolved";
226
+ }
227
+ return "neutral";
228
+ }
229
+ function getEntryPointClassName(state) {
230
+ if (state === "open") {
231
+ return "text-rose-600";
232
+ }
233
+ if (state === "recent_resolved") {
234
+ return "text-emerald-600";
235
+ }
236
+ return "text-slate-500";
237
+ }
238
+ function getIssueNoteLengthMessage(note, copy) {
239
+ if (note.length < NOTE_MIN_LENGTH) {
240
+ return `${NOTE_MIN_LENGTH - note.length} ${copy.noteMinimumSuffix}`;
241
+ }
242
+ return `${note.length}/${NOTE_MAX_LENGTH}`;
243
+ }
244
+ function useIssueReporting() {
245
+ const context = (0, import_react2.useContext)(IssueReportingContext);
246
+ if (!context) {
247
+ throw new Error(
248
+ "useIssueReporting must be used within an IssueReportingProvider"
249
+ );
250
+ }
251
+ return context;
252
+ }
253
+ function useIssueReportingStatus() {
254
+ const { client, isEligible } = useIssueReporting();
255
+ return (0, import_react_query.useQuery)({
256
+ queryKey: issueReportingKeys.status(),
257
+ queryFn: () => client.issueReporting.getStatus(),
258
+ enabled: isEligible,
259
+ retry: false,
260
+ staleTime: 3e4
261
+ });
262
+ }
263
+ function useIssueReportingHistory(filter = "all") {
264
+ const { client, isEligible } = useIssueReporting();
265
+ const query = (0, import_react_query.useQuery)({
266
+ queryKey: issueReportingKeys.history(),
267
+ queryFn: () => client.issueReporting.list({ limit: LIST_LIMIT, offset: 0 }),
268
+ enabled: isEligible,
269
+ retry: false
270
+ });
271
+ const items = (0, import_react2.useMemo)(
272
+ () => filterIssueReports(query.data?.items ?? [], filter),
273
+ [filter, query.data?.items]
274
+ );
275
+ return {
276
+ ...query,
277
+ items,
278
+ total: items.length
279
+ };
280
+ }
281
+ function useIssueReportingMutations() {
282
+ const queryClient = (0, import_react_query.useQueryClient)();
283
+ const { client } = useIssueReporting();
284
+ const invalidateAll = (0, import_react2.useCallback)(async () => {
285
+ await queryClient.invalidateQueries({
286
+ queryKey: issueReportingKeys.all
287
+ });
288
+ }, [queryClient]);
289
+ const createMutation = (0, import_react_query.useMutation)({
290
+ mutationFn: (payload) => client.issueReporting.create(payload),
291
+ onSuccess: invalidateAll
292
+ });
293
+ const updateMutation = (0, import_react_query.useMutation)({
294
+ mutationFn: ({
295
+ issueReportId,
296
+ note
297
+ }) => client.issueReporting.update(issueReportId, { note }),
298
+ onSuccess: invalidateAll
299
+ });
300
+ const replyMutation = (0, import_react_query.useMutation)({
301
+ mutationFn: ({
302
+ issueReportId,
303
+ note,
304
+ reporterRoleHint
305
+ }) => client.issueReporting.reply(issueReportId, {
306
+ note,
307
+ reporter_role_hint: reporterRoleHint
308
+ }),
309
+ onSuccess: invalidateAll
310
+ });
311
+ return {
312
+ createMutation,
313
+ updateMutation,
314
+ replyMutation
315
+ };
316
+ }
317
+ function IssueReportingProvider({
318
+ client,
319
+ isEligible,
320
+ reporterRoleHint,
321
+ getPageUrl,
322
+ copy,
323
+ children
324
+ }) {
325
+ const mergedCopy = (0, import_react2.useMemo)(
326
+ () => ({
327
+ ...defaultIssueReportingCopy,
328
+ ...copy
329
+ }),
330
+ [copy]
331
+ );
332
+ const queryClient = (0, import_react_query.useQueryClient)();
333
+ const [isReportMode, setIsReportMode] = (0, import_react2.useState)(false);
334
+ const [isPopoverOpen, setIsPopoverOpen] = (0, import_react2.useState)(false);
335
+ const [modalState, setModalState] = (0, import_react2.useState)(initialModalState);
336
+ const closePopover = (0, import_react2.useCallback)(() => {
337
+ setIsPopoverOpen(false);
338
+ }, []);
339
+ const openPopover = (0, import_react2.useCallback)(() => {
340
+ setIsPopoverOpen(true);
341
+ }, []);
342
+ const enterReportMode = (0, import_react2.useCallback)(() => {
343
+ setIsReportMode(true);
344
+ setIsPopoverOpen(false);
345
+ }, []);
346
+ const cancelReportMode = (0, import_react2.useCallback)(() => {
347
+ setIsReportMode(false);
348
+ }, []);
349
+ const closeModal = (0, import_react2.useCallback)(() => {
350
+ setModalState(initialModalState);
351
+ setIsPopoverOpen(true);
352
+ }, []);
353
+ const hydrateIssueIntoModal = (0, import_react2.useCallback)(
354
+ async (issueReportId, mode) => {
355
+ setModalState({
356
+ isOpen: true,
357
+ mode,
358
+ issueReportId,
359
+ issue: null,
360
+ target: null,
361
+ error: null,
362
+ isHydrating: true
363
+ });
364
+ try {
365
+ const issue = await queryClient.fetchQuery({
366
+ queryKey: issueReportingKeys.detail(issueReportId),
367
+ queryFn: () => client.issueReporting.get(issueReportId)
368
+ });
369
+ setModalState({
370
+ isOpen: true,
371
+ mode,
372
+ issueReportId,
373
+ issue,
374
+ target: normalizeIssueTarget(issue),
375
+ error: null,
376
+ isHydrating: false
377
+ });
378
+ } catch (error) {
379
+ setModalState({
380
+ isOpen: true,
381
+ mode,
382
+ issueReportId,
383
+ issue: null,
384
+ target: null,
385
+ error: toErrorMessage(error, mergedCopy.hydrateFailed),
386
+ isHydrating: false
387
+ });
388
+ }
389
+ },
390
+ [client.issueReporting, mergedCopy.hydrateFailed, queryClient]
391
+ );
392
+ const retryModalHydration = (0, import_react2.useCallback)(async () => {
393
+ if (!modalState.issueReportId || modalState.mode === "create") {
394
+ return;
395
+ }
396
+ await hydrateIssueIntoModal(modalState.issueReportId, modalState.mode);
397
+ }, [hydrateIssueIntoModal, modalState.issueReportId, modalState.mode]);
398
+ const openExistingIssueModal = (0, import_react2.useCallback)(
399
+ async (issueReportId, mode) => {
400
+ setIsPopoverOpen(false);
401
+ await hydrateIssueIntoModal(issueReportId, mode);
402
+ },
403
+ [hydrateIssueIntoModal]
404
+ );
405
+ const selectPanel = (0, import_react2.useCallback)(
406
+ (target) => {
407
+ if (!isEligible) {
408
+ return;
409
+ }
410
+ const normalizedTarget = normalizeTarget(target, getPageUrl);
411
+ setIsReportMode(false);
412
+ setModalState({
413
+ isOpen: true,
414
+ mode: "create",
415
+ issueReportId: null,
416
+ issue: null,
417
+ target: normalizedTarget,
418
+ error: null,
419
+ isHydrating: false
420
+ });
421
+ },
422
+ [getPageUrl, isEligible]
423
+ );
424
+ const contextValue = (0, import_react2.useMemo)(
425
+ () => ({
426
+ client,
427
+ isEligible,
428
+ reporterRoleHint,
429
+ copy: mergedCopy,
430
+ isReportMode,
431
+ enterReportMode,
432
+ cancelReportMode,
433
+ selectPanel,
434
+ isPopoverOpen,
435
+ openPopover,
436
+ closePopover,
437
+ modalState,
438
+ closeModal,
439
+ openExistingIssueModal,
440
+ retryModalHydration
441
+ }),
442
+ [
443
+ cancelReportMode,
444
+ client,
445
+ closeModal,
446
+ closePopover,
447
+ enterReportMode,
448
+ isEligible,
449
+ isPopoverOpen,
450
+ isReportMode,
451
+ mergedCopy,
452
+ modalState,
453
+ openExistingIssueModal,
454
+ openPopover,
455
+ reporterRoleHint,
456
+ retryModalHydration,
457
+ selectPanel
458
+ ]
459
+ );
460
+ const reportModeValue = (0, import_react2.useMemo)(
461
+ () => ({
462
+ isReportMode,
463
+ selectPanel,
464
+ cancelReportMode
465
+ }),
466
+ [cancelReportMode, isReportMode, selectPanel]
467
+ );
468
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IssueReportingContext.Provider, { value: contextValue, children }) });
469
+ }
470
+
471
+ // src/components.tsx
472
+ var import_jsx_runtime2 = require("react/jsx-runtime");
473
+ function cn(...values) {
474
+ return values.filter(Boolean).join(" ");
475
+ }
476
+ function truncate(value, max = 80) {
477
+ if (value.length <= max) {
478
+ return value;
479
+ }
480
+ return `${value.slice(0, max - 3)}...`;
481
+ }
482
+ function formatRelativeTime(timestamp) {
483
+ return (0, import_date_fns.formatDistanceToNow)(new Date(timestamp), {
484
+ addSuffix: true
485
+ });
486
+ }
487
+ function IssueReportModeBanner() {
488
+ const { copy } = useIssueReporting();
489
+ const reportMode = useReportMode();
490
+ if (!reportMode?.isReportMode) {
491
+ return null;
492
+ }
493
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fixed inset-x-4 top-4 z-[70] flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-w-xl rounded-full border border-amber-300 bg-amber-50/95 px-4 py-3 text-sm text-amber-950 shadow-lg backdrop-blur", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
494
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "font-medium", children: copy.reportModeTitle }),
495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-amber-900/80", children: copy.reportModeDescription }),
496
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
497
+ "button",
498
+ {
499
+ type: "button",
500
+ className: "rounded-full border border-amber-400 px-3 py-1 font-medium text-amber-900 transition hover:bg-amber-100",
501
+ onClick: reportMode.cancelReportMode,
502
+ children: copy.reportModeCancelAction
503
+ }
504
+ )
505
+ ] }) }) });
506
+ }
507
+ function IssueList({
508
+ filter,
509
+ onEdit,
510
+ onReply
511
+ }) {
512
+ const { copy } = useIssueReporting();
513
+ const history = useIssueReportingHistory(filter);
514
+ if (history.isPending) {
515
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-5 text-sm text-slate-600", children: [
516
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Spinner, { className: "h-4 w-4 animate-spin" }),
517
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: copy.historyLoading })
518
+ ] });
519
+ }
520
+ if (history.error) {
521
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: history.error.message || copy.historyLoadFailed }),
523
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
524
+ "button",
525
+ {
526
+ type: "button",
527
+ className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
528
+ onClick: () => history.refetch(),
529
+ children: copy.retryAction
530
+ }
531
+ )
532
+ ] });
533
+ }
534
+ if (history.items.length === 0) {
535
+ const emptyMessage = filter === "open" ? copy.emptyOpen : filter === "closed" ? copy.emptyClosed : copy.emptyAll;
536
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500", children: emptyMessage });
537
+ }
538
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-h-80 space-y-2 overflow-y-auto pr-1", children: history.items.map((issue) => {
539
+ const isClosed = isClosedIssueStatus(issue.status);
540
+ const actionLabel = isClosed ? copy.replyAction : copy.editAction;
541
+ const onAction = () => isClosed ? onReply(issue.id) : onEdit(issue.id);
542
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
543
+ "div",
544
+ {
545
+ className: "rounded-2xl border border-slate-200 bg-white px-3 py-3 shadow-sm",
546
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-start gap-3", children: [
547
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-0.5 flex-shrink-0", children: isClosed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.CheckCircle, { className: "h-4 w-4 text-emerald-600", weight: "fill" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Circle, { className: "h-4 w-4 text-rose-600", weight: "fill" }) }),
548
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0 flex-1", children: [
549
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
550
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "min-w-0", children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "truncate text-sm font-semibold text-slate-900", children: issue.target.component_label }),
552
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [
553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
554
+ "span",
555
+ {
556
+ className: cn(
557
+ "rounded-full px-2 py-0.5 text-[11px] font-medium",
558
+ getIssueStatusClassName(issue.status)
559
+ ),
560
+ children: getIssueStatusBadgeLabel(issue.status)
561
+ }
562
+ ),
563
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-xs text-slate-400", children: formatRelativeTime(issue.updated_at) })
564
+ ] })
565
+ ] }),
566
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
567
+ "button",
568
+ {
569
+ type: "button",
570
+ className: "rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-50",
571
+ onClick: onAction,
572
+ children: actionLabel
573
+ }
574
+ )
575
+ ] }),
576
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-2 text-sm text-slate-600", children: truncate(issue.note) }),
577
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-2 truncate text-xs text-slate-400", children: issue.target.page_url })
578
+ ] })
579
+ ] })
580
+ },
581
+ issue.id
582
+ );
583
+ }) });
584
+ }
585
+ function IssueReportPopover({ children }) {
586
+ const [filter, setFilter] = (0, import_react4.useState)("all");
587
+ const {
588
+ copy,
589
+ isPopoverOpen,
590
+ openPopover,
591
+ closePopover,
592
+ enterReportMode,
593
+ openExistingIssueModal
594
+ } = useIssueReporting();
595
+ const history = useIssueReportingHistory("all");
596
+ const status = useIssueReportingStatus();
597
+ const allItems = history.data?.items ?? [];
598
+ const counts = (0, import_react4.useMemo)(
599
+ () => ({
600
+ all: allItems.length,
601
+ open: filterIssueReports(allItems, "open").length,
602
+ closed: filterIssueReports(allItems, "closed").length
603
+ }),
604
+ [allItems]
605
+ );
606
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
607
+ Popover.Root,
608
+ {
609
+ open: isPopoverOpen,
610
+ onOpenChange: (open) => open ? openPopover() : closePopover(),
611
+ children: [
612
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Popover.Trigger, { asChild: true, children }),
613
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
614
+ Popover.Content,
615
+ {
616
+ align: "end",
617
+ side: "top",
618
+ sideOffset: 12,
619
+ className: "z-[70] w-[360px] rounded-3xl border border-slate-200 bg-white p-4 shadow-[0_18px_48px_rgba(15,23,42,0.18)]",
620
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4", children: [
621
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [
622
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "text-sm font-semibold text-slate-900", children: copy.popoverTitle }),
624
+ status.data?.has_open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "mt-1 text-xs text-slate-500", children: [
625
+ status.data.open_count,
626
+ " open issue",
627
+ status.data.open_count === 1 ? "" : "s"
628
+ ] }) : null
629
+ ] }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
631
+ "button",
632
+ {
633
+ type: "button",
634
+ className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
635
+ onClick: () => {
636
+ closePopover();
637
+ enterReportMode();
638
+ },
639
+ children: copy.reportNewAction
640
+ }
641
+ )
642
+ ] }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex gap-2", children: [
644
+ ["all", copy.filtersAll, counts.all],
645
+ ["open", copy.filtersOpen, counts.open],
646
+ ["closed", copy.filtersClosed, counts.closed]
647
+ ].map(([value, label, count]) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
648
+ "button",
649
+ {
650
+ type: "button",
651
+ className: cn(
652
+ "rounded-full px-3 py-1.5 text-xs font-medium transition",
653
+ filter === value ? "bg-slate-900 text-white" : "bg-slate-100 text-slate-600 hover:bg-slate-200"
654
+ ),
655
+ onClick: () => setFilter(value),
656
+ children: [
657
+ label,
658
+ count > 0 ? ` (${count})` : ""
659
+ ]
660
+ },
661
+ value
662
+ )) }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
664
+ IssueList,
665
+ {
666
+ filter,
667
+ onEdit: (issueReportId) => openExistingIssueModal(issueReportId, "edit"),
668
+ onReply: (issueReportId) => openExistingIssueModal(issueReportId, "reply")
669
+ }
670
+ ),
671
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "border-t border-slate-100 pt-3 text-xs leading-relaxed text-slate-500", children: copy.historyHelpText })
672
+ ] })
673
+ }
674
+ ) })
675
+ ]
676
+ }
677
+ );
678
+ }
679
+ function IssueReportModal() {
680
+ const {
681
+ copy,
682
+ reporterRoleHint,
683
+ modalState,
684
+ closeModal,
685
+ retryModalHydration
686
+ } = useIssueReporting();
687
+ const { createMutation, updateMutation, replyMutation } = useIssueReportingMutations();
688
+ const [note, setNote] = (0, import_react4.useState)("");
689
+ const [submitError, setSubmitError] = (0, import_react4.useState)(null);
690
+ const { isOpen, mode, issue, target, isHydrating, error } = modalState;
691
+ (0, import_react4.useEffect)(() => {
692
+ if (!isOpen) {
693
+ setNote("");
694
+ setSubmitError(null);
695
+ return;
696
+ }
697
+ if (mode === "edit" && issue) {
698
+ setNote(issue.note);
699
+ } else {
700
+ setNote("");
701
+ }
702
+ setSubmitError(null);
703
+ }, [isOpen, mode, issue]);
704
+ const isValid = note.trim().length >= 10 && note.trim().length <= 2e3;
705
+ const isSubmitting = createMutation.isPending || updateMutation.isPending || replyMutation.isPending;
706
+ const title = mode === "create" ? `${copy.createTitlePrefix}: ${target?.component_label ?? ""}` : mode === "edit" ? `${copy.editTitlePrefix}: ${target?.component_label ?? ""}` : `${copy.replyTitlePrefix}: ${target?.component_label ?? ""}`;
707
+ const handleSubmit = async () => {
708
+ if (!target || !isValid || isSubmitting) {
709
+ return;
710
+ }
711
+ setSubmitError(null);
712
+ try {
713
+ const normalizedNote = note.trim();
714
+ if (mode === "create") {
715
+ await createMutation.mutateAsync({
716
+ target,
717
+ note: normalizedNote,
718
+ reporter_role_hint: reporterRoleHint
719
+ });
720
+ } else if (mode === "edit" && issue) {
721
+ await updateMutation.mutateAsync({
722
+ issueReportId: issue.id,
723
+ note: normalizedNote
724
+ });
725
+ } else if (mode === "reply" && issue) {
726
+ await replyMutation.mutateAsync({
727
+ issueReportId: issue.id,
728
+ note: normalizedNote,
729
+ reporterRoleHint
730
+ });
731
+ }
732
+ closeModal();
733
+ } catch (submissionError) {
734
+ setSubmitError(
735
+ submissionError instanceof Error ? submissionError.message : "Failed to submit issue report"
736
+ );
737
+ }
738
+ };
739
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Root, { open: isOpen, onOpenChange: (open) => !open && closeModal(), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Dialog.Portal, { children: [
740
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Overlay, { className: "fixed inset-0 z-[80] bg-slate-950/45 backdrop-blur-sm" }),
741
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Dialog.Content, { className: "fixed left-1/2 top-1/2 z-[81] w-[calc(100vw-2rem)] max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-[28px] border border-slate-200 bg-white p-6 shadow-[0_28px_80px_rgba(15,23,42,0.24)] focus:outline-none", children: [
742
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Title, { className: "text-lg font-semibold text-slate-950", children: title }),
743
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Description, { className: "mt-2 text-sm text-slate-600", children: mode === "create" && target ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
744
+ copy.createDescriptionPrefix,
745
+ " ",
746
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { className: "rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700", children: target.page_url })
747
+ ] }) : mode === "edit" ? copy.editDescription : copy.replyDescription }),
748
+ isHydrating ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600", children: [
749
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Spinner, { className: "h-4 w-4 animate-spin" }),
750
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: copy.hydrateLoading })
751
+ ] }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: error }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-2", children: [
754
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
755
+ "button",
756
+ {
757
+ type: "button",
758
+ className: "rounded-full border border-rose-300 px-3 py-1 font-medium transition hover:bg-rose-100",
759
+ onClick: () => retryModalHydration(),
760
+ children: copy.retryAction
761
+ }
762
+ ),
763
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
764
+ "button",
765
+ {
766
+ type: "button",
767
+ className: "rounded-full border border-slate-300 px-3 py-1 font-medium text-slate-700 transition hover:bg-slate-50",
768
+ onClick: closeModal,
769
+ children: copy.cancelAction
770
+ }
771
+ )
772
+ ] })
773
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
774
+ mode === "reply" && issue ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3", children: [
775
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: copy.originalIssueLabel }),
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-1 text-sm text-slate-700", children: issue.note })
777
+ ] }) : null,
778
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 space-y-2", children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
780
+ "textarea",
781
+ {
782
+ value: note,
783
+ onChange: (event) => setNote(event.target.value),
784
+ onKeyDown: (event) => {
785
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
786
+ event.preventDefault();
787
+ void handleSubmit();
788
+ }
789
+ },
790
+ placeholder: copy.notePlaceholder,
791
+ className: "h-36 w-full resize-none rounded-2xl border border-slate-300 px-4 py-3 text-sm text-slate-800 outline-none transition focus:border-slate-500 focus:ring-2 focus:ring-slate-200",
792
+ disabled: isSubmitting,
793
+ autoFocus: true
794
+ }
795
+ ),
796
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between text-xs text-slate-500", children: [
797
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: getIssueNoteLengthMessage(note, copy) }),
798
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: copy.keyboardShortcutHint })
799
+ ] })
800
+ ] }),
801
+ submitError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700", children: submitError }) : null,
802
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-6 flex justify-end gap-3", children: [
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
804
+ "button",
805
+ {
806
+ type: "button",
807
+ className: "rounded-full border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50",
808
+ onClick: closeModal,
809
+ disabled: isSubmitting,
810
+ children: copy.cancelAction
811
+ }
812
+ ),
813
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
814
+ "button",
815
+ {
816
+ type: "button",
817
+ className: cn(
818
+ "rounded-full px-4 py-2 text-sm font-semibold text-white transition",
819
+ isValid && !isSubmitting ? "bg-slate-900 hover:bg-slate-800" : "cursor-not-allowed bg-slate-300"
820
+ ),
821
+ onClick: () => void handleSubmit(),
822
+ disabled: !isValid || isSubmitting,
823
+ children: isSubmitting ? copy.submittingAction : copy.submitAction
824
+ }
825
+ )
826
+ ] })
827
+ ] }),
828
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Close, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
829
+ "button",
830
+ {
831
+ type: "button",
832
+ className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200 text-slate-500 transition hover:bg-slate-50 hover:text-slate-900",
833
+ "aria-label": "Close issue report modal",
834
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.X, { className: "h-4 w-4" })
835
+ }
836
+ ) })
837
+ ] })
838
+ ] }) });
839
+ }
840
+ function FloatingIssueReportButton({
841
+ className,
842
+ positionClassName
843
+ }) {
844
+ const {
845
+ copy,
846
+ isEligible,
847
+ isReportMode,
848
+ isPopoverOpen,
849
+ openPopover,
850
+ closePopover
851
+ } = useIssueReporting();
852
+ const status = useIssueReportingStatus();
853
+ const entryPointState = getEntryPointState(status.data);
854
+ if (!isEligible) {
855
+ return null;
856
+ }
857
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModeBanner, {}),
859
+ !isReportMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cn("fixed bottom-12 right-4 z-[65]", positionClassName), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
860
+ "button",
861
+ {
862
+ type: "button",
863
+ "aria-label": copy.entryAriaLabel,
864
+ onClick: () => isPopoverOpen ? closePopover() : openPopover(),
865
+ className: cn(
866
+ "flex h-12 w-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-slate-300",
867
+ status.isPending && "animate-pulse",
868
+ className
869
+ ),
870
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
871
+ import_react3.BugBeetle,
872
+ {
873
+ className: cn("h-6 w-6", getEntryPointClassName(entryPointState)),
874
+ weight: "fill"
875
+ }
876
+ )
877
+ }
878
+ ) }) }) : null,
879
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IssueReportModal, {})
880
+ ] });
881
+ }
882
+ function ReportableSection({
883
+ reportableName,
884
+ children,
885
+ className,
886
+ as: Component = "div"
887
+ }) {
888
+ const reportMode = useReportMode();
889
+ const isSelectable = Boolean(reportMode?.isReportMode);
890
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
891
+ Component,
892
+ {
893
+ className: cn(
894
+ className,
895
+ isSelectable && "cursor-pointer ring-2 ring-amber-400 transition hover:ring-amber-500"
896
+ ),
897
+ onClick: isSelectable ? () => {
898
+ reportMode?.selectPanel(reportableName);
899
+ } : void 0,
900
+ onKeyDown: isSelectable ? (event) => {
901
+ if (event.key === "Enter" || event.key === " ") {
902
+ event.preventDefault();
903
+ reportMode?.selectPanel(reportableName);
904
+ }
905
+ } : void 0,
906
+ role: isSelectable ? "button" : void 0,
907
+ tabIndex: isSelectable ? 0 : void 0,
908
+ children
909
+ }
910
+ );
911
+ }
912
+ // Annotate the CommonJS export names for ESM import in node:
913
+ 0 && (module.exports = {
914
+ FloatingIssueReportButton,
915
+ IssueReportingProvider,
916
+ ReportModeContext,
917
+ ReportableSection,
918
+ defaultIssueReportingCopy,
919
+ filterIssueReports,
920
+ getEntryPointClassName,
921
+ getEntryPointState,
922
+ getIssueNoteLengthMessage,
923
+ getIssueStatusBadgeLabel,
924
+ getIssueStatusClassName,
925
+ isClosedIssueStatus,
926
+ isOpenIssueStatus,
927
+ issueReportingKeys,
928
+ useIssueReporting,
929
+ useIssueReportingHistory,
930
+ useIssueReportingMutations,
931
+ useIssueReportingStatus,
932
+ useReportMode
933
+ });