@the-portland-company/devnotes 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,4514 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DevNotesButton: () => DevNotesButton,
24
+ DevNotesDiscussion: () => DevNotesDiscussion,
25
+ DevNotesDot: () => DevNotesDot,
26
+ DevNotesForm: () => DevNotesForm,
27
+ DevNotesMenu: () => DevNotesMenu,
28
+ DevNotesOverlay: () => DevNotesOverlay,
29
+ DevNotesProvider: () => DevNotesProvider,
30
+ DevNotesTaskList: () => DevNotesTaskList,
31
+ buildAiFixPayload: () => buildAiFixPayload,
32
+ buildCaptureContext: () => buildCaptureContext,
33
+ calculateBugPositionFromPoint: () => calculateBugPositionFromPoint,
34
+ createDevNotesClient: () => createDevNotesClient,
35
+ deriveRouteLabelFromUrl: () => deriveRouteLabelFromUrl,
36
+ detectBrowserName: () => detectBrowserName,
37
+ formatAiFixPayloadForCopy: () => formatAiFixPayloadForCopy,
38
+ normalizePageUrl: () => normalizePageUrl,
39
+ resolveBugReportCoordinates: () => resolveBugReportCoordinates,
40
+ useBugReportPosition: () => useBugReportPosition,
41
+ useDevNotes: () => useDevNotes
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/DevNotesProvider.tsx
46
+ var import_react2 = require("react");
47
+
48
+ // src/hooks/useContainerOffset.ts
49
+ var import_react = require("react");
50
+ var sharedContainer = null;
51
+ var refCount = 0;
52
+ function syncBodyStyles(el) {
53
+ const bodyStyles = getComputedStyle(document.body);
54
+ el.style.fontFamily = bodyStyles.fontFamily;
55
+ el.style.color = bodyStyles.color;
56
+ el.style.fontSize = bodyStyles.fontSize;
57
+ el.style.lineHeight = bodyStyles.lineHeight;
58
+ }
59
+ function getOrCreateContainer() {
60
+ if (sharedContainer) return sharedContainer;
61
+ const el = document.createElement("div");
62
+ el.setAttribute("data-devnotes-layer", "");
63
+ el.style.cssText = [
64
+ "position:fixed",
65
+ "top:0",
66
+ "left:0",
67
+ "width:100vw",
68
+ "height:100vh",
69
+ "pointer-events:none",
70
+ "z-index:2147483646",
71
+ "overflow:visible"
72
+ ].join(";");
73
+ syncBodyStyles(el);
74
+ return el;
75
+ }
76
+ function useDevNotesContainer() {
77
+ const [container, setContainer] = (0, import_react.useState)(null);
78
+ const offsetRef = (0, import_react.useRef)({ x: 0, y: 0 });
79
+ (0, import_react.useEffect)(() => {
80
+ const el = getOrCreateContainer();
81
+ if (!sharedContainer) {
82
+ sharedContainer = el;
83
+ }
84
+ refCount++;
85
+ document.documentElement.appendChild(el);
86
+ setContainer(el);
87
+ const rect = el.getBoundingClientRect();
88
+ offsetRef.current = { x: rect.left, y: rect.top };
89
+ let rafId2 = null;
90
+ const recalc = () => {
91
+ const r = el.getBoundingClientRect();
92
+ offsetRef.current = { x: r.left, y: r.top };
93
+ syncBodyStyles(el);
94
+ };
95
+ const scheduleRecalc = () => {
96
+ if (rafId2 !== null) return;
97
+ rafId2 = window.requestAnimationFrame(() => {
98
+ rafId2 = null;
99
+ recalc();
100
+ });
101
+ };
102
+ const observer = new MutationObserver(scheduleRecalc);
103
+ observer.observe(document.documentElement, {
104
+ attributes: true,
105
+ attributeFilter: ["style", "class"]
106
+ });
107
+ observer.observe(document.body, {
108
+ attributes: true,
109
+ attributeFilter: ["style", "class"]
110
+ });
111
+ window.addEventListener("resize", scheduleRecalc);
112
+ window.addEventListener("scroll", scheduleRecalc, true);
113
+ document.addEventListener("scroll", scheduleRecalc, true);
114
+ window.visualViewport?.addEventListener("resize", scheduleRecalc);
115
+ window.visualViewport?.addEventListener("scroll", scheduleRecalc);
116
+ return () => {
117
+ observer.disconnect();
118
+ window.removeEventListener("resize", scheduleRecalc);
119
+ window.removeEventListener("scroll", scheduleRecalc, true);
120
+ document.removeEventListener("scroll", scheduleRecalc, true);
121
+ window.visualViewport?.removeEventListener("resize", scheduleRecalc);
122
+ window.visualViewport?.removeEventListener("scroll", scheduleRecalc);
123
+ if (rafId2 !== null) {
124
+ window.cancelAnimationFrame(rafId2);
125
+ rafId2 = null;
126
+ }
127
+ refCount--;
128
+ if (refCount <= 0) {
129
+ el.parentNode?.removeChild(el);
130
+ sharedContainer = null;
131
+ refCount = 0;
132
+ }
133
+ setContainer(null);
134
+ };
135
+ }, []);
136
+ const compensate = (0, import_react.useCallback)(
137
+ (viewportX, viewportY) => {
138
+ return {
139
+ x: viewportX - offsetRef.current.x,
140
+ y: viewportY - offsetRef.current.y
141
+ };
142
+ },
143
+ []
144
+ );
145
+ return { container, compensate };
146
+ }
147
+
148
+ // src/DevNotesProvider.tsx
149
+ var import_jsx_runtime = require("react/jsx-runtime");
150
+ var DevNotesContext = (0, import_react2.createContext)(null);
151
+ function DevNotesProvider({ adapter, user, config, children }) {
152
+ const { container: dotContainer, compensate } = useDevNotesContainer();
153
+ const storagePrefix = config?.storagePrefix || "devnotes";
154
+ const defaultGetPagePath = () => `${window.location.pathname}${window.location.search}`;
155
+ const getPagePathRef = (0, import_react2.useRef)(config?.getPagePath || defaultGetPagePath);
156
+ getPagePathRef.current = config?.getPagePath || defaultGetPagePath;
157
+ const getPagePath = (0, import_react2.useCallback)(() => getPagePathRef.current(), []);
158
+ const onNotify = config?.onNotify;
159
+ const aiProvider = config?.disableAi ? void 0 : config?.aiProvider;
160
+ const requireAi = Boolean(config?.requireAi && aiProvider);
161
+ const role = config?.role ?? "admin";
162
+ const SHOW_BUGS_ALWAYS_KEY = `${storagePrefix}_show_bugs_always`;
163
+ const HIDE_RESOLVED_CLOSED_KEY = `${storagePrefix}_hide_resolved_closed`;
164
+ const [isEnabled, setIsEnabled] = (0, import_react2.useState)(false);
165
+ const [showBugsAlways, setShowBugsAlwaysState] = (0, import_react2.useState)(() => {
166
+ try {
167
+ return localStorage.getItem(SHOW_BUGS_ALWAYS_KEY) === "true";
168
+ } catch {
169
+ return false;
170
+ }
171
+ });
172
+ const [hideResolvedClosed, setHideResolvedClosedState] = (0, import_react2.useState)(() => {
173
+ try {
174
+ const stored = localStorage.getItem(HIDE_RESOLVED_CLOSED_KEY);
175
+ return stored === null ? true : stored === "true";
176
+ } catch {
177
+ return true;
178
+ }
179
+ });
180
+ const [bugReports, setBugReports] = (0, import_react2.useState)([]);
181
+ const [bugReportTypes, setBugReportTypes] = (0, import_react2.useState)([]);
182
+ const [taskLists, setTaskLists] = (0, import_react2.useState)([]);
183
+ const [userProfiles, setUserProfiles] = (0, import_react2.useState)({});
184
+ const userProfilesRef = (0, import_react2.useRef)({});
185
+ const [unreadCounts, setUnreadCounts] = (0, import_react2.useState)({});
186
+ const [collaborators, setCollaborators] = (0, import_react2.useState)([]);
187
+ const [loading, setLoading] = (0, import_react2.useState)(false);
188
+ const [error, setError] = (0, import_react2.useState)(null);
189
+ const [capabilities, setCapabilities] = (0, import_react2.useState)({
190
+ ai: Boolean(aiProvider),
191
+ appLink: true
192
+ });
193
+ const [appLinkStatus, setAppLinkStatus] = (0, import_react2.useState)(null);
194
+ const [currentRoutePath, setCurrentRoutePath] = (0, import_react2.useState)(() => {
195
+ try {
196
+ return getPagePath();
197
+ } catch {
198
+ if (typeof window !== "undefined") return `${window.location.pathname}${window.location.search}`;
199
+ return "/";
200
+ }
201
+ });
202
+ const setShowBugsAlways = (0, import_react2.useCallback)(
203
+ (show) => {
204
+ setShowBugsAlwaysState(show);
205
+ try {
206
+ localStorage.setItem(SHOW_BUGS_ALWAYS_KEY, String(show));
207
+ } catch {
208
+ }
209
+ },
210
+ [SHOW_BUGS_ALWAYS_KEY]
211
+ );
212
+ const setHideResolvedClosed = (0, import_react2.useCallback)(
213
+ (hide) => {
214
+ setHideResolvedClosedState(hide);
215
+ try {
216
+ localStorage.setItem(HIDE_RESOLVED_CLOSED_KEY, String(hide));
217
+ } catch {
218
+ }
219
+ },
220
+ [HIDE_RESOLVED_CLOSED_KEY]
221
+ );
222
+ (0, import_react2.useEffect)(() => {
223
+ if (typeof window === "undefined") return void 0;
224
+ const updateRoutePath = () => {
225
+ try {
226
+ setCurrentRoutePath(getPagePath());
227
+ } catch {
228
+ setCurrentRoutePath(`${window.location.pathname}${window.location.search}`);
229
+ }
230
+ };
231
+ updateRoutePath();
232
+ const originalPushState = window.history.pushState;
233
+ const originalReplaceState = window.history.replaceState;
234
+ const patchedPushState = (...args) => {
235
+ originalPushState.apply(window.history, args);
236
+ updateRoutePath();
237
+ };
238
+ const patchedReplaceState = (...args) => {
239
+ originalReplaceState.apply(window.history, args);
240
+ updateRoutePath();
241
+ };
242
+ window.history.pushState = patchedPushState;
243
+ window.history.replaceState = patchedReplaceState;
244
+ window.addEventListener("popstate", updateRoutePath);
245
+ window.addEventListener("hashchange", updateRoutePath);
246
+ return () => {
247
+ window.history.pushState = originalPushState;
248
+ window.history.replaceState = originalReplaceState;
249
+ window.removeEventListener("popstate", updateRoutePath);
250
+ window.removeEventListener("hashchange", updateRoutePath);
251
+ };
252
+ }, [getPagePath]);
253
+ const visibleBugReports = (0, import_react2.useMemo)(() => {
254
+ if (role === "reporter") {
255
+ return bugReports.filter((report) => report.created_by === user.id);
256
+ }
257
+ return bugReports;
258
+ }, [bugReports, role, user.id]);
259
+ const currentPageBugReports = (0, import_react2.useMemo)(() => {
260
+ const toPath = (url) => url.split("#")[0].split("?")[0].replace(/\/+$/, "") || "/";
261
+ const currentPath = toPath(currentRoutePath);
262
+ return visibleBugReports.filter((report) => toPath(report.page_url) === currentPath);
263
+ }, [visibleBugReports, currentRoutePath]);
264
+ (0, import_react2.useEffect)(() => {
265
+ userProfilesRef.current = userProfiles;
266
+ }, [userProfiles]);
267
+ const loadProfilesForReports = (0, import_react2.useCallback)(
268
+ async (reports) => {
269
+ if (!reports.length) return;
270
+ const profileIds = /* @__PURE__ */ new Set();
271
+ reports.forEach((report) => {
272
+ if (report.created_by) profileIds.add(report.created_by);
273
+ if (report.assigned_to) profileIds.add(report.assigned_to);
274
+ if (report.resolved_by) profileIds.add(report.resolved_by);
275
+ });
276
+ const idsToFetch = Array.from(profileIds).filter((id) => !userProfilesRef.current[id]);
277
+ if (idsToFetch.length === 0) return;
278
+ try {
279
+ const profiles = await adapter.fetchProfiles(idsToFetch);
280
+ setUserProfiles((prev) => {
281
+ const next = { ...prev };
282
+ profiles.forEach((profile) => {
283
+ next[profile.id] = profile;
284
+ });
285
+ return next;
286
+ });
287
+ } catch (err) {
288
+ console.error("[DevNotes] Error loading profiles:", err);
289
+ }
290
+ },
291
+ [adapter]
292
+ );
293
+ const loadUnreadCounts = (0, import_react2.useCallback)(async () => {
294
+ try {
295
+ const counts = await adapter.fetchUnreadCounts();
296
+ setUnreadCounts(counts);
297
+ } catch (err) {
298
+ console.error("[DevNotes] Error loading unread counts:", err);
299
+ }
300
+ }, [adapter]);
301
+ const markMessagesAsRead = (0, import_react2.useCallback)(
302
+ async (reportId, messageIds) => {
303
+ if (messageIds.length === 0) return;
304
+ const uniqueIds = Array.from(new Set(messageIds));
305
+ try {
306
+ await adapter.markMessagesAsRead(uniqueIds);
307
+ setUnreadCounts((prev) => {
308
+ if (!prev[reportId]) return prev;
309
+ const next = { ...prev };
310
+ const nextValue = Math.max(0, (next[reportId] || 0) - uniqueIds.length);
311
+ if (nextValue === 0) {
312
+ delete next[reportId];
313
+ } else {
314
+ next[reportId] = nextValue;
315
+ }
316
+ return next;
317
+ });
318
+ } catch (err) {
319
+ console.error("[DevNotes] Error marking messages as read:", err);
320
+ }
321
+ },
322
+ [adapter]
323
+ );
324
+ const loadBugReports = (0, import_react2.useCallback)(async () => {
325
+ setLoading(true);
326
+ setError(null);
327
+ try {
328
+ const data = await adapter.fetchBugReports();
329
+ setBugReports(data);
330
+ await Promise.all([loadProfilesForReports(data), loadUnreadCounts()]);
331
+ } catch (err) {
332
+ console.error("[DevNotes] Error loading bug reports:", err);
333
+ setError(err.message);
334
+ } finally {
335
+ setLoading(false);
336
+ }
337
+ }, [adapter, loadProfilesForReports, loadUnreadCounts]);
338
+ const loadBugReportTypes = (0, import_react2.useCallback)(async () => {
339
+ try {
340
+ const data = await adapter.fetchBugReportTypes();
341
+ setBugReportTypes(data);
342
+ } catch (err) {
343
+ console.error("[DevNotes] Error loading bug report types:", err);
344
+ }
345
+ }, [adapter]);
346
+ const loadTaskLists = (0, import_react2.useCallback)(async () => {
347
+ try {
348
+ const data = await adapter.fetchTaskLists();
349
+ setTaskLists(data);
350
+ } catch (err) {
351
+ console.error("[DevNotes] Error loading task lists:", err);
352
+ }
353
+ }, [adapter]);
354
+ const loadCollaborators = (0, import_react2.useCallback)(async () => {
355
+ try {
356
+ const data = await adapter.fetchCollaborators();
357
+ setCollaborators(data);
358
+ } catch (err) {
359
+ console.error("[DevNotes] Error loading collaborators:", err);
360
+ }
361
+ }, [adapter]);
362
+ const refreshCapabilities = (0, import_react2.useCallback)(async () => {
363
+ try {
364
+ const data = await adapter.fetchCapabilities();
365
+ setCapabilities(data);
366
+ } catch (err) {
367
+ console.error("[DevNotes] Error loading capabilities:", err);
368
+ }
369
+ }, [adapter]);
370
+ const refreshAppLinkStatus = (0, import_react2.useCallback)(async () => {
371
+ try {
372
+ const data = await adapter.getAppLinkStatus();
373
+ setAppLinkStatus(data);
374
+ } catch (err) {
375
+ console.error("[DevNotes] Error loading app link status:", err);
376
+ setAppLinkStatus(null);
377
+ }
378
+ }, [adapter]);
379
+ const createBugReport = (0, import_react2.useCallback)(
380
+ async (report) => {
381
+ setLoading(true);
382
+ setError(null);
383
+ try {
384
+ const data = await adapter.createBugReport(report);
385
+ setBugReports((prev) => [data, ...prev]);
386
+ await loadProfilesForReports([data]);
387
+ return data;
388
+ } catch (err) {
389
+ console.error("[DevNotes] Error creating bug report:", err);
390
+ setError(err.message);
391
+ return null;
392
+ } finally {
393
+ setLoading(false);
394
+ }
395
+ },
396
+ [adapter, loadProfilesForReports]
397
+ );
398
+ const updateBugReport = (0, import_react2.useCallback)(
399
+ async (id, updates) => {
400
+ setLoading(true);
401
+ setError(null);
402
+ try {
403
+ const data = await adapter.updateBugReport(id, updates);
404
+ setBugReports(
405
+ (prev) => prev.map(
406
+ (report) => report.id === id ? { ...report, ...data, creator: report.creator || data.creator } : report
407
+ )
408
+ );
409
+ await loadProfilesForReports([data]);
410
+ if (updates.status === "Closed" && onNotify) {
411
+ const originalReport = bugReports.find((r) => r.id === id);
412
+ const authorEmail = originalReport?.creator?.email;
413
+ const authorName = originalReport?.creator?.full_name || "there";
414
+ const reportTitle = originalReport?.title || data.title || "Untitled";
415
+ const closerName = userProfilesRef.current[user.id]?.full_name || "A team member";
416
+ if (authorEmail && originalReport?.created_by !== user.id) {
417
+ onNotify({
418
+ type: "bug_closed",
419
+ recipientEmail: authorEmail,
420
+ subject: `Dev Note Closed: ${reportTitle}`,
421
+ textBody: `Hi ${authorName},
422
+
423
+ Your dev note "${reportTitle}" has been closed by ${closerName}.
424
+
425
+ Thank you,
426
+ Dev Notes`,
427
+ htmlBody: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
428
+ <h2 style="color: #333;">Dev Note Closed</h2>
429
+ <p>Hi ${authorName},</p>
430
+ <p>Your dev note <strong>"${reportTitle}"</strong> has been closed by <strong>${closerName}</strong>.</p>
431
+ <p>Thank you,<br>Dev Notes</p>
432
+ </div>`
433
+ });
434
+ }
435
+ }
436
+ return data;
437
+ } catch (err) {
438
+ console.error("[DevNotes] Error updating bug report:", err);
439
+ setError(err.message);
440
+ return null;
441
+ } finally {
442
+ setLoading(false);
443
+ }
444
+ },
445
+ [adapter, loadProfilesForReports, bugReports, user.id, onNotify]
446
+ );
447
+ const deleteBugReport = (0, import_react2.useCallback)(
448
+ async (id) => {
449
+ setLoading(true);
450
+ setError(null);
451
+ try {
452
+ await adapter.deleteBugReport(id);
453
+ setBugReports((prev) => prev.filter((report) => report.id !== id));
454
+ return true;
455
+ } catch (err) {
456
+ console.error("[DevNotes] Error deleting bug report:", err);
457
+ setError(err.message);
458
+ return false;
459
+ } finally {
460
+ setLoading(false);
461
+ }
462
+ },
463
+ [adapter]
464
+ );
465
+ const createTaskList = (0, import_react2.useCallback)(
466
+ async (name) => {
467
+ const trimmed = name.trim();
468
+ if (!trimmed) return null;
469
+ try {
470
+ const data = await adapter.createTaskList(trimmed);
471
+ setTaskLists((prev) => [...prev, data].sort((a, b) => a.name.localeCompare(b.name)));
472
+ return data;
473
+ } catch (err) {
474
+ console.error("[DevNotes] Error creating task list:", err);
475
+ return null;
476
+ }
477
+ },
478
+ [adapter]
479
+ );
480
+ const addBugReportType = (0, import_react2.useCallback)(
481
+ async (name) => {
482
+ try {
483
+ const data = await adapter.createBugReportType(name);
484
+ setBugReportTypes((prev) => [...prev, data].sort((a, b) => a.name.localeCompare(b.name)));
485
+ return data;
486
+ } catch (err) {
487
+ console.error("[DevNotes] Error adding bug report type:", err);
488
+ return null;
489
+ }
490
+ },
491
+ [adapter]
492
+ );
493
+ const deleteBugReportType = (0, import_react2.useCallback)(
494
+ async (id) => {
495
+ try {
496
+ await adapter.deleteBugReportType(id);
497
+ setBugReportTypes((prev) => prev.filter((type) => type.id !== id));
498
+ return true;
499
+ } catch (err) {
500
+ console.error("[DevNotes] Error deleting bug report type:", err);
501
+ return false;
502
+ }
503
+ },
504
+ [adapter]
505
+ );
506
+ (0, import_react2.useEffect)(() => {
507
+ loadBugReports();
508
+ loadBugReportTypes();
509
+ loadTaskLists();
510
+ loadCollaborators();
511
+ refreshCapabilities();
512
+ refreshAppLinkStatus();
513
+ }, [
514
+ loadBugReports,
515
+ loadBugReportTypes,
516
+ loadTaskLists,
517
+ loadCollaborators,
518
+ refreshCapabilities,
519
+ refreshAppLinkStatus
520
+ ]);
521
+ const value = (0, import_react2.useMemo)(
522
+ () => ({
523
+ isEnabled,
524
+ setIsEnabled,
525
+ showBugsAlways,
526
+ setShowBugsAlways,
527
+ hideResolvedClosed,
528
+ setHideResolvedClosed,
529
+ bugReports: visibleBugReports,
530
+ bugReportTypes,
531
+ taskLists,
532
+ userProfiles,
533
+ unreadCounts,
534
+ currentPageBugReports,
535
+ collaborators,
536
+ loadBugReports,
537
+ loadBugReportTypes,
538
+ loadTaskLists,
539
+ createBugReport,
540
+ updateBugReport,
541
+ deleteBugReport,
542
+ createTaskList,
543
+ addBugReportType,
544
+ deleteBugReportType,
545
+ loadUnreadCounts,
546
+ markMessagesAsRead,
547
+ user,
548
+ adapter,
549
+ capabilities,
550
+ appLinkStatus,
551
+ refreshCapabilities,
552
+ refreshAppLinkStatus,
553
+ onNotify,
554
+ aiProvider,
555
+ requireAi,
556
+ role,
557
+ loading,
558
+ error,
559
+ dotContainer,
560
+ compensate
561
+ }),
562
+ [
563
+ isEnabled,
564
+ setIsEnabled,
565
+ showBugsAlways,
566
+ setShowBugsAlways,
567
+ hideResolvedClosed,
568
+ setHideResolvedClosed,
569
+ visibleBugReports,
570
+ bugReportTypes,
571
+ taskLists,
572
+ userProfiles,
573
+ unreadCounts,
574
+ currentPageBugReports,
575
+ collaborators,
576
+ loadBugReports,
577
+ loadBugReportTypes,
578
+ loadTaskLists,
579
+ createBugReport,
580
+ updateBugReport,
581
+ deleteBugReport,
582
+ createTaskList,
583
+ addBugReportType,
584
+ deleteBugReportType,
585
+ loadUnreadCounts,
586
+ markMessagesAsRead,
587
+ user,
588
+ adapter,
589
+ capabilities,
590
+ appLinkStatus,
591
+ refreshCapabilities,
592
+ refreshAppLinkStatus,
593
+ onNotify,
594
+ aiProvider,
595
+ requireAi,
596
+ role,
597
+ loading,
598
+ error,
599
+ dotContainer,
600
+ compensate
601
+ ]
602
+ );
603
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DevNotesContext.Provider, { value, children });
604
+ }
605
+ var defaultContextValue = {
606
+ isEnabled: false,
607
+ setIsEnabled: () => {
608
+ },
609
+ showBugsAlways: false,
610
+ setShowBugsAlways: () => {
611
+ },
612
+ hideResolvedClosed: true,
613
+ setHideResolvedClosed: () => {
614
+ },
615
+ bugReports: [],
616
+ bugReportTypes: [],
617
+ taskLists: [],
618
+ userProfiles: {},
619
+ unreadCounts: {},
620
+ currentPageBugReports: [],
621
+ collaborators: [],
622
+ loadBugReports: async () => {
623
+ },
624
+ loadBugReportTypes: async () => {
625
+ },
626
+ loadTaskLists: async () => {
627
+ },
628
+ createBugReport: async () => null,
629
+ updateBugReport: async () => null,
630
+ deleteBugReport: async () => false,
631
+ createTaskList: async () => null,
632
+ addBugReportType: async () => null,
633
+ deleteBugReportType: async () => false,
634
+ loadUnreadCounts: async () => {
635
+ },
636
+ markMessagesAsRead: async () => {
637
+ },
638
+ user: { id: "", email: "" },
639
+ adapter: null,
640
+ capabilities: { ai: false, appLink: true },
641
+ appLinkStatus: null,
642
+ refreshCapabilities: async () => {
643
+ },
644
+ refreshAppLinkStatus: async () => {
645
+ },
646
+ aiProvider: void 0,
647
+ requireAi: false,
648
+ role: "none",
649
+ loading: false,
650
+ error: null,
651
+ dotContainer: null,
652
+ compensate: (vx, vy) => ({ x: vx, y: vy })
653
+ };
654
+ function useDevNotes() {
655
+ const context = (0, import_react2.useContext)(DevNotesContext);
656
+ return context || defaultContextValue;
657
+ }
658
+
659
+ // src/DevNotesButton.tsx
660
+ var import_react11 = require("react");
661
+ var import_react_dom2 = require("react-dom");
662
+
663
+ // src/DevNotesMenu.tsx
664
+ var import_react3 = require("react");
665
+ var import_fi = require("react-icons/fi");
666
+ var import_jsx_runtime2 = require("react/jsx-runtime");
667
+ function DevNotesMenu({ onViewTasks, onSettings, icon: IconComponent, position = "bottom-right", dropdownDirection = "down" }) {
668
+ const {
669
+ isEnabled,
670
+ setIsEnabled,
671
+ showBugsAlways,
672
+ setShowBugsAlways,
673
+ hideResolvedClosed,
674
+ setHideResolvedClosed,
675
+ bugReports,
676
+ role
677
+ } = useDevNotes();
678
+ const [open, setOpen] = (0, import_react3.useState)(false);
679
+ const menuRef = (0, import_react3.useRef)(null);
680
+ (0, import_react3.useEffect)(() => {
681
+ if (!open) return void 0;
682
+ const handleClickOutside = (e) => {
683
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
684
+ setOpen(false);
685
+ }
686
+ };
687
+ document.addEventListener("mousedown", handleClickOutside);
688
+ return () => document.removeEventListener("mousedown", handleClickOutside);
689
+ }, [open]);
690
+ const openBugCount = bugReports.filter(
691
+ (r) => r.status === "Open" || r.status === "In Progress" || r.status === "Needs Review"
692
+ ).length;
693
+ if (role === "none") return null;
694
+ const handleIconClick = (e) => {
695
+ if (isEnabled) {
696
+ e.preventDefault();
697
+ e.stopPropagation();
698
+ setIsEnabled(false);
699
+ return;
700
+ }
701
+ setOpen((prev) => !prev);
702
+ };
703
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
704
+ "div",
705
+ {
706
+ ref: menuRef,
707
+ "data-bug-menu": true,
708
+ className: "relative",
709
+ style: { zIndex: isEnabled ? 9995 : "auto" },
710
+ children: [
711
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
712
+ "button",
713
+ {
714
+ type: "button",
715
+ "aria-label": isEnabled ? "Click to disable bug reporting" : "Bug reporting menu",
716
+ onClick: handleIconClick,
717
+ className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-700 transition hover:text-emerald-600",
718
+ title: "Bug reports",
719
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "relative", children: [
720
+ IconComponent ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconComponent, { size: 20, color: isEnabled ? "#E53E3E" : void 0 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiAlertTriangle, { size: 20, color: isEnabled ? "#E53E3E" : void 0 }),
721
+ openBugCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "absolute -right-2 -top-1 inline-flex min-w-[16px] items-center justify-center rounded-full bg-red-600 px-1 text-[10px] font-bold text-white", children: openBugCount })
722
+ ] })
723
+ }
724
+ ),
725
+ open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
726
+ "div",
727
+ {
728
+ style: {
729
+ position: "absolute",
730
+ ...position?.includes("left") ? { left: 0 } : { right: 0 },
731
+ ...dropdownDirection === "up" ? { bottom: "100%", marginBottom: 8 } : { top: "100%", marginTop: 8 },
732
+ width: 320,
733
+ zIndex: 50,
734
+ borderRadius: 8,
735
+ border: "1px solid #e5e7eb",
736
+ backgroundColor: "#ffffff",
737
+ paddingTop: 8,
738
+ paddingBottom: 8,
739
+ boxShadow: "0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)"
740
+ },
741
+ children: [
742
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xs font-semibold text-gray-500", children: "DEV NOTES" }) }),
743
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "my-1 border-t border-gray-200" }),
744
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
745
+ "button",
746
+ {
747
+ type: "button",
748
+ "data-menu-item": true,
749
+ onClick: () => {
750
+ setIsEnabled(!isEnabled);
751
+ setOpen(false);
752
+ },
753
+ className: "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-gray-800 transition hover:bg-gray-50",
754
+ children: [
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-2 whitespace-nowrap", children: [
756
+ isEnabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiToggleRight, { className: "text-green-600" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiToggleLeft, {}),
757
+ isEnabled ? "Stop Reporting" : "Report Bug / Request Feature"
758
+ ] }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
760
+ "span",
761
+ {
762
+ role: "switch",
763
+ "aria-checked": isEnabled,
764
+ className: `relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full transition-colors duration-200 ${isEnabled ? "bg-green-500" : "bg-gray-300"}`,
765
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
766
+ "span",
767
+ {
768
+ className: `inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200 ${isEnabled ? "translate-x-4" : "translate-x-0.5"} mt-0.5`
769
+ }
770
+ )
771
+ }
772
+ )
773
+ ]
774
+ }
775
+ ),
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
777
+ "button",
778
+ {
779
+ type: "button",
780
+ "data-menu-item": true,
781
+ onClick: () => setShowBugsAlways(!showBugsAlways),
782
+ className: "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-gray-800 transition hover:bg-gray-50",
783
+ children: [
784
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-2 whitespace-nowrap", children: [
785
+ showBugsAlways ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiEye, { className: "text-blue-600" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiEyeOff, {}),
786
+ "Show Bugs Always"
787
+ ] }),
788
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
789
+ "span",
790
+ {
791
+ role: "switch",
792
+ "aria-checked": showBugsAlways,
793
+ className: `relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full transition-colors duration-200 ${showBugsAlways ? "bg-green-500" : "bg-gray-300"}`,
794
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
795
+ "span",
796
+ {
797
+ className: `inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200 ${showBugsAlways ? "translate-x-4" : "translate-x-0.5"} mt-0.5`
798
+ }
799
+ )
800
+ }
801
+ )
802
+ ]
803
+ }
804
+ ),
805
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
806
+ "button",
807
+ {
808
+ type: "button",
809
+ "data-menu-item": true,
810
+ onClick: () => setHideResolvedClosed(!hideResolvedClosed),
811
+ className: "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-gray-800 transition hover:bg-gray-50",
812
+ children: [
813
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-2 whitespace-nowrap", children: [
814
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
815
+ import_fi.FiFilter,
816
+ {
817
+ className: hideResolvedClosed ? "text-green-600" : "text-gray-500"
818
+ }
819
+ ),
820
+ "Hide Resolved/Closed"
821
+ ] }),
822
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
823
+ "span",
824
+ {
825
+ role: "switch",
826
+ "aria-checked": hideResolvedClosed,
827
+ className: `relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full transition-colors duration-200 ${hideResolvedClosed ? "bg-green-500" : "bg-gray-300"}`,
828
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
829
+ "span",
830
+ {
831
+ className: `inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200 ${hideResolvedClosed ? "translate-x-4" : "translate-x-0.5"} mt-0.5`
832
+ }
833
+ )
834
+ }
835
+ )
836
+ ]
837
+ }
838
+ ),
839
+ (onViewTasks || onSettings) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
840
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "my-1 border-t border-gray-200" }),
841
+ onViewTasks && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ "data-menu-item": true,
846
+ onClick: () => {
847
+ setOpen(false);
848
+ onViewTasks();
849
+ },
850
+ className: "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-gray-800 transition hover:bg-gray-50",
851
+ children: [
852
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-2 whitespace-nowrap", children: [
853
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiList, { className: "flex-shrink-0" }),
854
+ "View Tasks"
855
+ ] }),
856
+ openBugCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex min-w-[20px] items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700", children: openBugCount })
857
+ ]
858
+ }
859
+ ),
860
+ onSettings && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
861
+ "button",
862
+ {
863
+ type: "button",
864
+ "data-menu-item": true,
865
+ onClick: () => {
866
+ setOpen(false);
867
+ onSettings();
868
+ },
869
+ className: "flex w-full items-center gap-3 px-3 py-2 text-sm text-gray-800 transition hover:bg-gray-50",
870
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "inline-flex items-center gap-2 whitespace-nowrap", children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_fi.FiSettings, { className: "flex-shrink-0" }),
872
+ "Settings"
873
+ ] })
874
+ }
875
+ )
876
+ ] })
877
+ ]
878
+ }
879
+ )
880
+ ]
881
+ }
882
+ );
883
+ }
884
+
885
+ // src/DevNotesOverlay.tsx
886
+ var import_react9 = require("react");
887
+ var import_react_dom = require("react-dom");
888
+ var import_fi6 = require("react-icons/fi");
889
+
890
+ // src/DevNotesForm.tsx
891
+ var import_react6 = require("react");
892
+ var import_fi4 = require("react-icons/fi");
893
+
894
+ // src/DevNotesDiscussion.tsx
895
+ var import_react4 = require("react");
896
+ var import_fi2 = require("react-icons/fi");
897
+ var import_jsx_runtime3 = require("react/jsx-runtime");
898
+ var messageCache = /* @__PURE__ */ new Map();
899
+ var MESSAGE_CACHE_MAX = 50;
900
+ var getCachedMessages = (reportId) => {
901
+ const cached = messageCache.get(reportId);
902
+ if (!cached) return void 0;
903
+ messageCache.delete(reportId);
904
+ messageCache.set(reportId, cached);
905
+ return cached;
906
+ };
907
+ var setCachedMessages = (reportId, messages) => {
908
+ if (messageCache.has(reportId)) {
909
+ messageCache.delete(reportId);
910
+ }
911
+ messageCache.set(reportId, messages);
912
+ while (messageCache.size > MESSAGE_CACHE_MAX) {
913
+ const oldestKey = messageCache.keys().next().value;
914
+ if (!oldestKey) break;
915
+ messageCache.delete(oldestKey);
916
+ }
917
+ };
918
+ var detectActiveMention = (value, cursor) => {
919
+ const slice = value.slice(0, cursor);
920
+ const atIndex = slice.lastIndexOf("@");
921
+ if (atIndex === -1) return null;
922
+ if (atIndex > 0 && /\S/.test(slice.charAt(atIndex - 1))) {
923
+ return null;
924
+ }
925
+ const query = slice.slice(atIndex + 1);
926
+ if (query.includes(" ") || query.includes("\n") || query.includes(" ")) {
927
+ return null;
928
+ }
929
+ return { start: atIndex, end: cursor, query };
930
+ };
931
+ function DevNotesDiscussion({ report }) {
932
+ const { user, adapter, markMessagesAsRead, userProfiles, collaborators, onNotify } = useDevNotes();
933
+ const [messages, setMessages] = (0, import_react4.useState)([]);
934
+ const [loadingMessages, setLoadingMessages] = (0, import_react4.useState)(true);
935
+ const [sending, setSending] = (0, import_react4.useState)(false);
936
+ const [newMessage, setNewMessage] = (0, import_react4.useState)("");
937
+ const [editingMessageId, setEditingMessageId] = (0, import_react4.useState)(null);
938
+ const [editDraft, setEditDraft] = (0, import_react4.useState)("");
939
+ const [editLoading, setEditLoading] = (0, import_react4.useState)(false);
940
+ const [deletingId, setDeletingId] = (0, import_react4.useState)(null);
941
+ const textareaRef = (0, import_react4.useRef)(null);
942
+ const [mentionRange, setMentionRange] = (0, import_react4.useState)(null);
943
+ const [mentionQuery, setMentionQuery] = (0, import_react4.useState)("");
944
+ const [mentionHighlight, setMentionHighlight] = (0, import_react4.useState)(0);
945
+ const updateMentionTracking = (0, import_react4.useCallback)((value, cursor) => {
946
+ const mention = detectActiveMention(value, cursor);
947
+ if (mention) {
948
+ setMentionRange({ start: mention.start, end: mention.end });
949
+ setMentionQuery(mention.query.toLowerCase());
950
+ setMentionHighlight(0);
951
+ } else {
952
+ setMentionRange(null);
953
+ setMentionQuery("");
954
+ setMentionHighlight(0);
955
+ }
956
+ }, []);
957
+ const mentionCandidates = (0, import_react4.useMemo)(() => {
958
+ const map = /* @__PURE__ */ new Map();
959
+ collaborators.forEach((c) => {
960
+ if (c.id) map.set(c.id, c);
961
+ });
962
+ Object.entries(userProfiles).forEach(([id, profile]) => {
963
+ if (!map.has(id)) {
964
+ map.set(id, {
965
+ id,
966
+ full_name: profile.full_name || null,
967
+ email: profile.email || null
968
+ });
969
+ }
970
+ });
971
+ return Array.from(map.values()).sort((a, b) => {
972
+ const aLabel = (a.full_name || a.email || "").toLowerCase();
973
+ const bLabel = (b.full_name || b.email || "").toLowerCase();
974
+ return aLabel.localeCompare(bLabel);
975
+ });
976
+ }, [collaborators, userProfiles]);
977
+ const mentionOptions = (0, import_react4.useMemo)(() => {
978
+ if (!mentionRange) return [];
979
+ const query = mentionQuery.trim();
980
+ if (!query) return mentionCandidates;
981
+ return mentionCandidates.filter((c) => {
982
+ const label = (c.full_name || c.email || "").toLowerCase();
983
+ return label.includes(query);
984
+ });
985
+ }, [mentionCandidates, mentionQuery, mentionRange]);
986
+ (0, import_react4.useEffect)(() => {
987
+ if (!mentionRange) {
988
+ setMentionHighlight(0);
989
+ return;
990
+ }
991
+ setMentionHighlight((prev) => {
992
+ if (mentionOptions.length === 0) return 0;
993
+ return Math.min(prev, mentionOptions.length - 1);
994
+ });
995
+ }, [mentionOptions, mentionRange]);
996
+ const insertMention = (collaborator) => {
997
+ if (!mentionRange) return;
998
+ const label = collaborator.full_name || collaborator.email || "User";
999
+ const before = newMessage.slice(0, mentionRange.start);
1000
+ const after = newMessage.slice(mentionRange.end);
1001
+ const insertion = `@${label} `;
1002
+ const nextValue = `${before}${insertion}${after}`;
1003
+ setNewMessage(nextValue);
1004
+ setMentionRange(null);
1005
+ setMentionQuery("");
1006
+ setMentionHighlight(0);
1007
+ requestAnimationFrame(() => {
1008
+ const textarea = textareaRef.current;
1009
+ if (textarea) {
1010
+ const cursorPosition = before.length + insertion.length;
1011
+ textarea.focus();
1012
+ textarea.setSelectionRange(cursorPosition, cursorPosition);
1013
+ }
1014
+ });
1015
+ };
1016
+ const loadMessages = (0, import_react4.useCallback)(
1017
+ async (reportId, { silent } = { silent: false }) => {
1018
+ if (!reportId) {
1019
+ setMessages([]);
1020
+ setLoadingMessages(false);
1021
+ return;
1022
+ }
1023
+ if (!silent) {
1024
+ setLoadingMessages(true);
1025
+ }
1026
+ try {
1027
+ const data = await adapter.fetchMessages(reportId);
1028
+ setMessages(data);
1029
+ setCachedMessages(reportId, data);
1030
+ } catch (err) {
1031
+ console.error("[DevNotes] Failed to load messages", err);
1032
+ } finally {
1033
+ if (!silent) {
1034
+ setLoadingMessages(false);
1035
+ }
1036
+ }
1037
+ },
1038
+ [adapter]
1039
+ );
1040
+ (0, import_react4.useEffect)(() => {
1041
+ if (!report?.id) {
1042
+ setMessages([]);
1043
+ setLoadingMessages(false);
1044
+ return;
1045
+ }
1046
+ const cached = getCachedMessages(report.id);
1047
+ if (cached) {
1048
+ setMessages(cached);
1049
+ setLoadingMessages(false);
1050
+ loadMessages(report.id, { silent: true });
1051
+ } else {
1052
+ loadMessages(report.id);
1053
+ }
1054
+ }, [report?.id, loadMessages]);
1055
+ (0, import_react4.useEffect)(() => {
1056
+ if (!report?.id || !messages.length) return;
1057
+ const unreadMessageIds = messages.filter((message) => message.author_id !== user?.id).map((message) => message.id);
1058
+ if (unreadMessageIds.length) {
1059
+ markMessagesAsRead(report.id, unreadMessageIds);
1060
+ }
1061
+ }, [messages, report?.id, user?.id, markMessagesAsRead]);
1062
+ const formatTimestamp = (value) => {
1063
+ const parsed = new Date(value);
1064
+ return parsed.toLocaleString("en-US", {
1065
+ month: "short",
1066
+ day: "numeric",
1067
+ year: "numeric",
1068
+ hour: "numeric",
1069
+ minute: "2-digit"
1070
+ });
1071
+ };
1072
+ const directionBadge = (authorId) => {
1073
+ if (authorId === report.created_by) {
1074
+ return { label: "Reporter", className: "bg-purple-100 text-purple-800" };
1075
+ }
1076
+ return { label: "Team", className: "bg-blue-100 text-blue-800" };
1077
+ };
1078
+ const startEditing = (message) => {
1079
+ setEditingMessageId(message.id);
1080
+ setEditDraft(message.body);
1081
+ };
1082
+ const cancelEditing = () => {
1083
+ setEditingMessageId(null);
1084
+ setEditDraft("");
1085
+ };
1086
+ const handleMentionCursorUpdate = (0, import_react4.useCallback)(() => {
1087
+ const textarea = textareaRef.current;
1088
+ if (!textarea) return;
1089
+ const cursor = textarea.selectionStart ?? textarea.value.length;
1090
+ updateMentionTracking(textarea.value, cursor);
1091
+ }, [updateMentionTracking]);
1092
+ const handleMessageChange = (e) => {
1093
+ const value = e.target.value;
1094
+ setNewMessage(value);
1095
+ const cursor = e.target.selectionStart ?? value.length;
1096
+ updateMentionTracking(value, cursor);
1097
+ };
1098
+ const handleTextareaKeyDown = (e) => {
1099
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1100
+ e.preventDefault();
1101
+ handleSendMessage();
1102
+ return;
1103
+ }
1104
+ if (mentionRange && mentionOptions.length > 0) {
1105
+ if (e.key === "ArrowDown") {
1106
+ e.preventDefault();
1107
+ setMentionHighlight((prev) => (prev + 1) % mentionOptions.length);
1108
+ return;
1109
+ }
1110
+ if (e.key === "ArrowUp") {
1111
+ e.preventDefault();
1112
+ setMentionHighlight((prev) => prev - 1 < 0 ? mentionOptions.length - 1 : prev - 1);
1113
+ return;
1114
+ }
1115
+ if (e.key === "Enter" || e.key === "Tab") {
1116
+ e.preventDefault();
1117
+ insertMention(mentionOptions[mentionHighlight]);
1118
+ return;
1119
+ }
1120
+ }
1121
+ if (mentionRange && e.key === "Escape") {
1122
+ e.preventDefault();
1123
+ setMentionRange(null);
1124
+ setMentionQuery("");
1125
+ setMentionHighlight(0);
1126
+ }
1127
+ };
1128
+ const handleSendMessage = async () => {
1129
+ if (!newMessage.trim() || !report?.id || !user?.id) return;
1130
+ setSending(true);
1131
+ try {
1132
+ const data = await adapter.createMessage(report.id, newMessage.trim());
1133
+ setMessages((prev) => {
1134
+ const next = [...prev, data];
1135
+ if (report?.id) {
1136
+ setCachedMessages(report.id, next);
1137
+ }
1138
+ return next;
1139
+ });
1140
+ setNewMessage("");
1141
+ setMentionRange(null);
1142
+ setMentionQuery("");
1143
+ if (onNotify) {
1144
+ try {
1145
+ const commenterName = data.author?.full_name || "Someone";
1146
+ const reportTitle = report.title || "Untitled";
1147
+ const snippet = data.body.length > 200 ? data.body.slice(0, 200) + "..." : data.body;
1148
+ const recipientEmails = /* @__PURE__ */ new Set();
1149
+ if (report.creator?.email && report.created_by !== user.id) {
1150
+ recipientEmails.add(report.creator.email);
1151
+ }
1152
+ const priorMessages = getCachedMessages(report.id) || [];
1153
+ for (const msg of priorMessages) {
1154
+ if (msg.author_id !== user.id && msg.author?.email) {
1155
+ recipientEmails.add(msg.author.email);
1156
+ }
1157
+ }
1158
+ for (const email of recipientEmails) {
1159
+ onNotify({
1160
+ type: "new_comment",
1161
+ recipientEmail: email,
1162
+ subject: `New comment on Dev Note: ${reportTitle}`,
1163
+ textBody: `Hi,
1164
+
1165
+ ${commenterName} commented on the dev note "${reportTitle}":
1166
+
1167
+ "${snippet}"
1168
+
1169
+ Thank you,
1170
+ Dev Notes`,
1171
+ htmlBody: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
1172
+ <h2 style="color: #333;">New Comment on Dev Note</h2>
1173
+ <p><strong>${commenterName}</strong> commented on <strong>"${reportTitle}"</strong>:</p>
1174
+ <blockquote style="border-left: 3px solid #ccc; padding-left: 12px; color: #555; margin: 16px 0;">${snippet}</blockquote>
1175
+ <p>Thank you,<br>Dev Notes</p>
1176
+ </div>`
1177
+ });
1178
+ }
1179
+ } catch (notifyErr) {
1180
+ console.error("[DevNotes] Error building comment notifications:", notifyErr);
1181
+ }
1182
+ }
1183
+ } catch (err) {
1184
+ console.error("[DevNotes] Failed to add message", err);
1185
+ } finally {
1186
+ setSending(false);
1187
+ }
1188
+ };
1189
+ const handleUpdateMessage = async () => {
1190
+ if (!editingMessageId || !editDraft.trim() || !user?.id) return;
1191
+ setEditLoading(true);
1192
+ try {
1193
+ const data = await adapter.updateMessage(editingMessageId, editDraft.trim());
1194
+ setMessages((prev) => {
1195
+ const next = prev.map((msg) => msg.id === data.id ? data : msg);
1196
+ if (report?.id) {
1197
+ setCachedMessages(report.id, next);
1198
+ }
1199
+ return next;
1200
+ });
1201
+ cancelEditing();
1202
+ } catch (err) {
1203
+ console.error("[DevNotes] Failed to update message", err);
1204
+ } finally {
1205
+ setEditLoading(false);
1206
+ }
1207
+ };
1208
+ const handleDeleteMessage = async (messageId) => {
1209
+ if (!user?.id) return;
1210
+ const confirmed = window.confirm("Delete this note? This cannot be undone.");
1211
+ if (!confirmed) return;
1212
+ setDeletingId(messageId);
1213
+ try {
1214
+ await adapter.deleteMessage(messageId);
1215
+ setMessages((prev) => {
1216
+ const next = prev.filter((msg) => msg.id !== messageId);
1217
+ if (report?.id) {
1218
+ setCachedMessages(report.id, next);
1219
+ }
1220
+ return next;
1221
+ });
1222
+ if (editingMessageId === messageId) {
1223
+ cancelEditing();
1224
+ }
1225
+ } catch (err) {
1226
+ console.error("[DevNotes] Failed to delete message", err);
1227
+ } finally {
1228
+ setDeletingId(null);
1229
+ }
1230
+ };
1231
+ if (!report?.id) {
1232
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bg-gray-50 rounded-lg p-4 border border-gray-100 min-h-[250px]", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-600", children: "Save this task first to start a conversation." }) });
1233
+ }
1234
+ const getInitials = (name) => {
1235
+ const parts = name.trim().split(/\s+/);
1236
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
1237
+ return name.slice(0, 2).toUpperCase();
1238
+ };
1239
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-4 bg-gray-50 rounded-lg border border-gray-100 p-4 h-full", children: [
1240
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-semibold", children: "Comments" }) }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex-1 min-h-[220px] max-h-[360px] overflow-y-auto pr-2", children: loadingMessages ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex justify-center py-10", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-6 h-6 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" }) }) : messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center bg-white rounded-md border border-dashed border-gray-200 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500", children: "No notes yet. Start the conversation below." }) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-col gap-3", children: messages.map((message) => {
1242
+ const badge = directionBadge(message.author_id);
1243
+ const authorLabel = message.author?.full_name || message.author?.email || (message.author_id === report.created_by ? "Reporter" : "Team");
1244
+ const canManage = user?.id && message.author_id === user.id;
1245
+ const wasUpdated = message.updated_at && new Date(message.updated_at).toISOString() !== new Date(message.created_at).toISOString();
1246
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1247
+ "div",
1248
+ {
1249
+ className: "bg-white rounded-lg border border-gray-200 p-3",
1250
+ children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex justify-between items-start mb-1", children: [
1252
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-3", children: [
1253
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-xs font-bold text-white flex-shrink-0", children: getInitials(authorLabel) }),
1254
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1255
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
1256
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm font-semibold", children: authorLabel }),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `text-[0.65rem] px-1.5 py-0.5 rounded ${badge.className}`, children: badge.label })
1258
+ ] }),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-gray-500", children: [
1260
+ formatTimestamp(message.created_at),
1261
+ wasUpdated && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "text-gray-400", children: [
1262
+ " ",
1263
+ "\xB7 Updated ",
1264
+ formatTimestamp(message.updated_at)
1265
+ ] })
1266
+ ] })
1267
+ ] })
1268
+ ] }),
1269
+ canManage && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1", children: [
1270
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1271
+ "button",
1272
+ {
1273
+ type: "button",
1274
+ className: "p-1 rounded hover:bg-gray-100 text-gray-500",
1275
+ onClick: () => startEditing(message),
1276
+ "aria-label": "Edit note",
1277
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_fi2.FiEdit2, { size: 14 })
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1281
+ "button",
1282
+ {
1283
+ type: "button",
1284
+ className: "p-1 rounded hover:bg-gray-100 text-gray-500 disabled:opacity-50",
1285
+ onClick: () => handleDeleteMessage(message.id),
1286
+ disabled: deletingId === message.id,
1287
+ "aria-label": "Delete note",
1288
+ children: deletingId === message.id ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-3.5 h-3.5 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_fi2.FiTrash2, { size: 14 })
1289
+ }
1290
+ )
1291
+ ] })
1292
+ ] }),
1293
+ editingMessageId === message.id ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2 mt-2", children: [
1294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1295
+ "textarea",
1296
+ {
1297
+ value: editDraft,
1298
+ onChange: (e) => setEditDraft(e.target.value),
1299
+ rows: 4,
1300
+ className: "w-full rounded-md border border-gray-300 bg-gray-50 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
1301
+ }
1302
+ ),
1303
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex justify-end gap-2", children: [
1304
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1305
+ "button",
1306
+ {
1307
+ type: "button",
1308
+ className: "px-3 py-1 text-xs rounded hover:bg-gray-100",
1309
+ onClick: cancelEditing,
1310
+ children: "Cancel"
1311
+ }
1312
+ ),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1314
+ "button",
1315
+ {
1316
+ type: "button",
1317
+ className: "px-3 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50",
1318
+ onClick: handleUpdateMessage,
1319
+ disabled: !editDraft.trim() || editLoading,
1320
+ children: editLoading ? "Saving..." : "Save"
1321
+ }
1322
+ )
1323
+ ] })
1324
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-700 whitespace-pre-wrap", children: message.body })
1325
+ ]
1326
+ },
1327
+ message.id
1328
+ );
1329
+ }) }) }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
1332
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1333
+ "textarea",
1334
+ {
1335
+ ref: textareaRef,
1336
+ placeholder: "Add a reply or request more info...",
1337
+ value: newMessage,
1338
+ onChange: handleMessageChange,
1339
+ onKeyDown: handleTextareaKeyDown,
1340
+ onKeyUp: handleMentionCursorUpdate,
1341
+ onClick: handleMentionCursorUpdate,
1342
+ rows: 4,
1343
+ className: "w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
1344
+ }
1345
+ ),
1346
+ mentionRange && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute bottom-3 left-3 bg-white border border-gray-200 rounded-md shadow-lg min-w-[220px] max-h-[200px] overflow-y-auto z-[2]", children: mentionOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-sm text-gray-500", children: [
1347
+ 'No collaborators match "',
1348
+ mentionQuery,
1349
+ '"'
1350
+ ] }) }) : mentionOptions.map((collaborator, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1351
+ "div",
1352
+ {
1353
+ className: `px-3 py-2 cursor-pointer hover:bg-gray-100 ${mentionHighlight === index ? "bg-gray-100" : ""}`,
1354
+ onMouseDown: (e) => {
1355
+ e.preventDefault();
1356
+ insertMention(collaborator);
1357
+ setMentionHighlight(index);
1358
+ },
1359
+ children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-semibold", children: collaborator.full_name || collaborator.email || "Unknown" }),
1361
+ collaborator.email && collaborator.full_name && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-500", children: collaborator.email })
1362
+ ]
1363
+ },
1364
+ collaborator.id
1365
+ )) })
1366
+ ] }),
1367
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex justify-between items-center", children: [
1368
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-gray-500", children: [
1369
+ "Notes are visible to everyone with access to Dev Notes. Use",
1370
+ " ",
1371
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "font-bold", children: "@" }),
1372
+ " to mention a teammate."
1373
+ ] }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1375
+ "button",
1376
+ {
1377
+ type: "button",
1378
+ className: "px-4 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50",
1379
+ onClick: handleSendMessage,
1380
+ disabled: !newMessage.trim() || sending,
1381
+ children: sending ? "Sending..." : "Send"
1382
+ }
1383
+ )
1384
+ ] })
1385
+ ] })
1386
+ ] });
1387
+ }
1388
+
1389
+ // src/AiDescriptionChat.tsx
1390
+ var import_react5 = require("react");
1391
+ var import_fi3 = require("react-icons/fi");
1392
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1393
+ function AiDescriptionChat({
1394
+ initialDescription,
1395
+ context,
1396
+ aiProvider,
1397
+ onAccept,
1398
+ onCancel
1399
+ }) {
1400
+ const [conversationHistory, setConversationHistory] = (0, import_react5.useState)([]);
1401
+ const [userInput, setUserInput] = (0, import_react5.useState)("");
1402
+ const [isLoading, setIsLoading] = (0, import_react5.useState)(false);
1403
+ const [error, setError] = (0, import_react5.useState)(null);
1404
+ const [finalizedDescription, setFinalizedDescription] = (0, import_react5.useState)(null);
1405
+ const [isEditing, setIsEditing] = (0, import_react5.useState)(false);
1406
+ const [editDraft, setEditDraft] = (0, import_react5.useState)("");
1407
+ const scrollRef = (0, import_react5.useRef)(null);
1408
+ const textareaRef = (0, import_react5.useRef)(null);
1409
+ const scrollToBottom = (0, import_react5.useCallback)(() => {
1410
+ if (scrollRef.current) {
1411
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1412
+ }
1413
+ }, []);
1414
+ (0, import_react5.useEffect)(() => {
1415
+ scrollToBottom();
1416
+ }, [conversationHistory, finalizedDescription, scrollToBottom]);
1417
+ const callAiAssist = (0, import_react5.useCallback)(
1418
+ async (history) => {
1419
+ setIsLoading(true);
1420
+ setError(null);
1421
+ try {
1422
+ const result = await aiProvider.refineDescription({
1423
+ description: initialDescription,
1424
+ conversationHistory: history,
1425
+ context
1426
+ });
1427
+ if (result.type === "error") {
1428
+ throw new Error(result.message);
1429
+ }
1430
+ if (result.type === "finalized") {
1431
+ setFinalizedDescription(result.description);
1432
+ setConversationHistory((prev) => [
1433
+ ...prev,
1434
+ { role: "assistant", content: result.description }
1435
+ ]);
1436
+ } else if (result.type === "question") {
1437
+ setConversationHistory((prev) => [
1438
+ ...prev,
1439
+ { role: "assistant", content: result.message }
1440
+ ]);
1441
+ }
1442
+ } catch (err) {
1443
+ console.error("[AiDescriptionChat] Error calling AI assist:", err);
1444
+ const message = err?.message || "Failed to get AI response. Please try again.";
1445
+ setError(message);
1446
+ } finally {
1447
+ setIsLoading(false);
1448
+ }
1449
+ },
1450
+ [initialDescription, context, aiProvider]
1451
+ );
1452
+ const [hasStarted, setHasStarted] = (0, import_react5.useState)(false);
1453
+ (0, import_react5.useEffect)(() => {
1454
+ if (initialDescription.trim() && !hasStarted) {
1455
+ setHasStarted(true);
1456
+ callAiAssist([]);
1457
+ }
1458
+ }, [initialDescription, hasStarted]);
1459
+ const handleSendReply = async () => {
1460
+ const trimmed = userInput.trim();
1461
+ if (!trimmed || isLoading) return;
1462
+ const newUserMessage = { role: "user", content: trimmed };
1463
+ const updatedHistory = [...conversationHistory, newUserMessage];
1464
+ setConversationHistory(updatedHistory);
1465
+ setUserInput("");
1466
+ await callAiAssist(updatedHistory);
1467
+ };
1468
+ const handleKeyDown = (e) => {
1469
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1470
+ e.preventDefault();
1471
+ handleSendReply();
1472
+ }
1473
+ };
1474
+ const handleForceFinalize = async () => {
1475
+ const forceMessage = {
1476
+ role: "user",
1477
+ content: "Please finalize the description now with whatever information you have."
1478
+ };
1479
+ const updatedHistory = [...conversationHistory, forceMessage];
1480
+ setConversationHistory(updatedHistory);
1481
+ await callAiAssist(updatedHistory);
1482
+ };
1483
+ const handleAccept = () => {
1484
+ if (isEditing && editDraft.trim()) {
1485
+ onAccept(editDraft.trim());
1486
+ } else if (finalizedDescription) {
1487
+ onAccept(finalizedDescription);
1488
+ }
1489
+ };
1490
+ const handleEdit = () => {
1491
+ setIsEditing(true);
1492
+ setEditDraft(finalizedDescription || "");
1493
+ };
1494
+ const handleRetry = async () => {
1495
+ setError(null);
1496
+ await callAiAssist(conversationHistory);
1497
+ };
1498
+ const assistantMessageCount = conversationHistory.filter((m) => m.role === "assistant").length;
1499
+ const showFinalizeButton = !finalizedDescription && assistantMessageCount >= 3;
1500
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-3 rounded-xl border-2 border-purple-200 bg-gradient-to-b from-purple-50/50 to-white p-4 min-h-[200px] shadow-[0_0_0_3px_rgba(167,139,250,0.1)]", children: [
1501
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between items-center", children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiZap, { size: 16, className: "text-purple-600" }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm font-semibold text-purple-700", children: "AI Description Refinement" }),
1505
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[0.65rem] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700 font-semibold", children: "GPT-4" })
1506
+ ] }),
1507
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1508
+ "button",
1509
+ {
1510
+ type: "button",
1511
+ className: "inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-600",
1512
+ onClick: onCancel,
1513
+ children: [
1514
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiX, { size: 12 }),
1515
+ "Close"
1516
+ ]
1517
+ }
1518
+ )
1519
+ ] }),
1520
+ !hasStarted && !initialDescription.trim() && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-1 flex items-center justify-center min-h-[120px]", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "text-center", children: [
1521
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiZap, { size: 28, className: "mx-auto mb-2 text-purple-300" }),
1522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-gray-500", children: "Add a title above and AI will help build a full description" })
1523
+ ] }) }),
1524
+ hasStarted && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1525
+ "div",
1526
+ {
1527
+ ref: scrollRef,
1528
+ className: "flex-1 min-h-[200px] max-h-[350px] overflow-y-auto pr-1",
1529
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-3", children: [
1530
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-purple-50 border border-purple-200 rounded-lg p-3 max-w-[85%]", children: [
1531
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-purple-600 font-medium mb-1", children: "Your Description" }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm whitespace-pre-wrap", children: initialDescription })
1533
+ ] }) }),
1534
+ conversationHistory.map((msg, idx) => {
1535
+ const isAssistant = msg.role === "assistant";
1536
+ const isFinalMessage = finalizedDescription && idx === conversationHistory.length - 1;
1537
+ if (isFinalMessage) return null;
1538
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1539
+ "div",
1540
+ {
1541
+ className: `flex ${isAssistant ? "justify-start" : "justify-end"}`,
1542
+ children: [
1543
+ isAssistant && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 mr-2 mt-1", children: "AI" }),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1545
+ "div",
1546
+ {
1547
+ className: `border rounded-lg p-3 max-w-[85%] ${isAssistant ? "bg-white border-gray-200" : "bg-purple-50 border-purple-200"}`,
1548
+ children: [
1549
+ isAssistant && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-purple-600 font-medium mb-1", children: "AI Assistant" }),
1550
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm whitespace-pre-wrap", children: msg.content })
1551
+ ]
1552
+ }
1553
+ )
1554
+ ]
1555
+ },
1556
+ idx
1557
+ );
1558
+ }),
1559
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-start", children: [
1560
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 mr-2 mt-1", children: "AI" }),
1561
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "bg-white border border-gray-200 rounded-lg p-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
1562
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-4 h-4 border-2 border-purple-300 border-t-purple-500 rounded-full animate-spin" }),
1563
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-gray-500", children: "Analyzing..." })
1564
+ ] }) })
1565
+ ] }),
1566
+ error && !isLoading && (() => {
1567
+ const isConfigError = /edge function|fetch|network/i.test(error);
1568
+ return isConfigError ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-gray-50 border border-gray-200 rounded-lg p-3", children: [
1569
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-gray-600 mb-2", children: "AI refinement is not available. Your description will be saved as-is." }),
1570
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1571
+ "button",
1572
+ {
1573
+ type: "button",
1574
+ className: "px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-100",
1575
+ onClick: onCancel,
1576
+ children: "Dismiss"
1577
+ }
1578
+ )
1579
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-red-50 border border-red-200 rounded-lg p-3", children: [
1580
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-red-600 mb-2", children: error }),
1581
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1582
+ "button",
1583
+ {
1584
+ type: "button",
1585
+ className: "px-2 py-1 text-xs rounded border border-red-300 text-red-600 hover:bg-red-100",
1586
+ onClick: handleRetry,
1587
+ children: "Retry"
1588
+ }
1589
+ )
1590
+ ] });
1591
+ })(),
1592
+ finalizedDescription && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-green-50 border-2 border-green-300 rounded-lg p-4", children: [
1593
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex justify-between items-center mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
1594
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs font-bold text-green-700", children: "AI-Refined Description" }),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[0.6rem] px-1.5 py-0.5 rounded bg-green-100 text-green-800", children: "Ready" })
1596
+ ] }) }),
1597
+ isEditing ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-2", children: [
1598
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1599
+ "textarea",
1600
+ {
1601
+ value: editDraft,
1602
+ onChange: (e) => setEditDraft(e.target.value),
1603
+ rows: 8,
1604
+ className: "w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
1605
+ }
1606
+ ),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-end gap-2", children: [
1608
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1609
+ "button",
1610
+ {
1611
+ type: "button",
1612
+ className: "px-2 py-1 text-xs rounded hover:bg-gray-100",
1613
+ onClick: () => setIsEditing(false),
1614
+ children: "Cancel Edit"
1615
+ }
1616
+ ),
1617
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1618
+ "button",
1619
+ {
1620
+ type: "button",
1621
+ className: "inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded bg-green-600 text-white hover:bg-green-700 disabled:opacity-50",
1622
+ onClick: handleAccept,
1623
+ disabled: !editDraft.trim(),
1624
+ children: [
1625
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiCheck, { size: 14 }),
1626
+ "Accept Edited"
1627
+ ]
1628
+ }
1629
+ )
1630
+ ] })
1631
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1632
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm whitespace-pre-wrap text-gray-800", children: finalizedDescription }),
1633
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-end gap-2 mt-3", children: [
1634
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1635
+ "button",
1636
+ {
1637
+ type: "button",
1638
+ className: "inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded border border-gray-300 hover:bg-gray-100",
1639
+ onClick: handleEdit,
1640
+ children: [
1641
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiEdit2, { size: 14 }),
1642
+ "Edit"
1643
+ ]
1644
+ }
1645
+ ),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1647
+ "button",
1648
+ {
1649
+ type: "button",
1650
+ className: "inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded bg-green-600 text-white hover:bg-green-700",
1651
+ onClick: handleAccept,
1652
+ children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fi3.FiCheck, { size: 14 }),
1654
+ "Accept"
1655
+ ]
1656
+ }
1657
+ )
1658
+ ] })
1659
+ ] })
1660
+ ] })
1661
+ ] })
1662
+ }
1663
+ ),
1664
+ hasStarted && !finalizedDescription && !isLoading && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-2", children: [
1665
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1666
+ "textarea",
1667
+ {
1668
+ ref: textareaRef,
1669
+ placeholder: "Answer the AI's questions...",
1670
+ value: userInput,
1671
+ onChange: (e) => setUserInput(e.target.value),
1672
+ onKeyDown: handleKeyDown,
1673
+ rows: 3,
1674
+ className: "w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
1675
+ }
1676
+ ),
1677
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex justify-between items-center", children: [
1678
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs text-gray-500", children: "Ctrl/Cmd + Enter to send" }),
1679
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
1680
+ showFinalizeButton && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1681
+ "button",
1682
+ {
1683
+ type: "button",
1684
+ className: "px-3 py-1.5 text-sm rounded border border-purple-300 text-purple-700 hover:bg-purple-50",
1685
+ onClick: handleForceFinalize,
1686
+ children: "Finalize Now"
1687
+ }
1688
+ ),
1689
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1690
+ "button",
1691
+ {
1692
+ type: "button",
1693
+ className: "px-3 py-1.5 text-sm rounded bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50",
1694
+ onClick: handleSendReply,
1695
+ disabled: !userInput.trim(),
1696
+ children: "Send"
1697
+ }
1698
+ )
1699
+ ] })
1700
+ ] })
1701
+ ] })
1702
+ ] });
1703
+ }
1704
+
1705
+ // src/internal/bugAnchors.ts
1706
+ var normalizePageUrl = (value) => {
1707
+ const trimmed = value.trim();
1708
+ if (!trimmed) return "";
1709
+ const [pathAndQuery] = trimmed.split("#");
1710
+ const [rawPath, queryString] = pathAndQuery.split("?");
1711
+ const withoutTrailing = rawPath.replace(/\/+$/, "") || "/";
1712
+ if (queryString) {
1713
+ const params = new URLSearchParams(queryString);
1714
+ const normalizedQuery = params.toString();
1715
+ if (normalizedQuery) {
1716
+ return `${withoutTrailing}?${normalizedQuery}`;
1717
+ }
1718
+ }
1719
+ return withoutTrailing;
1720
+ };
1721
+ var ELEMENT_SELECTOR_MAX_DEPTH = 8;
1722
+ var PREFERRED_ATTRIBUTES = ["data-bug-anchor", "data-testid", "data-id", "data-role", "aria-label"];
1723
+ var cssEscape = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape : (value) => value.replace(/([ #.;?+<>~*:()[\]\\])/g, "\\$1");
1724
+ var clamp = (value, min = 0, max = 1) => Math.min(Math.max(value, min), max);
1725
+ var isNumber = (value) => typeof value === "number" && !Number.isNaN(value);
1726
+ var parseNumberish = (value) => {
1727
+ if (isNumber(value)) return value;
1728
+ if (typeof value !== "string") return null;
1729
+ const trimmed = value.trim();
1730
+ if (!trimmed) return null;
1731
+ const parsed = Number(trimmed);
1732
+ return Number.isFinite(parsed) ? parsed : null;
1733
+ };
1734
+ var shouldIgnoreClass = (className) => {
1735
+ return !className || className.startsWith("css-");
1736
+ };
1737
+ var buildSelectorSegment = (element) => {
1738
+ if (element.id) {
1739
+ return `#${cssEscape(element.id)}`;
1740
+ }
1741
+ for (const attr of PREFERRED_ATTRIBUTES) {
1742
+ const attrValue = element.getAttribute(attr);
1743
+ if (attrValue) {
1744
+ return `${element.tagName.toLowerCase()}[${attr}="${cssEscape(attrValue)}"]`;
1745
+ }
1746
+ }
1747
+ const stableClass = Array.from(element.classList).find(
1748
+ (className) => !shouldIgnoreClass(className)
1749
+ );
1750
+ const base = stableClass ? `${element.tagName.toLowerCase()}.${cssEscape(stableClass)}` : element.tagName.toLowerCase();
1751
+ if (!element.parentElement) {
1752
+ return base;
1753
+ }
1754
+ let index = 1;
1755
+ let sibling = element.previousElementSibling;
1756
+ while (sibling) {
1757
+ if (sibling.tagName === element.tagName) {
1758
+ index += 1;
1759
+ }
1760
+ sibling = sibling.previousElementSibling;
1761
+ }
1762
+ return `${base}:nth-of-type(${index})`;
1763
+ };
1764
+ var buildElementSelector = (element) => {
1765
+ if (!element || typeof document === "undefined") {
1766
+ return null;
1767
+ }
1768
+ const segments = [];
1769
+ let current = element;
1770
+ let depth = 0;
1771
+ while (current && depth < ELEMENT_SELECTOR_MAX_DEPTH) {
1772
+ const segment = buildSelectorSegment(current);
1773
+ segments.unshift(segment);
1774
+ if (segment.startsWith("#") || segment.includes("[")) {
1775
+ break;
1776
+ }
1777
+ current = current.parentElement;
1778
+ depth += 1;
1779
+ }
1780
+ if (!segments.length) {
1781
+ return null;
1782
+ }
1783
+ return segments.join(" > ");
1784
+ };
1785
+ var disablePointerEvents = (elements) => {
1786
+ const restored = [];
1787
+ elements.forEach((element) => {
1788
+ if (!element) return;
1789
+ restored.push({ element, pointerEvents: element.style.pointerEvents });
1790
+ element.style.pointerEvents = "none";
1791
+ });
1792
+ return () => {
1793
+ restored.forEach(({ element, pointerEvents }) => {
1794
+ element.style.pointerEvents = pointerEvents;
1795
+ });
1796
+ };
1797
+ };
1798
+ var calculateBugPositionFromPoint = ({
1799
+ clientX,
1800
+ clientY,
1801
+ elementsToIgnore = []
1802
+ }) => {
1803
+ if (typeof window === "undefined" || typeof document === "undefined") {
1804
+ return {
1805
+ x: clientX,
1806
+ y: clientY,
1807
+ targetSelector: null,
1808
+ targetRelativeX: null,
1809
+ targetRelativeY: null
1810
+ };
1811
+ }
1812
+ const restorePointerEvents = disablePointerEvents(elementsToIgnore);
1813
+ let target = document.elementFromPoint(clientX, clientY);
1814
+ restorePointerEvents();
1815
+ if (target && (target.hasAttribute("data-bug-dot") || target.closest("[data-bug-dot]"))) {
1816
+ target = target.closest("[data-bug-dot]");
1817
+ }
1818
+ let targetSelector = null;
1819
+ let targetRelativeX = null;
1820
+ let targetRelativeY = null;
1821
+ if (target && target !== document.documentElement && target !== document.body) {
1822
+ targetSelector = buildElementSelector(target);
1823
+ const rect = target.getBoundingClientRect();
1824
+ if (rect.width > 0 && rect.height > 0) {
1825
+ targetRelativeX = clamp((clientX - rect.left) / rect.width);
1826
+ targetRelativeY = clamp((clientY - rect.top) / rect.height);
1827
+ }
1828
+ }
1829
+ return {
1830
+ x: clientX + window.scrollX,
1831
+ y: clientY + window.scrollY,
1832
+ targetSelector,
1833
+ targetRelativeX,
1834
+ targetRelativeY
1835
+ };
1836
+ };
1837
+ var resolveStoredCoordinates = (x, y) => {
1838
+ if (typeof window === "undefined") {
1839
+ return { x, y };
1840
+ }
1841
+ if (x <= 100 && y <= 100) {
1842
+ return {
1843
+ x: x / 100 * window.innerWidth,
1844
+ y: y / 100 * window.innerHeight
1845
+ };
1846
+ }
1847
+ return {
1848
+ x: x - window.scrollX,
1849
+ y: y - window.scrollY
1850
+ };
1851
+ };
1852
+ var isElementVisible = (element) => {
1853
+ if (element === document.body || element === document.documentElement) {
1854
+ return true;
1855
+ }
1856
+ const style = window.getComputedStyle(element);
1857
+ if (style.display === "none" || style.visibility === "hidden") {
1858
+ return false;
1859
+ }
1860
+ if (element.offsetParent === null && style.position !== "fixed") {
1861
+ return false;
1862
+ }
1863
+ const rect = element.getBoundingClientRect();
1864
+ if (rect.width === 0 && rect.height === 0) {
1865
+ return false;
1866
+ }
1867
+ return true;
1868
+ };
1869
+ var resolveBugReportCoordinates = (report) => {
1870
+ if (typeof document !== "undefined") {
1871
+ const relativeX = parseNumberish(report.target_relative_x);
1872
+ const relativeY = parseNumberish(report.target_relative_y);
1873
+ if (report.target_selector && relativeX !== null && relativeY !== null) {
1874
+ let element = null;
1875
+ try {
1876
+ element = document.querySelector(report.target_selector);
1877
+ } catch {
1878
+ }
1879
+ if (element) {
1880
+ if (!isElementVisible(element)) {
1881
+ return null;
1882
+ }
1883
+ const rect = element.getBoundingClientRect();
1884
+ const x = rect.left + rect.width * relativeX;
1885
+ const y = rect.top + rect.height * relativeY;
1886
+ return { x, y };
1887
+ }
1888
+ }
1889
+ }
1890
+ return resolveStoredCoordinates(report.x_position, report.y_position);
1891
+ };
1892
+
1893
+ // src/internal/captureContext.ts
1894
+ function deriveRouteLabelFromUrl(rawUrl) {
1895
+ const fallback = "Current Page";
1896
+ try {
1897
+ const origin = typeof window !== "undefined" ? window.location.origin : "https://localhost";
1898
+ const parsed = new URL(rawUrl || "/", origin);
1899
+ const parts = parsed.pathname.split("/").filter(Boolean);
1900
+ if (parts.length === 0) return "Home";
1901
+ const last = decodeURIComponent(parts[parts.length - 1]).replace(/[-_]+/g, " ").trim();
1902
+ if (!last) return fallback;
1903
+ return last.split(/\s+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1904
+ } catch {
1905
+ return fallback;
1906
+ }
1907
+ }
1908
+ function detectBrowserName(userAgent) {
1909
+ const ua = userAgent.toLowerCase();
1910
+ if (ua.includes("edg/")) return "Edge";
1911
+ if (ua.includes("opr/") || ua.includes("opera")) return "Opera";
1912
+ if (ua.includes("chrome/") && !ua.includes("edg/")) return "Chrome";
1913
+ if (ua.includes("safari/") && !ua.includes("chrome/")) return "Safari";
1914
+ if (ua.includes("firefox/")) return "Firefox";
1915
+ return "Unknown";
1916
+ }
1917
+ function buildCaptureContext(pageUrl) {
1918
+ if (typeof window === "undefined" || typeof navigator === "undefined") return null;
1919
+ const normalizedUrl = normalizePageUrl(pageUrl || window.location.pathname);
1920
+ let path = window.location.pathname;
1921
+ try {
1922
+ path = new URL(normalizedUrl, window.location.origin).pathname;
1923
+ } catch {
1924
+ path = normalizedUrl;
1925
+ }
1926
+ const timezone = typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone || null : null;
1927
+ return {
1928
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1929
+ route_label: deriveRouteLabelFromUrl(normalizedUrl),
1930
+ path,
1931
+ browser: {
1932
+ name: detectBrowserName(navigator.userAgent || ""),
1933
+ user_agent: navigator.userAgent || "unknown",
1934
+ platform: navigator.platform || null,
1935
+ language: navigator.language || null
1936
+ },
1937
+ viewport: {
1938
+ width: window.innerWidth,
1939
+ height: window.innerHeight,
1940
+ pixel_ratio: window.devicePixelRatio || 1
1941
+ },
1942
+ timezone
1943
+ };
1944
+ }
1945
+
1946
+ // src/internal/aiPayload.ts
1947
+ function buildAiFixPayload(params) {
1948
+ return {
1949
+ source: params.source || "@the-portland-company/devnotes",
1950
+ copied_at: params.copiedAt || (/* @__PURE__ */ new Date()).toISOString(),
1951
+ report: {
1952
+ id: params.report.id ?? null,
1953
+ title: params.report.title ?? null,
1954
+ status: params.report.status,
1955
+ severity: params.report.severity,
1956
+ task_list_id: params.report.taskListId ?? null,
1957
+ types: params.report.types,
1958
+ type_names: params.report.typeNames,
1959
+ approved: params.report.approved,
1960
+ ai_ready: params.report.aiReady
1961
+ },
1962
+ narrative: {
1963
+ description: params.narrative.description ?? null,
1964
+ expected_behavior: params.narrative.expectedBehavior ?? null,
1965
+ actual_behavior: params.narrative.actualBehavior ?? null,
1966
+ ai_description: params.narrative.aiDescription ?? null,
1967
+ response: params.narrative.response ?? null
1968
+ },
1969
+ context: {
1970
+ page_url: params.context.pageUrl,
1971
+ route_label: params.context.routeLabel,
1972
+ x_position: params.context.xPosition,
1973
+ y_position: params.context.yPosition,
1974
+ target_selector: params.context.targetSelector ?? null,
1975
+ target_relative_x: params.context.targetRelativeX ?? null,
1976
+ target_relative_y: params.context.targetRelativeY ?? null,
1977
+ capture_context: params.context.captureContext ?? null
1978
+ },
1979
+ workflow: {
1980
+ assigned_to: params.workflow.assignedTo ?? null,
1981
+ resolved_by: params.workflow.resolvedBy ?? null,
1982
+ created_by: params.workflow.createdBy,
1983
+ created_at: params.workflow.createdAt ?? null,
1984
+ updated_at: params.workflow.updatedAt ?? null
1985
+ }
1986
+ };
1987
+ }
1988
+ var normalizeText = (value) => {
1989
+ if (typeof value !== "string") return null;
1990
+ const trimmed = value.trim();
1991
+ return trimmed ? trimmed : null;
1992
+ };
1993
+ var buildNarrativeFallback = (payload) => {
1994
+ const description = normalizeText(payload.narrative.description);
1995
+ const expectedBehavior = normalizeText(payload.narrative.expected_behavior);
1996
+ const actualBehavior = normalizeText(payload.narrative.actual_behavior);
1997
+ const sections = [];
1998
+ if (description) sections.push(`Description: ${description}`);
1999
+ if (expectedBehavior) sections.push(`Expected behavior: ${expectedBehavior}`);
2000
+ if (actualBehavior) sections.push(`Actual behavior: ${actualBehavior}`);
2001
+ return {
2002
+ description,
2003
+ expected_behavior: expectedBehavior,
2004
+ actual_behavior: actualBehavior,
2005
+ derived_scope: sections.length > 0 ? sections.join("\n") : null
2006
+ };
2007
+ };
2008
+ function formatAiFixPayloadForCopy(payload) {
2009
+ const refinedSpec = normalizeText(payload.narrative.ai_description);
2010
+ const narrative = buildNarrativeFallback(payload);
2011
+ const aiReadyWithRefinement = Boolean(payload.report.ai_ready && refinedSpec);
2012
+ const copyPayload = {
2013
+ agent_brief: {
2014
+ objective: "Implement and verify the fix for this issue in the current codebase.",
2015
+ scope_directive: aiReadyWithRefinement ? "AI_READY: Use ai_refinement.primary_spec as the source of truth. Use narrative as supporting context only." : "NO_AI_REFINEMENT: Derive scope from narrative.description, narrative.expected_behavior, and narrative.actual_behavior.",
2016
+ implementation_notes: [
2017
+ "Follow existing project patterns and UI conventions.",
2018
+ "If required detail is missing, inspect the referenced page/component before coding.",
2019
+ "Prefer a direct fix over broad refactors."
2020
+ ]
2021
+ },
2022
+ issue: {
2023
+ report_id: payload.report.id,
2024
+ title: payload.report.title,
2025
+ severity: payload.report.severity,
2026
+ status: payload.report.status,
2027
+ type_names: payload.report.type_names,
2028
+ location: {
2029
+ page_url: payload.context.page_url,
2030
+ route_label: payload.context.route_label,
2031
+ target_selector: payload.context.target_selector
2032
+ }
2033
+ },
2034
+ ai_refinement: {
2035
+ ai_ready: payload.report.ai_ready,
2036
+ primary_spec: aiReadyWithRefinement ? refinedSpec : null
2037
+ },
2038
+ narrative,
2039
+ diagnostic_context: payload.context.capture_context ? {
2040
+ browser: payload.context.capture_context.browser.name,
2041
+ viewport: payload.context.capture_context.viewport,
2042
+ timezone: payload.context.capture_context.timezone
2043
+ } : null
2044
+ };
2045
+ return [
2046
+ "AI_FIX_PAYLOAD",
2047
+ "Use this payload to scope and implement the fix.",
2048
+ "```json",
2049
+ JSON.stringify(copyPayload, null, 2),
2050
+ "```"
2051
+ ].join("\n");
2052
+ }
2053
+
2054
+ // src/DevNotesForm.tsx
2055
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2056
+ var COMPACT_BEHAVIOR_HEIGHT = 56;
2057
+ var EXPANDED_BEHAVIOR_MIN_HEIGHT = 92;
2058
+ function SearchableSingleSelect({
2059
+ label,
2060
+ options,
2061
+ value,
2062
+ onChange,
2063
+ placeholder,
2064
+ isSuperscript = false
2065
+ }) {
2066
+ const [searchTerm, setSearchTerm] = (0, import_react6.useState)("");
2067
+ const [showDropdown, setShowDropdown] = (0, import_react6.useState)(false);
2068
+ const selectedOption = (0, import_react6.useMemo)(
2069
+ () => options.find((option) => option.id === value) || null,
2070
+ [options, value]
2071
+ );
2072
+ const filteredOptions = (0, import_react6.useMemo)(() => {
2073
+ const normalized = searchTerm.trim().toLowerCase();
2074
+ return options.filter((option) => option.label.toLowerCase().includes(normalized));
2075
+ }, [options, searchTerm]);
2076
+ const handleSelect = (optionId) => {
2077
+ onChange(optionId);
2078
+ setSearchTerm("");
2079
+ setShowDropdown(false);
2080
+ };
2081
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscript ? "relative" : "", children: [
2082
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2083
+ "label",
2084
+ {
2085
+ className: isSuperscript ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2086
+ children: label
2087
+ }
2088
+ ),
2089
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative", children: [
2090
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "border border-gray-200 rounded-md px-2 py-1 min-h-[40px] bg-white focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 flex items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
2091
+ selectedOption && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-sm bg-transparent px-1 py-0.5 text-xs font-medium text-gray-700", children: [
2092
+ selectedOption.label,
2093
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2094
+ "button",
2095
+ {
2096
+ type: "button",
2097
+ className: "ml-0.5 text-gray-400 hover:text-gray-700",
2098
+ onClick: () => {
2099
+ onChange(null);
2100
+ setSearchTerm("");
2101
+ setShowDropdown(false);
2102
+ },
2103
+ children: "\xD7"
2104
+ }
2105
+ )
2106
+ ] }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2108
+ "input",
2109
+ {
2110
+ type: "text",
2111
+ className: "flex-1 min-w-[120px] border-none outline-none text-sm bg-transparent",
2112
+ placeholder: selectedOption ? "Type to search..." : placeholder,
2113
+ value: searchTerm,
2114
+ onChange: (e) => {
2115
+ setSearchTerm(e.target.value);
2116
+ setShowDropdown(true);
2117
+ },
2118
+ onFocus: () => setShowDropdown(true),
2119
+ onBlur: () => setTimeout(() => setShowDropdown(false), 200),
2120
+ onKeyDown: (e) => {
2121
+ if (e.key === "Enter") {
2122
+ e.preventDefault();
2123
+ if (filteredOptions.length > 0) {
2124
+ handleSelect(filteredOptions[0].id);
2125
+ }
2126
+ }
2127
+ }
2128
+ }
2129
+ )
2130
+ ] }) }),
2131
+ showDropdown && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute top-[calc(100%+4px)] left-0 right-0 bg-white rounded-md shadow-lg border border-gray-200 max-h-[220px] overflow-y-auto z-20", children: filteredOptions.length > 0 ? filteredOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2132
+ "div",
2133
+ {
2134
+ className: `flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 ${option.id === value ? "bg-blue-50" : ""}`,
2135
+ onMouseDown: () => handleSelect(option.id),
2136
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm", children: option.label })
2137
+ },
2138
+ option.id
2139
+ )) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm text-gray-500", children: "No matches" }) }) })
2140
+ ] })
2141
+ ] });
2142
+ }
2143
+ function DevNotesForm({
2144
+ pageUrl,
2145
+ xPosition = 0,
2146
+ yPosition = 0,
2147
+ targetSelector = null,
2148
+ targetRelativeX = null,
2149
+ targetRelativeY = null,
2150
+ existingReport,
2151
+ onSave,
2152
+ onCancel,
2153
+ onDelete
2154
+ }) {
2155
+ const {
2156
+ bugReportTypes,
2157
+ createBugReport,
2158
+ updateBugReport,
2159
+ addBugReportType,
2160
+ deleteBugReportType,
2161
+ taskLists,
2162
+ createTaskList,
2163
+ loading,
2164
+ userProfiles,
2165
+ collaborators,
2166
+ user,
2167
+ aiProvider,
2168
+ requireAi,
2169
+ error: bugReportingError,
2170
+ role
2171
+ } = useDevNotes();
2172
+ const isAdmin = role === "admin" || role === "contributor";
2173
+ const getFirstName = (value) => {
2174
+ if (!value) return "\u2014";
2175
+ const trimmed = value.trim();
2176
+ if (!trimmed) return "\u2014";
2177
+ if (trimmed.includes("@")) {
2178
+ const localPart = trimmed.split("@")[0];
2179
+ return localPart || "\u2014";
2180
+ }
2181
+ const firstToken = trimmed.split(/\s+/)[0];
2182
+ return firstToken || "\u2014";
2183
+ };
2184
+ const availableCollaborators = (0, import_react6.useMemo)(() => {
2185
+ const map = /* @__PURE__ */ new Map();
2186
+ collaborators.forEach((c) => {
2187
+ if (c.id) {
2188
+ map.set(c.id, {
2189
+ id: c.id,
2190
+ label: getFirstName(c.full_name || c.email)
2191
+ });
2192
+ }
2193
+ });
2194
+ Object.entries(userProfiles).forEach(([id, profile]) => {
2195
+ if (!map.has(id)) {
2196
+ map.set(id, {
2197
+ id,
2198
+ label: getFirstName(profile.full_name || profile.email)
2199
+ });
2200
+ }
2201
+ });
2202
+ return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label));
2203
+ }, [collaborators, userProfiles]);
2204
+ const [selectedTypes, setSelectedTypes] = (0, import_react6.useState)(existingReport?.types || []);
2205
+ (0, import_react6.useEffect)(() => {
2206
+ if (existingReport || selectedTypes.length > 0) return;
2207
+ const bugType = bugReportTypes.find((t) => t.name.toLowerCase() === "bug");
2208
+ if (bugType) {
2209
+ setSelectedTypes([bugType.id]);
2210
+ }
2211
+ }, [bugReportTypes, existingReport, selectedTypes.length]);
2212
+ const [severity, setSeverity] = (0, import_react6.useState)(
2213
+ existingReport?.severity || "Medium"
2214
+ );
2215
+ const [title, setTitle] = (0, import_react6.useState)(existingReport?.title || "");
2216
+ const [description, setDescription] = (0, import_react6.useState)(existingReport?.description || "");
2217
+ const [expectedBehavior, setExpectedBehavior] = (0, import_react6.useState)(
2218
+ existingReport?.expected_behavior || ""
2219
+ );
2220
+ const [actualBehavior, setActualBehavior] = (0, import_react6.useState)(existingReport?.actual_behavior || "");
2221
+ const [status, setStatus] = (0, import_react6.useState)(existingReport?.status || "Open");
2222
+ const [assignedTo, setAssignedTo] = (0, import_react6.useState)(existingReport?.assigned_to || null);
2223
+ const [resolvedBy, setResolvedBy] = (0, import_react6.useState)(existingReport?.resolved_by || null);
2224
+ const [approved, setApproved] = (0, import_react6.useState)(existingReport?.approved || false);
2225
+ const [aiReady, setAiReady] = (0, import_react6.useState)(existingReport?.ai_ready || false);
2226
+ const [aiDescription, setAiDescription] = (0, import_react6.useState)(
2227
+ existingReport?.ai_description || null
2228
+ );
2229
+ const [reportPageUrl, setReportPageUrl] = (0, import_react6.useState)(existingReport?.page_url || pageUrl);
2230
+ const defaultTaskListId = (0, import_react6.useMemo)(() => {
2231
+ const defaultList = taskLists.find((list) => list.is_default);
2232
+ return defaultList?.id || taskLists[0]?.id || "";
2233
+ }, [taskLists]);
2234
+ const [taskListId, setTaskListId] = (0, import_react6.useState)(existingReport?.task_list_id || defaultTaskListId);
2235
+ const [newTypeName, setNewTypeName] = (0, import_react6.useState)("");
2236
+ const [showTypeDropdown, setShowTypeDropdown] = (0, import_react6.useState)(false);
2237
+ const [pendingTypeName, setPendingTypeName] = (0, import_react6.useState)(null);
2238
+ const typeInputRef = (0, import_react6.useRef)(null);
2239
+ const [showCopied, setShowCopied] = (0, import_react6.useState)(false);
2240
+ const copyTimeoutRef = (0, import_react6.useRef)(null);
2241
+ const [showLinkCopied, setShowLinkCopied] = (0, import_react6.useState)(false);
2242
+ const linkCopyTimeoutRef = (0, import_react6.useRef)(null);
2243
+ const [showAiPayloadCopied, setShowAiPayloadCopied] = (0, import_react6.useState)(false);
2244
+ const aiPayloadCopyTimeoutRef = (0, import_react6.useRef)(null);
2245
+ const [taskListSearchTerm, setTaskListSearchTerm] = (0, import_react6.useState)("");
2246
+ const [showTaskListDropdown, setShowTaskListDropdown] = (0, import_react6.useState)(false);
2247
+ const [pendingTaskListName, setPendingTaskListName] = (0, import_react6.useState)(null);
2248
+ const taskListInputRef = (0, import_react6.useRef)(null);
2249
+ const descriptionRef = (0, import_react6.useRef)(null);
2250
+ const expectedBehaviorRef = (0, import_react6.useRef)(null);
2251
+ const actualBehaviorRef = (0, import_react6.useRef)(null);
2252
+ const [descriptionHeight, setDescriptionHeight] = (0, import_react6.useState)("120px");
2253
+ const [expectedBehaviorHeight, setExpectedBehaviorHeight] = (0, import_react6.useState)(
2254
+ `${expectedBehavior.trim() ? EXPANDED_BEHAVIOR_MIN_HEIGHT : COMPACT_BEHAVIOR_HEIGHT}px`
2255
+ );
2256
+ const [actualBehaviorHeight, setActualBehaviorHeight] = (0, import_react6.useState)(
2257
+ `${actualBehavior.trim() ? EXPANDED_BEHAVIOR_MIN_HEIGHT : COMPACT_BEHAVIOR_HEIGHT}px`
2258
+ );
2259
+ const [showAiChat, setShowAiChat] = (0, import_react6.useState)(false);
2260
+ const [submitAttempted, setSubmitAttempted] = (0, import_react6.useState)(false);
2261
+ const capturedContext = (0, import_react6.useMemo)(
2262
+ () => existingReport?.capture_context || buildCaptureContext(reportPageUrl),
2263
+ [existingReport?.capture_context, reportPageUrl]
2264
+ );
2265
+ const isSuperscriptLabels = Boolean(existingReport);
2266
+ const severityOptions = [
2267
+ { id: "Critical", label: "Critical" },
2268
+ { id: "High", label: "High" },
2269
+ { id: "Medium", label: "Medium" },
2270
+ { id: "Low", label: "Low" }
2271
+ ];
2272
+ const statusIcons = {
2273
+ Open: { icon: import_fi4.FiAlertCircle, colorClass: "bg-red-100 text-red-800" },
2274
+ "In Progress": { icon: import_fi4.FiLoader, colorClass: "bg-blue-100 text-blue-800" },
2275
+ "Needs Review": { icon: import_fi4.FiEye, colorClass: "bg-purple-100 text-purple-800" },
2276
+ Resolved: { icon: import_fi4.FiCheckCircle, colorClass: "bg-green-100 text-green-800" },
2277
+ Closed: { icon: import_fi4.FiArchive, colorClass: "bg-gray-100 text-gray-800" }
2278
+ };
2279
+ const statusOptions = [
2280
+ { id: "Open", label: "Open" },
2281
+ { id: "In Progress", label: "In Progress" },
2282
+ { id: "Needs Review", label: "Needs Review" },
2283
+ { id: "Resolved", label: "Resolved" },
2284
+ { id: "Closed", label: "Closed" }
2285
+ ];
2286
+ const collaboratorOptions = (0, import_react6.useMemo)(
2287
+ () => availableCollaborators.map((c) => ({
2288
+ id: c.id,
2289
+ label: c.label
2290
+ })),
2291
+ [availableCollaborators]
2292
+ );
2293
+ const formatCreatedDate = (value) => {
2294
+ const createdAt = new Date(value);
2295
+ if (Number.isNaN(createdAt.getTime())) return value;
2296
+ const monthMap = [
2297
+ "Jan.",
2298
+ "Feb.",
2299
+ "Mar.",
2300
+ "Apr.",
2301
+ "May",
2302
+ "Jun.",
2303
+ "Jul.",
2304
+ "Aug.",
2305
+ "Sep.",
2306
+ "Oct.",
2307
+ "Nov.",
2308
+ "Dec."
2309
+ ];
2310
+ const month = monthMap[createdAt.getMonth()];
2311
+ const day = createdAt.getDate();
2312
+ const year = createdAt.getFullYear();
2313
+ const hours24 = createdAt.getHours();
2314
+ const minutes = String(createdAt.getMinutes()).padStart(2, "0");
2315
+ const meridiem = hours24 >= 12 ? "PM" : "AM";
2316
+ const hours12 = hours24 % 12 || 12;
2317
+ const suffix = day % 10 === 1 && day % 100 !== 11 ? "st" : day % 10 === 2 && day % 100 !== 12 ? "nd" : day % 10 === 3 && day % 100 !== 13 ? "rd" : "th";
2318
+ return `${month} ${day}${suffix}, ${year} at ${hours12}:${minutes}${meridiem}.`;
2319
+ };
2320
+ const resizeDescriptionField = () => {
2321
+ const element = descriptionRef.current;
2322
+ if (!element) return;
2323
+ element.style.height = "auto";
2324
+ const nextHeight = Math.max(element.scrollHeight, 120);
2325
+ setDescriptionHeight(`${nextHeight}px`);
2326
+ };
2327
+ const resizeBehaviorField = (element, value, setHeight) => {
2328
+ if (!element) return;
2329
+ if (!value.trim()) {
2330
+ const compact = `${COMPACT_BEHAVIOR_HEIGHT}px`;
2331
+ element.style.height = compact;
2332
+ setHeight(compact);
2333
+ return;
2334
+ }
2335
+ element.style.height = "auto";
2336
+ const nextHeight = Math.max(element.scrollHeight, EXPANDED_BEHAVIOR_MIN_HEIGHT);
2337
+ setHeight(`${nextHeight}px`);
2338
+ };
2339
+ const composePageUrlWithTab = (value) => {
2340
+ return normalizePageUrl(value || "");
2341
+ };
2342
+ (0, import_react6.useEffect)(() => {
2343
+ setReportPageUrl(existingReport?.page_url || pageUrl);
2344
+ }, [existingReport?.page_url, pageUrl]);
2345
+ (0, import_react6.useEffect)(() => {
2346
+ if (!existingReport?.task_list_id && defaultTaskListId && !taskListId) {
2347
+ setTaskListId(defaultTaskListId);
2348
+ }
2349
+ }, [defaultTaskListId, existingReport?.task_list_id, taskListId]);
2350
+ (0, import_react6.useEffect)(() => {
2351
+ return () => {
2352
+ if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
2353
+ if (linkCopyTimeoutRef.current) window.clearTimeout(linkCopyTimeoutRef.current);
2354
+ if (aiPayloadCopyTimeoutRef.current) window.clearTimeout(aiPayloadCopyTimeoutRef.current);
2355
+ };
2356
+ }, []);
2357
+ (0, import_react6.useEffect)(() => {
2358
+ if ((status === "Closed" || status === "Resolved") && !resolvedBy && user?.id) {
2359
+ setResolvedBy(user.id);
2360
+ }
2361
+ }, [status, resolvedBy, user?.id]);
2362
+ (0, import_react6.useEffect)(() => {
2363
+ resizeDescriptionField();
2364
+ }, [description]);
2365
+ (0, import_react6.useEffect)(() => {
2366
+ resizeBehaviorField(expectedBehaviorRef.current, expectedBehavior, setExpectedBehaviorHeight);
2367
+ }, [expectedBehavior]);
2368
+ (0, import_react6.useEffect)(() => {
2369
+ resizeBehaviorField(actualBehaviorRef.current, actualBehavior, setActualBehaviorHeight);
2370
+ }, [actualBehavior]);
2371
+ const availableTypes = bugReportTypes.filter((type) => !selectedTypes.includes(type.id));
2372
+ const handleTypeSelect = (typeId) => {
2373
+ setSelectedTypes((prev) => [...prev, typeId]);
2374
+ setShowTypeDropdown(false);
2375
+ setNewTypeName("");
2376
+ };
2377
+ const handleTypeRemove = (typeId) => {
2378
+ setSelectedTypes((prev) => prev.filter((id) => id !== typeId));
2379
+ };
2380
+ const createTypeFromValue = async (value) => {
2381
+ const trimmedValue = value.trim();
2382
+ if (!trimmedValue) return;
2383
+ const existingType = bugReportTypes.find(
2384
+ (type) => type.name.toLowerCase() === trimmedValue.toLowerCase()
2385
+ );
2386
+ if (existingType) {
2387
+ if (!selectedTypes.includes(existingType.id)) {
2388
+ setSelectedTypes((prev) => [...prev, existingType.id]);
2389
+ }
2390
+ setNewTypeName("");
2391
+ setShowTypeDropdown(false);
2392
+ setPendingTypeName(null);
2393
+ return;
2394
+ }
2395
+ const newType = await addBugReportType(trimmedValue);
2396
+ if (newType) {
2397
+ setSelectedTypes((prev) => [...prev, newType.id]);
2398
+ setNewTypeName("");
2399
+ setShowTypeDropdown(false);
2400
+ setPendingTypeName(null);
2401
+ }
2402
+ };
2403
+ const handleTypeKeyDown = (e) => {
2404
+ if (e.key === "Enter") {
2405
+ e.preventDefault();
2406
+ if (pendingTypeName && e.shiftKey) {
2407
+ createTypeFromValue(pendingTypeName);
2408
+ return;
2409
+ }
2410
+ const trimmedValue = newTypeName.trim();
2411
+ if (!trimmedValue) return;
2412
+ const existingType = bugReportTypes.find(
2413
+ (type) => type.name.toLowerCase() === trimmedValue.toLowerCase()
2414
+ );
2415
+ if (existingType) {
2416
+ handleTypeSelect(existingType.id);
2417
+ return;
2418
+ }
2419
+ setPendingTypeName(trimmedValue);
2420
+ }
2421
+ };
2422
+ const handleDeleteType = async (typeId, e) => {
2423
+ e.stopPropagation();
2424
+ const typeToDelete = bugReportTypes.find((t) => t.id === typeId);
2425
+ if (typeToDelete?.is_default) return;
2426
+ const success = await deleteBugReportType(typeId);
2427
+ if (success) {
2428
+ setSelectedTypes((prev) => prev.filter((id) => id !== typeId));
2429
+ }
2430
+ };
2431
+ const handleCopyTaskId = async () => {
2432
+ if (!existingReport?.id) return;
2433
+ try {
2434
+ await navigator.clipboard.writeText(existingReport.id);
2435
+ setShowCopied(true);
2436
+ if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
2437
+ copyTimeoutRef.current = window.setTimeout(() => setShowCopied(false), 1200);
2438
+ } catch (err) {
2439
+ console.error("[DevNotesForm] Failed to copy task id", err);
2440
+ }
2441
+ };
2442
+ const handleCopyLink = async () => {
2443
+ if (!existingReport?.id) return;
2444
+ try {
2445
+ const link = `${window.location.origin}/bug-reports/${existingReport.id}`;
2446
+ await navigator.clipboard.writeText(link);
2447
+ setShowLinkCopied(true);
2448
+ if (linkCopyTimeoutRef.current) window.clearTimeout(linkCopyTimeoutRef.current);
2449
+ linkCopyTimeoutRef.current = window.setTimeout(() => setShowLinkCopied(false), 1200);
2450
+ } catch (err) {
2451
+ console.error("[DevNotesForm] Failed to copy link", err);
2452
+ }
2453
+ };
2454
+ const handleCopyAiPayload = async () => {
2455
+ const typeNames = selectedTypes.map((typeId) => {
2456
+ const type = bugReportTypes.find((item) => item.id === typeId);
2457
+ return type?.name || typeId;
2458
+ });
2459
+ const normalizedPageUrl = normalizePageUrl(composePageUrlWithTab(reportPageUrl));
2460
+ const payload = buildAiFixPayload({
2461
+ source: "@the-portland-company/devnotes",
2462
+ report: {
2463
+ id: existingReport?.id || null,
2464
+ title: title.trim() || null,
2465
+ status,
2466
+ severity,
2467
+ taskListId: taskListId || null,
2468
+ types: selectedTypes,
2469
+ typeNames,
2470
+ approved,
2471
+ aiReady
2472
+ },
2473
+ narrative: {
2474
+ description: description.trim() || null,
2475
+ expectedBehavior: expectedBehavior.trim() || null,
2476
+ actualBehavior: actualBehavior.trim() || null,
2477
+ aiDescription: aiDescription || null,
2478
+ response: existingReport?.response || null
2479
+ },
2480
+ context: {
2481
+ pageUrl: normalizedPageUrl,
2482
+ routeLabel: capturedContext?.route_label || deriveRouteLabelFromUrl(normalizedPageUrl),
2483
+ xPosition: existingReport?.x_position ?? xPosition,
2484
+ yPosition: existingReport?.y_position ?? yPosition,
2485
+ targetSelector: existingReport?.target_selector ?? targetSelector ?? null,
2486
+ targetRelativeX: existingReport?.target_relative_x ?? targetRelativeX ?? null,
2487
+ targetRelativeY: existingReport?.target_relative_y ?? targetRelativeY ?? null,
2488
+ captureContext: capturedContext
2489
+ },
2490
+ workflow: {
2491
+ assignedTo: assignedTo || null,
2492
+ resolvedBy: resolvedBy || null,
2493
+ createdBy: existingReport?.created_by || user.id,
2494
+ createdAt: existingReport?.created_at || null,
2495
+ updatedAt: existingReport?.updated_at || null
2496
+ }
2497
+ });
2498
+ const copyText = formatAiFixPayloadForCopy(payload);
2499
+ try {
2500
+ await navigator.clipboard.writeText(copyText);
2501
+ setShowAiPayloadCopied(true);
2502
+ if (aiPayloadCopyTimeoutRef.current) {
2503
+ window.clearTimeout(aiPayloadCopyTimeoutRef.current);
2504
+ }
2505
+ aiPayloadCopyTimeoutRef.current = window.setTimeout(
2506
+ () => setShowAiPayloadCopied(false),
2507
+ 1400
2508
+ );
2509
+ } catch (err) {
2510
+ console.error("[DevNotesForm] Failed to copy AI payload", err);
2511
+ }
2512
+ };
2513
+ const trimmedDescription = description.trim();
2514
+ const trimmedExpectedBehavior = expectedBehavior.trim();
2515
+ const trimmedActualBehavior = actualBehavior.trim();
2516
+ const hasDescription = trimmedDescription.length > 0;
2517
+ const hasBehavior = trimmedExpectedBehavior.length > 0 || trimmedActualBehavior.length > 0;
2518
+ const hasNarrative = hasDescription || hasBehavior;
2519
+ const aiRequired = requireAi && !existingReport && !aiDescription;
2520
+ const submitDisabled = loading || aiRequired || !hasNarrative;
2521
+ const submitTitle = aiRequired ? "AI refinement is required before submitting" : !hasNarrative ? "Add a description, expected behavior, or actual behavior" : existingReport ? "Update" : "Save";
2522
+ const aiSeedDescription = hasDescription ? trimmedDescription : hasBehavior ? [trimmedExpectedBehavior, trimmedActualBehavior].filter(Boolean).join("\n") : title.trim();
2523
+ const handleSubmit = async () => {
2524
+ setSubmitAttempted(true);
2525
+ if (!title.trim() || !taskListId || selectedTypes.length === 0 || !hasNarrative) return;
2526
+ if (aiRequired) return;
2527
+ const reportData = {
2528
+ task_list_id: taskListId,
2529
+ page_url: normalizePageUrl(composePageUrlWithTab(reportPageUrl)),
2530
+ x_position: xPosition,
2531
+ y_position: yPosition,
2532
+ target_selector: targetSelector,
2533
+ target_relative_x: targetRelativeX,
2534
+ target_relative_y: targetRelativeY,
2535
+ types: selectedTypes,
2536
+ severity,
2537
+ title: title.trim(),
2538
+ description: trimmedDescription || null,
2539
+ expected_behavior: trimmedExpectedBehavior || null,
2540
+ actual_behavior: trimmedActualBehavior || null,
2541
+ response: null,
2542
+ status,
2543
+ assigned_to: assignedTo,
2544
+ resolved_by: resolvedBy,
2545
+ approved,
2546
+ ai_ready: aiReady,
2547
+ ai_description: aiDescription
2548
+ };
2549
+ let result = null;
2550
+ if (existingReport) {
2551
+ result = await updateBugReport(existingReport.id, {
2552
+ ...reportData,
2553
+ capture_context: existingReport.capture_context || capturedContext,
2554
+ assigned_to: assignedTo,
2555
+ resolved_by: resolvedBy
2556
+ });
2557
+ } else {
2558
+ result = await createBugReport({
2559
+ ...reportData,
2560
+ capture_context: capturedContext
2561
+ });
2562
+ }
2563
+ if (result) {
2564
+ onSave(result);
2565
+ }
2566
+ };
2567
+ const getTypeName = (typeId) => {
2568
+ const type = bugReportTypes.find((t) => t.id === typeId);
2569
+ return type?.name || "Unknown";
2570
+ };
2571
+ const getTaskListName = (listId) => {
2572
+ const list = taskLists.find((l) => l.id === listId);
2573
+ return list?.name || "";
2574
+ };
2575
+ const handleTaskListSelect = (listId) => {
2576
+ setTaskListId(listId);
2577
+ setTaskListSearchTerm("");
2578
+ setShowTaskListDropdown(false);
2579
+ setPendingTaskListName(null);
2580
+ };
2581
+ const createTaskListFromValue = async (value) => {
2582
+ const trimmedValue = value.trim();
2583
+ if (!trimmedValue) return;
2584
+ const existingList = taskLists.find(
2585
+ (list) => list.name.toLowerCase() === trimmedValue.toLowerCase()
2586
+ );
2587
+ if (existingList) {
2588
+ setTaskListId(existingList.id);
2589
+ setTaskListSearchTerm("");
2590
+ setShowTaskListDropdown(false);
2591
+ setPendingTaskListName(null);
2592
+ return;
2593
+ }
2594
+ const created = await createTaskList(trimmedValue);
2595
+ if (created) {
2596
+ setTaskListId(created.id);
2597
+ setTaskListSearchTerm("");
2598
+ setShowTaskListDropdown(false);
2599
+ setPendingTaskListName(null);
2600
+ }
2601
+ };
2602
+ const handleTaskListKeyDown = (e) => {
2603
+ if (e.key === "Enter") {
2604
+ e.preventDefault();
2605
+ if (pendingTaskListName && e.shiftKey) {
2606
+ createTaskListFromValue(pendingTaskListName);
2607
+ return;
2608
+ }
2609
+ const trimmedValue = taskListSearchTerm.trim();
2610
+ if (!trimmedValue) return;
2611
+ const existingList = taskLists.find(
2612
+ (list) => list.name.toLowerCase() === trimmedValue.toLowerCase()
2613
+ );
2614
+ if (existingList) {
2615
+ handleTaskListSelect(existingList.id);
2616
+ return;
2617
+ }
2618
+ setPendingTaskListName(trimmedValue);
2619
+ }
2620
+ };
2621
+ const StatusIcon = statusIcons[status]?.icon || import_fi4.FiAlertCircle;
2622
+ const statusColorClass = statusIcons[status]?.colorClass || "bg-red-100 text-red-800";
2623
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "bg-white rounded-xl p-4 md:p-6 min-w-[320px] w-full max-w-[960px] mx-auto relative shadow-sm", children: [
2624
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-between items-start mb-3", children: [
2625
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-col gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
2626
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-bold text-base", children: existingReport ? "Edit Bug Report" : "Report Bug" }),
2627
+ existingReport && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: `text-xs px-2 py-0.5 rounded-full flex items-center gap-1 ${statusColorClass}`, children: [
2628
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatusIcon, { size: 12 }),
2629
+ status
2630
+ ] })
2631
+ ] }) }),
2632
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
2633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2634
+ "button",
2635
+ {
2636
+ type: "button",
2637
+ className: "p-1.5 rounded hover:bg-gray-100 text-gray-500",
2638
+ onClick: onCancel,
2639
+ "aria-label": "Cancel",
2640
+ title: "Cancel",
2641
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiX, { size: 16 })
2642
+ }
2643
+ ),
2644
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2645
+ "button",
2646
+ {
2647
+ type: "button",
2648
+ className: "p-1.5 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50",
2649
+ onClick: handleSubmit,
2650
+ disabled: submitDisabled,
2651
+ "aria-label": existingReport ? "Update" : "Save",
2652
+ title: submitTitle,
2653
+ children: loading ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiCheck, { size: 16 })
2654
+ }
2655
+ )
2656
+ ] })
2657
+ ] }),
2658
+ existingReport && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-wrap items-center gap-2 mb-3 text-xs relative", children: [
2659
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-gray-500", children: [
2660
+ "Created by",
2661
+ " ",
2662
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium text-gray-700", children: getFirstName(
2663
+ existingReport.creator?.full_name || existingReport.creator?.email || "Unknown"
2664
+ ) }),
2665
+ " on ",
2666
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-gray-600", children: formatCreatedDate(existingReport.created_at) })
2667
+ ] }),
2668
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-gray-400", children: "|" }),
2669
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-gray-500", children: "Task ID" }),
2670
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2671
+ "button",
2672
+ {
2673
+ type: "button",
2674
+ className: "font-mono text-gray-800 hover:underline",
2675
+ onClick: handleCopyTaskId,
2676
+ children: existingReport.id
2677
+ }
2678
+ ),
2679
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-gray-400", children: "|" }),
2680
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2681
+ "button",
2682
+ {
2683
+ type: "button",
2684
+ className: "inline-flex items-center gap-1 text-blue-600 hover:underline",
2685
+ onClick: handleCopyLink,
2686
+ title: "Copy shareable link",
2687
+ children: [
2688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiLink2, { size: 12 }),
2689
+ "Copy Link"
2690
+ ]
2691
+ }
2692
+ ),
2693
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-gray-400", children: "|" }),
2694
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2695
+ "button",
2696
+ {
2697
+ type: "button",
2698
+ className: "inline-flex items-center gap-1 text-purple-700 hover:underline",
2699
+ onClick: handleCopyAiPayload,
2700
+ title: "Copy AI fix payload",
2701
+ children: [
2702
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiCopy, { size: 12 }),
2703
+ "Copy AI Payload"
2704
+ ]
2705
+ }
2706
+ ),
2707
+ (showCopied || showLinkCopied) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "absolute left-0 top-full mt-1 text-xs text-black animate-devnotes-fade-up pointer-events-none", children: showLinkCopied ? "Link copied!" : "Copied!" }),
2708
+ showAiPayloadCopied && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "absolute left-0 top-full mt-1 text-xs text-black animate-devnotes-fade-up pointer-events-none", children: "AI payload copied!" })
2709
+ ] }),
2710
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-5", children: [
2711
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-4", children: [
2712
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative my-3" : "my-3", children: [
2713
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2714
+ "label",
2715
+ {
2716
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2717
+ children: [
2718
+ "Title ",
2719
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-red-500", children: "*" })
2720
+ ]
2721
+ }
2722
+ ),
2723
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2724
+ "input",
2725
+ {
2726
+ type: "text",
2727
+ className: "w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none hover:border-gray-400",
2728
+ placeholder: "Brief description of the issue",
2729
+ value: title,
2730
+ onChange: (e) => setTitle(e.target.value)
2731
+ }
2732
+ )
2733
+ ] }),
2734
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
2735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2736
+ "label",
2737
+ {
2738
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2739
+ children: "Description"
2740
+ }
2741
+ ),
2742
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2743
+ "textarea",
2744
+ {
2745
+ ref: descriptionRef,
2746
+ className: "w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none resize-none transition-[height] duration-200 hover:border-gray-400",
2747
+ placeholder: "Detailed description (optional)",
2748
+ value: description,
2749
+ onChange: (e) => setDescription(e.target.value),
2750
+ onInput: resizeDescriptionField,
2751
+ rows: 5,
2752
+ style: { minHeight: "120px", height: descriptionHeight }
2753
+ }
2754
+ )
2755
+ ] }),
2756
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 py-1", children: [
2757
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px flex-1 bg-gray-200" }),
2758
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "inline-flex items-center rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 text-[11px] font-semibold tracking-wide text-gray-500", children: "OR" }),
2759
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-px flex-1 bg-gray-200" })
2760
+ ] }),
2761
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [
2762
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
2763
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2764
+ "label",
2765
+ {
2766
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2767
+ children: "Expected Behavior"
2768
+ }
2769
+ ),
2770
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2771
+ "textarea",
2772
+ {
2773
+ ref: expectedBehaviorRef,
2774
+ className: "w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none resize-none transition-[height] duration-200 hover:border-gray-400",
2775
+ placeholder: "What should have happened?",
2776
+ value: expectedBehavior,
2777
+ onChange: (e) => setExpectedBehavior(e.target.value),
2778
+ onInput: (e) => resizeBehaviorField(
2779
+ e.currentTarget,
2780
+ e.currentTarget.value,
2781
+ setExpectedBehaviorHeight
2782
+ ),
2783
+ rows: 2,
2784
+ style: {
2785
+ minHeight: `${COMPACT_BEHAVIOR_HEIGHT}px`,
2786
+ height: expectedBehaviorHeight
2787
+ }
2788
+ }
2789
+ )
2790
+ ] }),
2791
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
2792
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2793
+ "label",
2794
+ {
2795
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2796
+ children: "Actual Behavior"
2797
+ }
2798
+ ),
2799
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2800
+ "textarea",
2801
+ {
2802
+ ref: actualBehaviorRef,
2803
+ className: "w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none resize-none transition-[height] duration-200 hover:border-gray-400",
2804
+ placeholder: "What actually happened?",
2805
+ value: actualBehavior,
2806
+ onChange: (e) => setActualBehavior(e.target.value),
2807
+ onInput: (e) => resizeBehaviorField(
2808
+ e.currentTarget,
2809
+ e.currentTarget.value,
2810
+ setActualBehaviorHeight
2811
+ ),
2812
+ rows: 2,
2813
+ style: {
2814
+ minHeight: `${COMPACT_BEHAVIOR_HEIGHT}px`,
2815
+ height: actualBehaviorHeight
2816
+ }
2817
+ }
2818
+ )
2819
+ ] })
2820
+ ] }),
2821
+ submitAttempted && !hasNarrative && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-red-600", children: "Add a description, expected behavior, or actual behavior." }),
2822
+ !aiProvider && !aiDescription && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-gray-400 italic", children: "AI Refinement Off" }),
2823
+ aiProvider && !aiDescription && !showAiChat && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2824
+ "button",
2825
+ {
2826
+ type: "button",
2827
+ className: `w-full py-3 rounded-xl border-2 bg-white text-purple-700 font-medium hover:bg-purple-50 flex items-center justify-center gap-2 transition-all ${requireAi && !existingReport ? "border-purple-500 shadow-[0_0_0_3px_rgba(167,139,250,0.3)] hover:border-purple-600" : "border-purple-300 shadow-[0_0_0_3px_rgba(167,139,250,0.15)] hover:border-purple-400"}`,
2828
+ onClick: () => setShowAiChat(true),
2829
+ children: [
2830
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiZap, { size: 18 }),
2831
+ "Refine with AI",
2832
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `text-[0.65rem] px-2 py-0.5 rounded-full font-semibold uppercase tracking-wide ${requireAi && !existingReport ? "bg-red-100 text-red-700" : "bg-purple-100 text-purple-700"}`, children: requireAi && !existingReport ? "Required" : "Recommended" })
2833
+ ]
2834
+ }
2835
+ ),
2836
+ showAiChat && aiProvider && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2837
+ AiDescriptionChat,
2838
+ {
2839
+ initialDescription: aiSeedDescription,
2840
+ context: {
2841
+ title,
2842
+ page_url: reportPageUrl,
2843
+ route_label: capturedContext?.route_label || deriveRouteLabelFromUrl(reportPageUrl),
2844
+ severity,
2845
+ types: selectedTypes,
2846
+ target_selector: targetSelector ?? void 0,
2847
+ expected_behavior: expectedBehavior || void 0,
2848
+ actual_behavior: actualBehavior || void 0,
2849
+ capture_context: capturedContext || void 0
2850
+ },
2851
+ aiProvider,
2852
+ onAccept: (refined) => {
2853
+ setAiDescription(refined);
2854
+ setAiReady(true);
2855
+ setShowAiChat(false);
2856
+ },
2857
+ onCancel: () => setShowAiChat(false)
2858
+ }
2859
+ ),
2860
+ aiDescription && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "bg-green-50 border border-green-300 rounded-lg p-3", children: [
2861
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-between items-center mb-1", children: [
2862
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-xs font-bold text-green-700", children: "AI-Refined Description" }),
2863
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2864
+ "button",
2865
+ {
2866
+ type: "button",
2867
+ className: "text-xs text-red-600 hover:text-red-700",
2868
+ onClick: () => {
2869
+ setAiDescription(null);
2870
+ setAiReady(false);
2871
+ },
2872
+ children: "Remove"
2873
+ }
2874
+ )
2875
+ ] }),
2876
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm whitespace-pre-wrap text-gray-800", children: aiDescription }),
2877
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2878
+ "button",
2879
+ {
2880
+ type: "button",
2881
+ className: "inline-flex items-center gap-1 text-xs text-purple-700 hover:text-purple-800",
2882
+ onClick: handleCopyAiPayload,
2883
+ children: [
2884
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiCopy, { size: 12 }),
2885
+ "Copy AI Fix Payload"
2886
+ ]
2887
+ }
2888
+ ) }),
2889
+ showAiPayloadCopied && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-1 text-xs text-purple-700 text-right", children: "AI payload copied!" })
2890
+ ] }),
2891
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4", children: [
2892
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
2893
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2894
+ "label",
2895
+ {
2896
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
2897
+ children: "Type(s)"
2898
+ }
2899
+ ),
2900
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative", children: [
2901
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "border border-gray-200 rounded-md px-2 py-1 min-h-[40px] bg-white focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 flex items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
2902
+ selectedTypes.map((typeId) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2903
+ "span",
2904
+ {
2905
+ className: "inline-flex items-center gap-1 rounded-sm bg-transparent px-1 py-0.5 text-xs font-medium text-gray-700",
2906
+ children: [
2907
+ getTypeName(typeId),
2908
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2909
+ "button",
2910
+ {
2911
+ type: "button",
2912
+ className: "ml-0.5 text-gray-400 hover:text-gray-700",
2913
+ onClick: () => handleTypeRemove(typeId),
2914
+ children: "\xD7"
2915
+ }
2916
+ )
2917
+ ]
2918
+ },
2919
+ typeId
2920
+ )),
2921
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2922
+ "input",
2923
+ {
2924
+ ref: typeInputRef,
2925
+ type: "text",
2926
+ className: "flex-1 min-w-[120px] border-none outline-none text-sm bg-transparent",
2927
+ placeholder: "Type to search or add...",
2928
+ value: newTypeName,
2929
+ onChange: (e) => {
2930
+ setPendingTypeName(null);
2931
+ setNewTypeName(e.target.value);
2932
+ setShowTypeDropdown(true);
2933
+ },
2934
+ onFocus: () => setShowTypeDropdown(true),
2935
+ onBlur: () => setTimeout(() => setShowTypeDropdown(false), 200),
2936
+ onKeyDown: handleTypeKeyDown
2937
+ }
2938
+ )
2939
+ ] }) }),
2940
+ showTypeDropdown && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "absolute top-[calc(100%+4px)] left-0 right-0 bg-white rounded-md shadow-lg border border-gray-200 max-h-[200px] overflow-y-auto z-20", children: [
2941
+ availableTypes.filter(
2942
+ (type) => type.name.toLowerCase().includes(newTypeName.toLowerCase())
2943
+ ).map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2944
+ "div",
2945
+ {
2946
+ className: "flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-100",
2947
+ onMouseDown: () => handleTypeSelect(type.id),
2948
+ children: [
2949
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm", children: type.name }),
2950
+ !type.is_default && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2951
+ "button",
2952
+ {
2953
+ type: "button",
2954
+ className: "p-1 text-red-500 hover:text-red-700",
2955
+ "aria-label": "Delete type",
2956
+ onMouseDown: (e) => handleDeleteType(type.id, e),
2957
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiTrash2, { size: 12 })
2958
+ }
2959
+ )
2960
+ ]
2961
+ },
2962
+ type.id
2963
+ )),
2964
+ newTypeName.trim() && !bugReportTypes.some(
2965
+ (t) => t.name.toLowerCase() === newTypeName.trim().toLowerCase()
2966
+ ) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2967
+ "div",
2968
+ {
2969
+ className: "px-3 py-2 cursor-pointer bg-blue-50 hover:bg-blue-100",
2970
+ onMouseDown: () => setPendingTypeName(newTypeName.trim()),
2971
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-sm text-blue-600", children: [
2972
+ '+ Queue "',
2973
+ newTypeName.trim(),
2974
+ '" for approval'
2975
+ ] })
2976
+ }
2977
+ ),
2978
+ availableTypes.length === 0 && !newTypeName.trim() && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm text-gray-500", children: "No more types available" }) })
2979
+ ] }),
2980
+ pendingTypeName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute top-[calc(100%+8px)] left-0 bg-white border border-yellow-300 rounded-md shadow-md p-2 z-30", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-2", children: [
2981
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-xs text-gray-700", children: [
2982
+ 'Add "',
2983
+ pendingTypeName,
2984
+ '"? Press Shift+Enter or approve.'
2985
+ ] }),
2986
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-end gap-2", children: [
2987
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2988
+ "button",
2989
+ {
2990
+ type: "button",
2991
+ className: "px-2 py-1 text-xs rounded hover:bg-gray-100",
2992
+ onClick: () => setPendingTypeName(null),
2993
+ children: "Cancel"
2994
+ }
2995
+ ),
2996
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2997
+ "button",
2998
+ {
2999
+ type: "button",
3000
+ className: "px-2 py-1 text-xs rounded bg-yellow-400 hover:bg-yellow-500",
3001
+ onClick: () => createTypeFromValue(pendingTypeName),
3002
+ children: "Approve"
3003
+ }
3004
+ )
3005
+ ] })
3006
+ ] }) })
3007
+ ] })
3008
+ ] }),
3009
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3010
+ SearchableSingleSelect,
3011
+ {
3012
+ label: "Severity",
3013
+ options: severityOptions,
3014
+ value: severity,
3015
+ onChange: (value) => {
3016
+ if (!value) return;
3017
+ setSeverity(value);
3018
+ },
3019
+ placeholder: "Search severity...",
3020
+ isSuperscript: isSuperscriptLabels
3021
+ }
3022
+ ),
3023
+ isAdmin && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
3024
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3025
+ "label",
3026
+ {
3027
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
3028
+ children: "Assignment & Workflow"
3029
+ }
3030
+ ),
3031
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-2", children: [
3032
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3033
+ SearchableSingleSelect,
3034
+ {
3035
+ label: "Assignee",
3036
+ options: [{ id: "", label: "Unassigned" }, ...collaboratorOptions],
3037
+ value: assignedTo ?? "",
3038
+ onChange: (value) => setAssignedTo(value || null),
3039
+ placeholder: "Search assignee...",
3040
+ isSuperscript: isSuperscriptLabels
3041
+ }
3042
+ ),
3043
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-4 flex-wrap", children: [
3044
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "inline-flex items-center gap-1.5 text-sm cursor-pointer", children: [
3045
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3046
+ "input",
3047
+ {
3048
+ type: "checkbox",
3049
+ checked: approved,
3050
+ onChange: (e) => setApproved(e.target.checked),
3051
+ className: "rounded border-gray-300 text-yellow-500 focus:ring-yellow-500"
3052
+ }
3053
+ ),
3054
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { title: "When a Senior Engineer has reviewed this task from the Submitter it should be marked as Approved for the development team to complete.", children: "Approved" })
3055
+ ] }),
3056
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "inline-flex items-center gap-1.5 text-sm cursor-pointer", children: [
3057
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3058
+ "input",
3059
+ {
3060
+ type: "checkbox",
3061
+ checked: aiReady,
3062
+ onChange: (e) => {
3063
+ setAiReady(e.target.checked);
3064
+ if (e.target.checked) setApproved(true);
3065
+ },
3066
+ className: "rounded border-gray-300 text-blue-500 focus:ring-blue-500"
3067
+ }
3068
+ ),
3069
+ "AI Ready"
3070
+ ] })
3071
+ ] })
3072
+ ] })
3073
+ ] }),
3074
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
3075
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3076
+ "label",
3077
+ {
3078
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
3079
+ children: "Task List"
3080
+ }
3081
+ ),
3082
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative", children: [
3083
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "border border-gray-200 rounded-md px-2 py-1 min-h-[40px] bg-white focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 flex items-center", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
3084
+ taskListId && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "inline-flex items-center gap-1 bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded-full", children: [
3085
+ getTaskListName(taskListId),
3086
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3087
+ "button",
3088
+ {
3089
+ type: "button",
3090
+ className: "ml-0.5 hover:text-blue-600",
3091
+ onClick: () => setTaskListId(""),
3092
+ children: "\xD7"
3093
+ }
3094
+ )
3095
+ ] }),
3096
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3097
+ "input",
3098
+ {
3099
+ ref: taskListInputRef,
3100
+ type: "text",
3101
+ className: "flex-1 min-w-[120px] border-none outline-none text-sm bg-transparent",
3102
+ placeholder: "Type to search or add...",
3103
+ value: taskListSearchTerm,
3104
+ onChange: (e) => {
3105
+ setPendingTaskListName(null);
3106
+ setTaskListSearchTerm(e.target.value);
3107
+ setShowTaskListDropdown(true);
3108
+ },
3109
+ onFocus: () => setShowTaskListDropdown(true),
3110
+ onBlur: () => setTimeout(() => setShowTaskListDropdown(false), 200),
3111
+ onKeyDown: handleTaskListKeyDown
3112
+ }
3113
+ )
3114
+ ] }) }),
3115
+ showTaskListDropdown && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "absolute top-[calc(100%+4px)] left-0 right-0 bg-white rounded-md shadow-lg border border-gray-200 max-h-[200px] overflow-y-auto z-20", children: [
3116
+ taskLists.filter(
3117
+ (list) => list.name.toLowerCase().includes(taskListSearchTerm.toLowerCase())
3118
+ ).map((list) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3119
+ "div",
3120
+ {
3121
+ className: `flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-gray-100 ${list.id === taskListId ? "bg-blue-50" : ""}`,
3122
+ onMouseDown: () => handleTaskListSelect(list.id),
3123
+ children: [
3124
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3125
+ "span",
3126
+ {
3127
+ className: `text-sm ${list.id === taskListId ? "font-medium" : ""}`,
3128
+ children: list.name
3129
+ }
3130
+ ),
3131
+ list.is_default && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-800", children: "Default" })
3132
+ ]
3133
+ },
3134
+ list.id
3135
+ )),
3136
+ taskListSearchTerm.trim() && !taskLists.some(
3137
+ (list) => list.name.toLowerCase() === taskListSearchTerm.trim().toLowerCase()
3138
+ ) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3139
+ "div",
3140
+ {
3141
+ className: "px-3 py-2 cursor-pointer bg-blue-50 hover:bg-blue-100",
3142
+ onMouseDown: () => setPendingTaskListName(taskListSearchTerm.trim()),
3143
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "text-sm text-blue-600", children: [
3144
+ '+ Queue "',
3145
+ taskListSearchTerm.trim(),
3146
+ '" for approval'
3147
+ ] })
3148
+ }
3149
+ ),
3150
+ taskLists.length === 0 && !taskListSearchTerm.trim() && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-sm text-gray-500", children: "No task lists available" }) })
3151
+ ] }),
3152
+ pendingTaskListName && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute top-[calc(100%+8px)] left-0 bg-white border border-yellow-300 rounded-md shadow-md p-2 z-30", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-2", children: [
3153
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-xs text-gray-700", children: [
3154
+ 'Add "',
3155
+ pendingTaskListName,
3156
+ '"? Press Shift+Enter or approve.'
3157
+ ] }),
3158
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-end gap-2", children: [
3159
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3160
+ "button",
3161
+ {
3162
+ type: "button",
3163
+ className: "px-2 py-1 text-xs rounded hover:bg-gray-100",
3164
+ onClick: () => setPendingTaskListName(null),
3165
+ children: "Cancel"
3166
+ }
3167
+ ),
3168
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3169
+ "button",
3170
+ {
3171
+ type: "button",
3172
+ className: "px-2 py-1 text-xs rounded bg-yellow-400 hover:bg-yellow-500",
3173
+ onClick: () => createTaskListFromValue(pendingTaskListName),
3174
+ children: "Approve"
3175
+ }
3176
+ )
3177
+ ] })
3178
+ ] }) })
3179
+ ] })
3180
+ ] }),
3181
+ existingReport && isAdmin && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3182
+ SearchableSingleSelect,
3183
+ {
3184
+ label: "Status",
3185
+ options: statusOptions,
3186
+ value: status,
3187
+ onChange: (value) => {
3188
+ if (!value) return;
3189
+ setStatus(value);
3190
+ },
3191
+ placeholder: "Search status...",
3192
+ isSuperscript: isSuperscriptLabels
3193
+ }
3194
+ ),
3195
+ existingReport && isAdmin && (status === "Closed" || status === "Resolved") && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3196
+ SearchableSingleSelect,
3197
+ {
3198
+ label: "Resolved By",
3199
+ options: [{ id: "", label: "Not Set" }, ...collaboratorOptions],
3200
+ value: resolvedBy ?? "",
3201
+ onChange: (value) => setResolvedBy(value || null),
3202
+ placeholder: "Search resolver...",
3203
+ isSuperscript: isSuperscriptLabels
3204
+ }
3205
+ ),
3206
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: isSuperscriptLabels ? "relative" : "", children: [
3207
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3208
+ "label",
3209
+ {
3210
+ className: isSuperscriptLabels ? "absolute -top-[9px] left-[10px] bg-white px-1.5 text-xs leading-4 rounded-md pointer-events-none z-[2] text-gray-700" : "block text-sm mb-1 text-gray-700",
3211
+ children: "Page URL"
3212
+ }
3213
+ ),
3214
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative", children: [
3215
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3216
+ "input",
3217
+ {
3218
+ type: "text",
3219
+ className: `w-full rounded-md border border-gray-200 px-3 py-1.5 pr-10 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none hover:border-gray-400 ${!existingReport ? "bg-gray-50 cursor-not-allowed" : ""}`,
3220
+ value: reportPageUrl,
3221
+ onChange: (e) => setReportPageUrl(e.target.value),
3222
+ readOnly: !existingReport
3223
+ }
3224
+ ),
3225
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3226
+ "a",
3227
+ {
3228
+ href: composePageUrlWithTab(reportPageUrl),
3229
+ target: "_blank",
3230
+ rel: "noreferrer",
3231
+ className: "absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600",
3232
+ title: "Open in new tab",
3233
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiExternalLink, { size: 14 })
3234
+ }
3235
+ )
3236
+ ] })
3237
+ ] })
3238
+ ] }),
3239
+ existingReport && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DevNotesDiscussion, { report: existingReport })
3240
+ ] }),
3241
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex justify-between pt-2", children: [
3242
+ existingReport && onDelete ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3243
+ "button",
3244
+ {
3245
+ type: "button",
3246
+ className: "p-1.5 rounded text-red-500 hover:bg-red-50 disabled:opacity-50",
3247
+ onClick: onDelete,
3248
+ disabled: loading,
3249
+ "aria-label": "Delete",
3250
+ title: "Delete",
3251
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiTrash2, { size: 16 })
3252
+ }
3253
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", {}),
3254
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
3255
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3256
+ "button",
3257
+ {
3258
+ type: "button",
3259
+ className: "p-1.5 rounded hover:bg-gray-100 text-gray-500",
3260
+ onClick: onCancel,
3261
+ "aria-label": "Cancel",
3262
+ title: "Cancel",
3263
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiX, { size: 16 })
3264
+ }
3265
+ ),
3266
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3267
+ "button",
3268
+ {
3269
+ type: "button",
3270
+ className: "p-1.5 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50",
3271
+ onClick: handleSubmit,
3272
+ disabled: submitDisabled,
3273
+ "aria-label": existingReport ? "Update" : "Save",
3274
+ title: submitTitle,
3275
+ children: loading ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_fi4.FiCheck, { size: 16 })
3276
+ }
3277
+ )
3278
+ ] })
3279
+ ] })
3280
+ ] })
3281
+ ] });
3282
+ }
3283
+
3284
+ // src/DevNotesDot.tsx
3285
+ var import_react8 = require("react");
3286
+ var import_fi5 = require("react-icons/fi");
3287
+
3288
+ // src/hooks/useBugReportPosition.ts
3289
+ var import_react7 = require("react");
3290
+ var subscribers = /* @__PURE__ */ new Set();
3291
+ var cleanupGlobalListeners = null;
3292
+ var rafId = null;
3293
+ var schedulePositionUpdate = () => {
3294
+ if (typeof window === "undefined") return;
3295
+ if (rafId !== null) return;
3296
+ rafId = window.requestAnimationFrame(() => {
3297
+ rafId = null;
3298
+ subscribers.forEach((subscriber) => {
3299
+ try {
3300
+ subscriber();
3301
+ } catch {
3302
+ }
3303
+ });
3304
+ });
3305
+ };
3306
+ var ensureGlobalListeners = () => {
3307
+ if (cleanupGlobalListeners || typeof window === "undefined" || typeof document === "undefined") {
3308
+ return;
3309
+ }
3310
+ const handleUpdate = () => {
3311
+ schedulePositionUpdate();
3312
+ };
3313
+ const observer = new MutationObserver(handleUpdate);
3314
+ observer.observe(document.body, {
3315
+ childList: true,
3316
+ subtree: true
3317
+ });
3318
+ window.addEventListener("resize", handleUpdate);
3319
+ window.addEventListener("scroll", handleUpdate, true);
3320
+ document.addEventListener("scroll", handleUpdate, true);
3321
+ cleanupGlobalListeners = () => {
3322
+ window.removeEventListener("resize", handleUpdate);
3323
+ window.removeEventListener("scroll", handleUpdate, true);
3324
+ document.removeEventListener("scroll", handleUpdate, true);
3325
+ observer.disconnect();
3326
+ if (rafId !== null) {
3327
+ cancelAnimationFrame(rafId);
3328
+ rafId = null;
3329
+ }
3330
+ cleanupGlobalListeners = null;
3331
+ };
3332
+ };
3333
+ var subscribeToPositionUpdates = (subscriber) => {
3334
+ subscribers.add(subscriber);
3335
+ ensureGlobalListeners();
3336
+ return () => {
3337
+ subscribers.delete(subscriber);
3338
+ if (subscribers.size === 0 && cleanupGlobalListeners) {
3339
+ cleanupGlobalListeners();
3340
+ }
3341
+ };
3342
+ };
3343
+ var useBugReportPosition = (report) => {
3344
+ const calculate = (0, import_react7.useCallback)(() => {
3345
+ if (!report) return null;
3346
+ return resolveBugReportCoordinates(report);
3347
+ }, [report]);
3348
+ const [position, setPosition] = (0, import_react7.useState)(() => calculate());
3349
+ (0, import_react7.useEffect)(() => {
3350
+ setPosition(calculate());
3351
+ }, [calculate]);
3352
+ (0, import_react7.useEffect)(() => {
3353
+ if (!report) return void 0;
3354
+ return subscribeToPositionUpdates(() => {
3355
+ setPosition(calculate());
3356
+ });
3357
+ }, [report, calculate]);
3358
+ return position;
3359
+ };
3360
+
3361
+ // src/DevNotesDot.tsx
3362
+ var import_jsx_runtime6 = require("react/jsx-runtime");
3363
+ var statusConfig = {
3364
+ Open: { color: "red", bgClass: "bg-red-500", bgHoverClass: "bg-red-600", icon: import_fi5.FiAlertCircle },
3365
+ "In Progress": { color: "blue", bgClass: "bg-blue-500", bgHoverClass: "bg-blue-600", icon: import_fi5.FiLoader },
3366
+ "Needs Review": { color: "purple", bgClass: "bg-purple-500", bgHoverClass: "bg-purple-600", icon: import_fi5.FiEye },
3367
+ Resolved: { color: "green", bgClass: "bg-green-500", bgHoverClass: "bg-green-600", icon: import_fi5.FiCheckCircle },
3368
+ Closed: { color: "gray", bgClass: "bg-gray-500", bgHoverClass: "bg-gray-600", icon: import_fi5.FiArchive }
3369
+ };
3370
+ var severityBadgeColors = {
3371
+ Critical: "bg-red-100 text-red-800",
3372
+ High: "bg-orange-100 text-orange-800",
3373
+ Medium: "bg-yellow-100 text-yellow-800",
3374
+ Low: "bg-green-100 text-green-800"
3375
+ };
3376
+ var MIN_DRAG_DISTANCE = 5;
3377
+ var DEFAULT_DOT_Z_INDEX = 9998;
3378
+ var resolveAttachedElementZIndex = (selector) => {
3379
+ if (!selector || typeof document === "undefined" || typeof window === "undefined") return null;
3380
+ const targetElement = document.querySelector(selector);
3381
+ if (!targetElement) return null;
3382
+ let currentElement = targetElement;
3383
+ while (currentElement && currentElement !== document.body && currentElement !== document.documentElement) {
3384
+ const computed = window.getComputedStyle(currentElement).zIndex;
3385
+ if (computed !== "auto") {
3386
+ const parsed = Number.parseInt(computed, 10);
3387
+ if (!Number.isNaN(parsed)) {
3388
+ return parsed;
3389
+ }
3390
+ }
3391
+ currentElement = currentElement.parentElement;
3392
+ }
3393
+ return 0;
3394
+ };
3395
+ function DevNotesDot({ report }) {
3396
+ const { deleteBugReport, bugReportTypes, updateBugReport, compensate } = useDevNotes();
3397
+ const [isFormOpen, setIsFormOpen] = (0, import_react8.useState)(false);
3398
+ const [isDragging, setIsDragging] = (0, import_react8.useState)(false);
3399
+ const [dragPosition, setDragPosition] = (0, import_react8.useState)(null);
3400
+ const [pendingMove, setPendingMove] = (0, import_react8.useState)(null);
3401
+ const [showTooltip, setShowTooltip] = (0, import_react8.useState)(false);
3402
+ const dragStartRef = (0, import_react8.useRef)(null);
3403
+ const didDragRef = (0, import_react8.useRef)(false);
3404
+ const dotRef = (0, import_react8.useRef)(null);
3405
+ const handleDelete = async () => {
3406
+ const success = await deleteBugReport(report.id);
3407
+ if (success) {
3408
+ setIsFormOpen(false);
3409
+ }
3410
+ };
3411
+ const getTypeNames = (0, import_react8.useCallback)(() => {
3412
+ return report.types.map((typeId) => {
3413
+ const type = bugReportTypes.find((t) => t.id === typeId);
3414
+ return type?.name || "Unknown";
3415
+ }).join(", ");
3416
+ }, [report.types, bugReportTypes]);
3417
+ const persistPosition = (0, import_react8.useCallback)(
3418
+ async (clientX, clientY) => {
3419
+ const payload = calculateBugPositionFromPoint({
3420
+ clientX,
3421
+ clientY,
3422
+ elementsToIgnore: [dotRef.current]
3423
+ });
3424
+ await updateBugReport(report.id, {
3425
+ x_position: payload.x,
3426
+ y_position: payload.y,
3427
+ target_selector: payload.targetSelector,
3428
+ target_relative_x: payload.targetRelativeX,
3429
+ target_relative_y: payload.targetRelativeY,
3430
+ page_url: normalizePageUrl(`${window.location.pathname}${window.location.search}`)
3431
+ });
3432
+ },
3433
+ [report.id, updateBugReport]
3434
+ );
3435
+ const anchoredPosition = useBugReportPosition(report);
3436
+ const resolvedPosition = anchoredPosition ?? resolveBugReportCoordinates(report);
3437
+ const handleDragStart = (0, import_react8.useCallback)(
3438
+ (event) => {
3439
+ if (!event.shiftKey) return;
3440
+ event.preventDefault();
3441
+ event.stopPropagation();
3442
+ didDragRef.current = false;
3443
+ const currentPosition = dragPosition || resolvedPosition;
3444
+ if (!currentPosition) return;
3445
+ dragStartRef.current = {
3446
+ x: event.clientX,
3447
+ y: event.clientY,
3448
+ dotX: currentPosition.x,
3449
+ dotY: currentPosition.y,
3450
+ hasMoved: false
3451
+ };
3452
+ setIsDragging(true);
3453
+ setDragPosition(currentPosition);
3454
+ },
3455
+ [dragPosition, resolvedPosition]
3456
+ );
3457
+ const handleDragMove = (0, import_react8.useCallback)(
3458
+ (event) => {
3459
+ if (!isDragging || !dragStartRef.current) return;
3460
+ const deltaX = event.clientX - dragStartRef.current.x;
3461
+ const deltaY = event.clientY - dragStartRef.current.y;
3462
+ if (!dragStartRef.current.hasMoved && Math.sqrt(deltaX * deltaX + deltaY * deltaY) > MIN_DRAG_DISTANCE) {
3463
+ dragStartRef.current.hasMoved = true;
3464
+ }
3465
+ setDragPosition({
3466
+ x: dragStartRef.current.dotX + deltaX,
3467
+ y: dragStartRef.current.dotY + deltaY
3468
+ });
3469
+ },
3470
+ [isDragging]
3471
+ );
3472
+ const handleDragEnd = (0, import_react8.useCallback)(
3473
+ (event) => {
3474
+ if (!isDragging || !dragStartRef.current) return;
3475
+ const hasMoved = dragStartRef.current.hasMoved;
3476
+ didDragRef.current = hasMoved;
3477
+ setIsDragging(false);
3478
+ dragStartRef.current = null;
3479
+ if (hasMoved && event) {
3480
+ setPendingMove({ clientX: event.clientX, clientY: event.clientY });
3481
+ } else {
3482
+ setDragPosition(null);
3483
+ }
3484
+ },
3485
+ [isDragging]
3486
+ );
3487
+ const confirmMove = (0, import_react8.useCallback)(async () => {
3488
+ if (!pendingMove) return;
3489
+ await persistPosition(pendingMove.clientX, pendingMove.clientY);
3490
+ setPendingMove(null);
3491
+ setDragPosition(null);
3492
+ }, [pendingMove, persistPosition]);
3493
+ const cancelMove = (0, import_react8.useCallback)(() => {
3494
+ setPendingMove(null);
3495
+ setDragPosition(null);
3496
+ }, []);
3497
+ (0, import_react8.useEffect)(() => {
3498
+ if (!isDragging) return void 0;
3499
+ window.addEventListener("mousemove", handleDragMove);
3500
+ window.addEventListener("mouseup", handleDragEnd);
3501
+ return () => {
3502
+ window.removeEventListener("mousemove", handleDragMove);
3503
+ window.removeEventListener("mouseup", handleDragEnd);
3504
+ };
3505
+ }, [isDragging, handleDragMove, handleDragEnd]);
3506
+ if (!resolvedPosition && !dragPosition) {
3507
+ return null;
3508
+ }
3509
+ const displayPosition = dragPosition || resolvedPosition;
3510
+ const attachedElementZIndex = resolveAttachedElementZIndex(report.target_selector);
3511
+ const dotZIndex = attachedElementZIndex !== null ? attachedElementZIndex + 1 : DEFAULT_DOT_Z_INDEX;
3512
+ const config = statusConfig[report.status] || statusConfig.Open;
3513
+ const StatusIcon = config.icon;
3514
+ const handleKeyDown = (event) => {
3515
+ if (event.key === "Enter" || event.key === " ") {
3516
+ event.preventDefault();
3517
+ setIsFormOpen(true);
3518
+ }
3519
+ };
3520
+ const creatorName = report.creator?.full_name || report.creator?.email || report.created_by || "Unknown";
3521
+ const descriptionPreview = report.description?.trim() || "No description provided.";
3522
+ const createdLabel = new Date(report.created_at).toLocaleString();
3523
+ const needsApproval = !report.approved && !report.ai_ready;
3524
+ const compensated = compensate(displayPosition.x, displayPosition.y);
3525
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3526
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3527
+ "div",
3528
+ {
3529
+ className: "group",
3530
+ style: {
3531
+ position: "absolute",
3532
+ left: `${compensated.x}px`,
3533
+ top: `${compensated.y}px`,
3534
+ transform: "translate(-50%, -50%)",
3535
+ zIndex: isDragging ? dotZIndex + 1 : dotZIndex
3536
+ },
3537
+ onMouseEnter: () => !isFormOpen && !isDragging && setShowTooltip(true),
3538
+ onMouseLeave: () => setShowTooltip(false),
3539
+ children: [
3540
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3541
+ "div",
3542
+ {
3543
+ ref: dotRef,
3544
+ className: `${isDragging ? `${config.bgHoverClass} w-8 h-8` : `${config.bgClass} w-6 h-6`} rounded-full border-[3px] border-white flex items-center justify-center transition-all duration-150 ${isDragging ? "shadow-[0_4px_16px_rgba(0,0,0,0.4)] cursor-grabbing" : "shadow-[0_2px_8px_rgba(0,0,0,0.3)] cursor-pointer hover:scale-[1.2] hover:shadow-[0_4px_12px_rgba(0,0,0,0.4)]"}`,
3545
+ onMouseDown: handleDragStart,
3546
+ onClick: () => {
3547
+ if (didDragRef.current) {
3548
+ didDragRef.current = false;
3549
+ return;
3550
+ }
3551
+ if (pendingMove) return;
3552
+ setIsFormOpen(true);
3553
+ },
3554
+ onKeyDown: handleKeyDown,
3555
+ tabIndex: 0,
3556
+ role: "button",
3557
+ children: [
3558
+ isDragging ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fi5.FiMove, { color: "white", size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatusIcon, { color: "white", size: 12 }),
3559
+ needsApproval && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "absolute -top-2 -right-2 text-base font-bold text-orange-500 pointer-events-none leading-none", children: "*" })
3560
+ ]
3561
+ }
3562
+ ),
3563
+ showTooltip && !pendingMove && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-[280px] bg-gray-800 text-white rounded-lg p-3 shadow-xl z-[2147483647] pointer-events-none", children: [
3564
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "font-bold text-sm mb-1", children: report.title }),
3565
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2 mb-1 flex-wrap", children: [
3566
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `text-xs px-1.5 py-0.5 rounded ${severityBadgeColors[report.severity] || "bg-gray-100 text-gray-800"}`, children: report.severity }),
3567
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: `text-xs px-1.5 py-0.5 rounded flex items-center gap-1 ${config.color === "red" ? "bg-red-100 text-red-800" : config.color === "blue" ? "bg-blue-100 text-blue-800" : config.color === "purple" ? "bg-purple-100 text-purple-800" : config.color === "green" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}`, children: [
3568
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatusIcon, { size: 10 }),
3569
+ report.status
3570
+ ] }),
3571
+ report.approved && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-800", children: "Approved" }),
3572
+ report.ai_ready && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-800", children: "AI Ready" })
3573
+ ] }),
3574
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs text-gray-300", children: getTypeNames() }),
3575
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs text-gray-200 mt-2", children: descriptionPreview }),
3576
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-xs text-gray-300 mt-2", children: [
3577
+ "Created by ",
3578
+ creatorName,
3579
+ " on ",
3580
+ createdLabel
3581
+ ] }),
3582
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs text-gray-400 mt-1", children: "Click to view/edit \xB7 Hold Shift + drag to reposition" }),
3583
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-800" })
3584
+ ] }),
3585
+ pendingMove && !isDragging && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "absolute top-full left-1/2 -translate-x-1/2 mt-2 flex gap-1.5", style: { pointerEvents: "auto" }, children: [
3586
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3587
+ "button",
3588
+ {
3589
+ className: "w-8 h-8 rounded-full bg-green-500 hover:bg-green-600 flex items-center justify-center shadow-lg border-2 border-white cursor-pointer transition-colors",
3590
+ onClick: (e) => {
3591
+ e.stopPropagation();
3592
+ confirmMove();
3593
+ },
3594
+ title: "Confirm new position",
3595
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fi5.FiCheck, { color: "white", size: 14 })
3596
+ }
3597
+ ),
3598
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3599
+ "button",
3600
+ {
3601
+ className: "w-8 h-8 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center shadow-lg border-2 border-white cursor-pointer transition-colors",
3602
+ onClick: (e) => {
3603
+ e.stopPropagation();
3604
+ cancelMove();
3605
+ },
3606
+ title: "Cancel move",
3607
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fi5.FiX, { color: "white", size: 14 })
3608
+ }
3609
+ )
3610
+ ] })
3611
+ ]
3612
+ }
3613
+ ),
3614
+ isFormOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3615
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3616
+ "div",
3617
+ {
3618
+ style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.3)", zIndex: 9998, pointerEvents: "auto" },
3619
+ onClick: () => setIsFormOpen(false)
3620
+ }
3621
+ ),
3622
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { position: "absolute", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", padding: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "pointer-events-auto max-h-[calc(100vh-32px)] overflow-y-auto rounded-lg shadow-xl", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3623
+ DevNotesForm,
3624
+ {
3625
+ pageUrl: report.page_url,
3626
+ xPosition: report.x_position,
3627
+ yPosition: report.y_position,
3628
+ targetSelector: report.target_selector ?? null,
3629
+ targetRelativeX: report.target_relative_x ?? null,
3630
+ targetRelativeY: report.target_relative_y ?? null,
3631
+ existingReport: report,
3632
+ onSave: () => setIsFormOpen(false),
3633
+ onCancel: () => setIsFormOpen(false),
3634
+ onDelete: handleDelete
3635
+ }
3636
+ ) }) })
3637
+ ] })
3638
+ ] });
3639
+ }
3640
+
3641
+ // src/DevNotesOverlay.tsx
3642
+ var import_jsx_runtime7 = require("react/jsx-runtime");
3643
+ function DevNotesOverlay({
3644
+ openReportId,
3645
+ onOpenReportClose
3646
+ } = {}) {
3647
+ const {
3648
+ isEnabled,
3649
+ setIsEnabled,
3650
+ showBugsAlways,
3651
+ hideResolvedClosed,
3652
+ bugReports,
3653
+ currentPageBugReports,
3654
+ deleteBugReport,
3655
+ dotContainer,
3656
+ compensate,
3657
+ role
3658
+ } = useDevNotes();
3659
+ const [pendingDot, setPendingDot] = (0, import_react9.useState)(null);
3660
+ const [showPendingForm, setShowPendingForm] = (0, import_react9.useState)(false);
3661
+ const [openedReport, setOpenedReport] = (0, import_react9.useState)(null);
3662
+ const pendingDotRef = (0, import_react9.useRef)(null);
3663
+ const [isDragging, setIsDragging] = (0, import_react9.useState)(false);
3664
+ const dragStartRef = (0, import_react9.useRef)(null);
3665
+ const didDragRef = (0, import_react9.useRef)(false);
3666
+ const justEnabledRef = (0, import_react9.useRef)(false);
3667
+ (0, import_react9.useEffect)(() => {
3668
+ if (isEnabled) {
3669
+ justEnabledRef.current = true;
3670
+ const timer = setTimeout(() => {
3671
+ justEnabledRef.current = false;
3672
+ }, 300);
3673
+ return () => clearTimeout(timer);
3674
+ }
3675
+ justEnabledRef.current = false;
3676
+ return void 0;
3677
+ }, [isEnabled]);
3678
+ (0, import_react9.useEffect)(() => {
3679
+ if (openReportId) {
3680
+ const report = bugReports.find((r) => r.id === openReportId);
3681
+ if (report) {
3682
+ setOpenedReport(report);
3683
+ }
3684
+ }
3685
+ }, [openReportId, bugReports]);
3686
+ (0, import_react9.useEffect)(() => {
3687
+ const handleKeyDown = (e) => {
3688
+ if (e.key === "Escape") {
3689
+ if (showPendingForm) {
3690
+ setShowPendingForm(false);
3691
+ } else if (pendingDot) {
3692
+ setPendingDot(null);
3693
+ } else if (isEnabled) {
3694
+ setIsEnabled(false);
3695
+ }
3696
+ }
3697
+ };
3698
+ window.addEventListener("keydown", handleKeyDown);
3699
+ return () => window.removeEventListener("keydown", handleKeyDown);
3700
+ }, [isEnabled, setIsEnabled, pendingDot, showPendingForm]);
3701
+ (0, import_react9.useEffect)(() => {
3702
+ if (isEnabled && !showPendingForm) {
3703
+ document.body.style.cursor = "crosshair";
3704
+ return () => {
3705
+ document.body.style.cursor = "";
3706
+ };
3707
+ }
3708
+ return void 0;
3709
+ }, [isEnabled, showPendingForm]);
3710
+ const handleCloseOpenedReport = (0, import_react9.useCallback)(() => {
3711
+ setOpenedReport(null);
3712
+ onOpenReportClose?.();
3713
+ }, [onOpenReportClose]);
3714
+ const handleDeleteOpenedReport = (0, import_react9.useCallback)(async () => {
3715
+ if (openedReport) {
3716
+ await deleteBugReport(openedReport.id);
3717
+ setOpenedReport(null);
3718
+ onOpenReportClose?.();
3719
+ }
3720
+ }, [openedReport, deleteBugReport, onOpenReportClose]);
3721
+ (0, import_react9.useEffect)(() => {
3722
+ if (!isEnabled || showPendingForm) return void 0;
3723
+ const handleDocumentClick = (e) => {
3724
+ if (justEnabledRef.current) return;
3725
+ const target = e.target;
3726
+ if (target.closest("[data-bug-form]") || target.closest("[data-bug-dot]") || target.closest("[data-bug-menu]") || target.closest("[data-pending-dot]")) {
3727
+ return;
3728
+ }
3729
+ const position = calculateBugPositionFromPoint({
3730
+ clientX: e.clientX,
3731
+ clientY: e.clientY
3732
+ });
3733
+ setPendingDot(position);
3734
+ };
3735
+ document.addEventListener("click", handleDocumentClick);
3736
+ return () => document.removeEventListener("click", handleDocumentClick);
3737
+ }, [isEnabled, showPendingForm]);
3738
+ const handleSave = (0, import_react9.useCallback)((_report) => {
3739
+ setPendingDot(null);
3740
+ setShowPendingForm(false);
3741
+ }, []);
3742
+ const handleCancel = (0, import_react9.useCallback)(() => {
3743
+ setPendingDot(null);
3744
+ setShowPendingForm(false);
3745
+ }, []);
3746
+ const handlePendingDotClick = (0, import_react9.useCallback)((e) => {
3747
+ e.stopPropagation();
3748
+ if (didDragRef.current) {
3749
+ didDragRef.current = false;
3750
+ return;
3751
+ }
3752
+ setIsDragging(false);
3753
+ setShowPendingForm(true);
3754
+ }, []);
3755
+ const handleDragStart = (0, import_react9.useCallback)(
3756
+ (e) => {
3757
+ e.preventDefault();
3758
+ e.stopPropagation();
3759
+ if (!pendingDot) return;
3760
+ didDragRef.current = false;
3761
+ setIsDragging(true);
3762
+ dragStartRef.current = {
3763
+ x: e.clientX,
3764
+ y: e.clientY,
3765
+ dotX: pendingDot.x,
3766
+ dotY: pendingDot.y
3767
+ };
3768
+ },
3769
+ [pendingDot]
3770
+ );
3771
+ const handleDragMove = (0, import_react9.useCallback)(
3772
+ (e) => {
3773
+ if (!isDragging || !dragStartRef.current || !pendingDot) return;
3774
+ const deltaX = e.clientX - dragStartRef.current.x;
3775
+ const deltaY = e.clientY - dragStartRef.current.y;
3776
+ if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
3777
+ didDragRef.current = true;
3778
+ }
3779
+ setPendingDot(
3780
+ (prev) => prev ? {
3781
+ ...prev,
3782
+ x: dragStartRef.current.dotX + deltaX,
3783
+ y: dragStartRef.current.dotY + deltaY
3784
+ } : prev
3785
+ );
3786
+ },
3787
+ [isDragging, pendingDot]
3788
+ );
3789
+ const handleDragEnd = (0, import_react9.useCallback)((event) => {
3790
+ setIsDragging(false);
3791
+ dragStartRef.current = null;
3792
+ if (event && didDragRef.current) {
3793
+ const position = calculateBugPositionFromPoint({
3794
+ clientX: event.clientX,
3795
+ clientY: event.clientY,
3796
+ elementsToIgnore: [pendingDotRef.current]
3797
+ });
3798
+ setPendingDot(position);
3799
+ }
3800
+ }, []);
3801
+ (0, import_react9.useEffect)(() => {
3802
+ if (isDragging) {
3803
+ window.addEventListener("mousemove", handleDragMove);
3804
+ window.addEventListener("mouseup", handleDragEnd);
3805
+ return () => {
3806
+ window.removeEventListener("mousemove", handleDragMove);
3807
+ window.removeEventListener("mouseup", handleDragEnd);
3808
+ };
3809
+ }
3810
+ return void 0;
3811
+ }, [isDragging, handleDragMove, handleDragEnd]);
3812
+ const visiblePageReports = (0, import_react9.useMemo)(
3813
+ () => currentPageBugReports.filter((report) => {
3814
+ if (hideResolvedClosed) {
3815
+ return report.status !== "Closed" && report.status !== "Resolved";
3816
+ }
3817
+ return true;
3818
+ }),
3819
+ [currentPageBugReports, hideResolvedClosed]
3820
+ );
3821
+ const renderOpenedReportModal = () => {
3822
+ if (!openedReport) return null;
3823
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3824
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3825
+ "div",
3826
+ {
3827
+ style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.3)", zIndex: 9997, pointerEvents: "auto" },
3828
+ onClick: handleCloseOpenedReport
3829
+ }
3830
+ ),
3831
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { position: "absolute", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", padding: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3832
+ "div",
3833
+ {
3834
+ className: "pointer-events-auto max-h-[calc(100vh-32px)] overflow-y-auto rounded-lg shadow-xl",
3835
+ "data-bug-form": true,
3836
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3837
+ DevNotesForm,
3838
+ {
3839
+ pageUrl: openedReport.page_url,
3840
+ xPosition: openedReport.x_position,
3841
+ yPosition: openedReport.y_position,
3842
+ targetSelector: openedReport.target_selector ?? null,
3843
+ targetRelativeX: openedReport.target_relative_x ?? null,
3844
+ targetRelativeY: openedReport.target_relative_y ?? null,
3845
+ existingReport: openedReport,
3846
+ onSave: handleCloseOpenedReport,
3847
+ onCancel: handleCloseOpenedReport,
3848
+ onDelete: handleDeleteOpenedReport
3849
+ }
3850
+ )
3851
+ }
3852
+ ) })
3853
+ ] });
3854
+ };
3855
+ if (role === "none") return null;
3856
+ if (!isEnabled) {
3857
+ if (!showBugsAlways && !openedReport) {
3858
+ return null;
3859
+ }
3860
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3861
+ showBugsAlways && dotContainer && (0, import_react_dom.createPortal)(
3862
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3863
+ visiblePageReports.map((report) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { "data-bug-dot": true, style: { pointerEvents: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DevNotesDot, { report }) }, report.id)),
3864
+ renderOpenedReportModal()
3865
+ ] }),
3866
+ dotContainer
3867
+ ),
3868
+ !dotContainer && renderOpenedReportModal()
3869
+ ] });
3870
+ }
3871
+ if (!dotContainer) return null;
3872
+ const pendingViewport = pendingDot ? compensate(
3873
+ pendingDot.x - (typeof window !== "undefined" ? window.scrollX : 0),
3874
+ pendingDot.y - (typeof window !== "undefined" ? window.scrollY : 0)
3875
+ ) : null;
3876
+ return (0, import_react_dom.createPortal)(
3877
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3878
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3879
+ "div",
3880
+ {
3881
+ style: { position: "absolute", bottom: 16, left: "50%", transform: "translateX(-50%)", zIndex: 9991, pointerEvents: "auto" },
3882
+ className: "bg-red-500 text-white px-4 py-2 rounded-full shadow-lg flex items-center gap-2",
3883
+ children: [
3884
+ pendingDot && !showPendingForm ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fi6.FiMove, {}) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fi6.FiCrosshair, {}),
3885
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-sm font-medium", children: pendingDot && !showPendingForm ? "Click pin to add details, or click elsewhere to reposition" : "Click anywhere to report a bug" })
3886
+ ]
3887
+ }
3888
+ ),
3889
+ visiblePageReports.map((report) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { "data-bug-dot": true, style: { pointerEvents: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DevNotesDot, { report }) }, report.id)),
3890
+ pendingDot && pendingViewport && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3891
+ "div",
3892
+ {
3893
+ "data-pending-dot": true,
3894
+ ref: pendingDotRef,
3895
+ className: `w-8 h-8 rounded-full border-[3px] border-white z-[9998] flex items-center justify-center transition-all duration-150 ${isDragging ? "bg-red-600 shadow-[0_4px_16px_rgba(0,0,0,0.4)] cursor-grabbing" : "bg-red-500 shadow-[0_2px_8px_rgba(0,0,0,0.3)] cursor-grab animate-devnotes-pulse hover:scale-110"}`,
3896
+ style: {
3897
+ position: "absolute",
3898
+ left: `${pendingViewport.x}px`,
3899
+ top: `${pendingViewport.y}px`,
3900
+ transform: "translate(-50%, -50%)",
3901
+ pointerEvents: "auto"
3902
+ },
3903
+ onMouseDown: handleDragStart,
3904
+ onClick: handlePendingDotClick,
3905
+ title: "Drag to reposition, click to add details",
3906
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_fi6.FiMove, { color: "white", size: 14 })
3907
+ }
3908
+ ),
3909
+ pendingDot && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3910
+ showPendingForm && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3911
+ "div",
3912
+ {
3913
+ style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.3)", zIndex: 9997, pointerEvents: "auto" },
3914
+ onClick: handleCancel
3915
+ }
3916
+ ),
3917
+ showPendingForm && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { position: "absolute", inset: 0, zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", padding: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3918
+ "div",
3919
+ {
3920
+ className: "pointer-events-auto max-h-[calc(100vh-32px)] overflow-y-auto rounded-lg shadow-xl",
3921
+ "data-bug-form": true,
3922
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3923
+ DevNotesForm,
3924
+ {
3925
+ pageUrl: `${window.location.pathname}${window.location.search}`,
3926
+ xPosition: pendingDot.x,
3927
+ yPosition: pendingDot.y,
3928
+ targetSelector: pendingDot.targetSelector,
3929
+ targetRelativeX: pendingDot.targetRelativeX,
3930
+ targetRelativeY: pendingDot.targetRelativeY,
3931
+ onSave: handleSave,
3932
+ onCancel: handleCancel
3933
+ }
3934
+ )
3935
+ }
3936
+ ) })
3937
+ ] }),
3938
+ openedReport && !pendingDot && renderOpenedReportModal()
3939
+ ] }),
3940
+ dotContainer
3941
+ );
3942
+ }
3943
+
3944
+ // src/DevNotesTaskList.tsx
3945
+ var import_react10 = require("react");
3946
+ var import_fi7 = require("react-icons/fi");
3947
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3948
+ var STATUS_COLORS = {
3949
+ Open: "bg-red-100 text-red-700",
3950
+ "In Progress": "bg-blue-100 text-blue-700",
3951
+ "Needs Review": "bg-purple-100 text-purple-700",
3952
+ Resolved: "bg-green-100 text-green-700",
3953
+ Closed: "bg-gray-100 text-gray-600"
3954
+ };
3955
+ var SEVERITY_COLORS = {
3956
+ Critical: "bg-red-500 text-white",
3957
+ High: "bg-orange-100 text-orange-700",
3958
+ Medium: "bg-yellow-100 text-yellow-700",
3959
+ Low: "bg-gray-100 text-gray-600"
3960
+ };
3961
+ var STALE_DAYS = 7;
3962
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
3963
+ function DevNotesTaskList({
3964
+ onNavigateToPage,
3965
+ onClose,
3966
+ title = "Dev Notes Tasks"
3967
+ }) {
3968
+ const {
3969
+ bugReports,
3970
+ bugReportTypes,
3971
+ loading,
3972
+ userProfiles,
3973
+ unreadCounts,
3974
+ deleteBugReport
3975
+ } = useDevNotes();
3976
+ const [searchQuery, setSearchQuery] = (0, import_react10.useState)("");
3977
+ const [filterStatus, setFilterStatus] = (0, import_react10.useState)("all");
3978
+ const [filterSeverity, setFilterSeverity] = (0, import_react10.useState)("all");
3979
+ const [showClosed, setShowClosed] = (0, import_react10.useState)(false);
3980
+ const [selectedReport, setSelectedReport] = (0, import_react10.useState)(null);
3981
+ const [sortField, setSortField] = (0, import_react10.useState)("stale");
3982
+ const [sortDir, setSortDir] = (0, import_react10.useState)("desc");
3983
+ const getStaleMeta = (report) => {
3984
+ const updatedTs = new Date(report.updated_at || report.created_at).getTime();
3985
+ if (Number.isNaN(updatedTs)) {
3986
+ return { isStale: false, ageDays: 0 };
3987
+ }
3988
+ const ageDays = Math.max(0, Math.floor((Date.now() - updatedTs) / MS_PER_DAY));
3989
+ const isCompleted = report.status === "Resolved" || report.status === "Closed";
3990
+ return {
3991
+ isStale: !isCompleted && ageDays >= STALE_DAYS,
3992
+ ageDays
3993
+ };
3994
+ };
3995
+ const stats = (0, import_react10.useMemo)(() => ({
3996
+ total: bugReports.length,
3997
+ open: bugReports.filter((r) => r.status === "Open").length,
3998
+ inProgress: bugReports.filter((r) => r.status === "In Progress").length,
3999
+ needsReview: bugReports.filter((r) => r.status === "Needs Review").length,
4000
+ resolved: bugReports.filter((r) => r.status === "Resolved").length,
4001
+ closed: bugReports.filter((r) => r.status === "Closed").length
4002
+ }), [bugReports]);
4003
+ const filteredReports = (0, import_react10.useMemo)(() => {
4004
+ const severityOrder = { Critical: 0, High: 1, Medium: 2, Low: 3 };
4005
+ const statusOrder = { Open: 0, "In Progress": 1, "Needs Review": 2, Resolved: 3, Closed: 4 };
4006
+ return bugReports.filter((r) => {
4007
+ if (!showClosed && r.status === "Closed") return false;
4008
+ if (showClosed && r.status !== "Closed") return false;
4009
+ if (filterStatus !== "all" && r.status !== filterStatus) return false;
4010
+ if (filterSeverity !== "all" && r.severity !== filterSeverity) return false;
4011
+ if (searchQuery.trim()) {
4012
+ const q = searchQuery.toLowerCase();
4013
+ return r.title.toLowerCase().includes(q) || r.description?.toLowerCase().includes(q) || r.page_url.toLowerCase().includes(q);
4014
+ }
4015
+ return true;
4016
+ }).sort((a, b) => {
4017
+ let cmp = 0;
4018
+ if (sortField === "stale") {
4019
+ const staleA = getStaleMeta(a);
4020
+ const staleB = getStaleMeta(b);
4021
+ if (staleA.isStale !== staleB.isStale) {
4022
+ cmp = Number(staleA.isStale) - Number(staleB.isStale);
4023
+ } else {
4024
+ cmp = staleA.ageDays - staleB.ageDays;
4025
+ if (cmp === 0) {
4026
+ cmp = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
4027
+ }
4028
+ }
4029
+ } else if (sortField === "severity") {
4030
+ cmp = (severityOrder[a.severity] ?? 9) - (severityOrder[b.severity] ?? 9);
4031
+ } else if (sortField === "status") {
4032
+ cmp = (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9);
4033
+ } else {
4034
+ cmp = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
4035
+ }
4036
+ return sortDir === "desc" ? -cmp : cmp;
4037
+ });
4038
+ }, [bugReports, searchQuery, filterStatus, filterSeverity, showClosed, sortField, sortDir]);
4039
+ const getProfileName = (id) => {
4040
+ if (!id) return null;
4041
+ const p = userProfiles[id];
4042
+ if (!p) return null;
4043
+ if (p.full_name) {
4044
+ const first = p.full_name.split(/\s+/)[0];
4045
+ return first;
4046
+ }
4047
+ if (p.email) return p.email.split("@")[0];
4048
+ return null;
4049
+ };
4050
+ const getPageLabel = (pageUrl) => {
4051
+ const cleaned = pageUrl.split("?")[0].split("#")[0].replace(/\/+$/, "");
4052
+ const parts = cleaned.split("/").filter(Boolean);
4053
+ if (parts.length === 0) return "Home";
4054
+ return decodeURIComponent(parts[parts.length - 1]).replace(/[-_]/g, " ");
4055
+ };
4056
+ const formatDate = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" });
4057
+ const handleSort = (field) => {
4058
+ if (sortField === field) {
4059
+ setSortDir((d) => d === "asc" ? "desc" : "asc");
4060
+ } else {
4061
+ setSortField(field);
4062
+ setSortDir("desc");
4063
+ }
4064
+ };
4065
+ const SortIcon = ({ field }) => {
4066
+ if (sortField !== field) return null;
4067
+ return sortDir === "desc" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiChevronDown, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiChevronUp, { size: 12 });
4068
+ };
4069
+ if (selectedReport) {
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex flex-col h-full", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4071
+ DevNotesForm,
4072
+ {
4073
+ pageUrl: selectedReport.page_url,
4074
+ existingReport: selectedReport,
4075
+ onSave: () => setSelectedReport(null),
4076
+ onCancel: () => setSelectedReport(null),
4077
+ onDelete: async () => {
4078
+ await deleteBugReport(selectedReport.id);
4079
+ setSelectedReport(null);
4080
+ }
4081
+ }
4082
+ ) });
4083
+ }
4084
+ if (loading && bugReports.length === 0) {
4085
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "w-6 h-6 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" }) });
4086
+ }
4087
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col gap-4", children: [
4088
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center justify-between", children: [
4089
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { className: "text-lg font-semibold text-gray-900", children: title }),
4090
+ onClose && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4091
+ "button",
4092
+ {
4093
+ type: "button",
4094
+ onClick: onClose,
4095
+ className: "p-1 rounded hover:bg-gray-100 text-gray-500",
4096
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiX, { size: 18 })
4097
+ }
4098
+ )
4099
+ ] }),
4100
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "grid grid-cols-3 sm:grid-cols-6 gap-2", children: [
4101
+ ["Open", stats.open, "text-red-600"],
4102
+ ["In Progress", stats.inProgress, "text-blue-600"],
4103
+ ["Review", stats.needsReview, "text-purple-600"],
4104
+ ["Resolved", stats.resolved, "text-green-600"],
4105
+ ["Closed", stats.closed, "text-gray-500"],
4106
+ ["Total", stats.total, "text-gray-700"]
4107
+ ].map(([label, count, color]) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "text-center rounded-lg border border-gray-100 py-2", children: [
4108
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: `text-xl font-bold ${color}`, children: count }),
4109
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "text-[0.65rem] text-gray-500", children: label })
4110
+ ] }, label)) }),
4111
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap gap-2", children: [
4112
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "relative flex-1 min-w-[180px]", children: [
4113
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiSearch, { className: "absolute left-2.5 top-2.5 text-gray-400", size: 14 }),
4114
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4115
+ "input",
4116
+ {
4117
+ type: "text",
4118
+ placeholder: "Search tasks...",
4119
+ value: searchQuery,
4120
+ onChange: (e) => setSearchQuery(e.target.value),
4121
+ className: "w-full pl-8 pr-3 py-2 text-sm border border-gray-200 rounded-md focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
4122
+ }
4123
+ )
4124
+ ] }),
4125
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4126
+ "select",
4127
+ {
4128
+ value: filterStatus,
4129
+ onChange: (e) => setFilterStatus(e.target.value),
4130
+ className: "px-2 py-2 text-sm border border-gray-200 rounded-md bg-white",
4131
+ children: [
4132
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "all", children: "All Statuses" }),
4133
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Open", children: "Open" }),
4134
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "In Progress", children: "In Progress" }),
4135
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Needs Review", children: "Needs Review" }),
4136
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Resolved", children: "Resolved" })
4137
+ ]
4138
+ }
4139
+ ),
4140
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4141
+ "select",
4142
+ {
4143
+ value: filterSeverity,
4144
+ onChange: (e) => setFilterSeverity(e.target.value),
4145
+ className: "px-2 py-2 text-sm border border-gray-200 rounded-md bg-white",
4146
+ children: [
4147
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "all", children: "All Severities" }),
4148
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Critical", children: "Critical" }),
4149
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "High", children: "High" }),
4150
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Medium", children: "Medium" }),
4151
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "Low", children: "Low" })
4152
+ ]
4153
+ }
4154
+ ),
4155
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4156
+ "button",
4157
+ {
4158
+ type: "button",
4159
+ onClick: () => setShowClosed((v) => !v),
4160
+ className: `px-3 py-2 text-sm rounded-md border transition ${showClosed ? "bg-gray-800 text-white border-gray-800" : "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"}`,
4161
+ children: showClosed ? "Show Active" : "Show Closed"
4162
+ }
4163
+ )
4164
+ ] }),
4165
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "text-xs text-gray-500", children: [
4166
+ filteredReports.length,
4167
+ " of ",
4168
+ showClosed ? stats.closed : stats.total - stats.closed,
4169
+ " ",
4170
+ showClosed ? "closed" : "active",
4171
+ " tasks"
4172
+ ] }),
4173
+ filteredReports.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col items-center py-12 text-gray-400", children: [
4174
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiAlertTriangle, { size: 32, className: "mb-3" }),
4175
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-sm", children: bugReports.length === 0 ? "No tasks yet. Use the Dev Notes menu to report issues." : "No tasks match your filters." })
4176
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "border border-gray-200 rounded-lg overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("table", { className: "w-full text-sm", children: [
4177
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { className: "bg-gray-50 text-left text-xs text-gray-500 uppercase tracking-wide", children: [
4178
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { className: "px-3 py-2 font-medium", children: "Title" }),
4179
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4180
+ "th",
4181
+ {
4182
+ className: "px-3 py-2 font-medium cursor-pointer select-none",
4183
+ onClick: () => handleSort("status"),
4184
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
4185
+ "Status ",
4186
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SortIcon, { field: "status" })
4187
+ ] })
4188
+ }
4189
+ ),
4190
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4191
+ "th",
4192
+ {
4193
+ className: "px-3 py-2 font-medium cursor-pointer select-none",
4194
+ onClick: () => handleSort("severity"),
4195
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
4196
+ "Severity ",
4197
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SortIcon, { field: "severity" })
4198
+ ] })
4199
+ }
4200
+ ),
4201
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { className: "px-3 py-2 font-medium hidden md:table-cell", children: "Page" }),
4202
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { className: "px-3 py-2 font-medium hidden lg:table-cell", children: "Assigned" }),
4203
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4204
+ "th",
4205
+ {
4206
+ className: "px-3 py-2 font-medium cursor-pointer select-none hidden md:table-cell",
4207
+ onClick: () => handleSort("stale"),
4208
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
4209
+ "Freshness ",
4210
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SortIcon, { field: "stale" })
4211
+ ] })
4212
+ }
4213
+ ),
4214
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4215
+ "th",
4216
+ {
4217
+ className: "px-3 py-2 font-medium cursor-pointer select-none",
4218
+ onClick: () => handleSort("created_at"),
4219
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
4220
+ "Date ",
4221
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SortIcon, { field: "created_at" })
4222
+ ] })
4223
+ }
4224
+ )
4225
+ ] }) }),
4226
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tbody", { className: "divide-y divide-gray-100", children: filteredReports.map((report) => {
4227
+ const unread = unreadCounts[report.id] || 0;
4228
+ const stale = getStaleMeta(report);
4229
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4230
+ "tr",
4231
+ {
4232
+ className: "hover:bg-gray-50 cursor-pointer transition",
4233
+ onClick: () => setSelectedReport(report),
4234
+ children: [
4235
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("td", { className: "px-3 py-2.5 max-w-[280px]", children: [
4236
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-2", children: [
4237
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-gray-900 truncate", children: report.title }),
4238
+ unread > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "inline-flex min-w-[18px] items-center justify-center rounded-full bg-purple-100 px-1.5 text-[10px] font-bold text-purple-700", children: unread }),
4239
+ stale.isStale && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800", children: [
4240
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiClock, { size: 10 }),
4241
+ "Stale ",
4242
+ stale.ageDays,
4243
+ "d"
4244
+ ] })
4245
+ ] }),
4246
+ report.types.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex gap-1 mt-0.5", children: report.types.slice(0, 2).map((t) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4247
+ "span",
4248
+ {
4249
+ className: "text-[0.6rem] px-1.5 py-0.5 rounded bg-gray-100 text-gray-500",
4250
+ children: t
4251
+ },
4252
+ t
4253
+ )) })
4254
+ ] }),
4255
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4256
+ "span",
4257
+ {
4258
+ className: `inline-block px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_COLORS[report.status] || "bg-gray-100 text-gray-600"}`,
4259
+ children: report.status
4260
+ }
4261
+ ) }),
4262
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4263
+ "span",
4264
+ {
4265
+ className: `inline-block px-2 py-0.5 rounded-full text-xs font-medium ${SEVERITY_COLORS[report.severity] || "bg-gray-100 text-gray-600"}`,
4266
+ children: report.severity
4267
+ }
4268
+ ) }),
4269
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5 hidden md:table-cell", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1 text-xs text-gray-500", children: [
4270
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate max-w-[140px]", children: getPageLabel(report.page_url) }),
4271
+ onNavigateToPage && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4272
+ "button",
4273
+ {
4274
+ type: "button",
4275
+ className: "flex-shrink-0 p-0.5 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-600",
4276
+ onClick: (e) => {
4277
+ e.stopPropagation();
4278
+ onNavigateToPage(report.page_url, report.id);
4279
+ },
4280
+ title: "Go to page",
4281
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiExternalLink, { size: 12 })
4282
+ }
4283
+ )
4284
+ ] }) }),
4285
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5 hidden lg:table-cell text-xs text-gray-500", children: getProfileName(report.assigned_to) || "\u2014" }),
4286
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5 hidden md:table-cell", children: stale.isStale ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800", children: [
4287
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_fi7.FiClock, { size: 11 }),
4288
+ stale.ageDays,
4289
+ "d stale"
4290
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "inline-flex rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700", children: "Fresh" }) }),
4291
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "px-3 py-2.5 text-xs text-gray-500 whitespace-nowrap", children: formatDate(report.created_at) })
4292
+ ]
4293
+ },
4294
+ report.id
4295
+ );
4296
+ }) })
4297
+ ] }) })
4298
+ ] });
4299
+ }
4300
+
4301
+ // src/DevNotesButton.tsx
4302
+ var import_jsx_runtime9 = require("react/jsx-runtime");
4303
+ var positionStyles = {
4304
+ "bottom-right": { position: "absolute", bottom: 16, right: 16 },
4305
+ "bottom-left": { position: "absolute", bottom: 16, left: 16 },
4306
+ "top-right": { position: "absolute", top: 16, right: 16 },
4307
+ "top-left": { position: "absolute", top: 16, left: 16 }
4308
+ };
4309
+ function DevNotesButton({
4310
+ position = "bottom-right",
4311
+ onViewTasks,
4312
+ onSettings,
4313
+ icon,
4314
+ openReportId,
4315
+ onOpenReportClose,
4316
+ onNavigateToPage
4317
+ }) {
4318
+ const { dotContainer, role } = useDevNotes();
4319
+ const [showTaskPanel, setShowTaskPanel] = (0, import_react11.useState)(false);
4320
+ const [taskPanelTitle, setTaskPanelTitle] = (0, import_react11.useState)("Dev Notes Tasks");
4321
+ if (role === "none") return null;
4322
+ const openBuiltInTaskPanel = (title) => {
4323
+ setTaskPanelTitle(title);
4324
+ setShowTaskPanel(true);
4325
+ };
4326
+ const handleViewTasks = onViewTasks || (() => openBuiltInTaskPanel("Dev Notes Tasks"));
4327
+ const handleSettings = onSettings || (() => openBuiltInTaskPanel("Task Settings"));
4328
+ const buttonContent = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4329
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4330
+ "div",
4331
+ {
4332
+ style: { ...positionStyles[position] || positionStyles["bottom-right"], zIndex: 9990, pointerEvents: "auto" },
4333
+ "data-bug-menu": true,
4334
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4335
+ DevNotesMenu,
4336
+ {
4337
+ onViewTasks: handleViewTasks,
4338
+ onSettings: handleSettings,
4339
+ icon,
4340
+ position,
4341
+ dropdownDirection: position?.includes("bottom") ? "up" : "down"
4342
+ }
4343
+ )
4344
+ }
4345
+ ),
4346
+ showTaskPanel && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { position: "absolute", inset: 0, zIndex: 9998, display: "flex", justifyContent: "flex-end", pointerEvents: "auto" }, children: [
4347
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4348
+ "div",
4349
+ {
4350
+ style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.3)" },
4351
+ onClick: () => setShowTaskPanel(false)
4352
+ }
4353
+ ),
4354
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "relative w-full max-w-2xl bg-white shadow-2xl overflow-y-auto p-6 animate-[slideIn_0.2s_ease-out]", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4355
+ DevNotesTaskList,
4356
+ {
4357
+ title: taskPanelTitle,
4358
+ onClose: () => setShowTaskPanel(false),
4359
+ onNavigateToPage
4360
+ }
4361
+ ) })
4362
+ ] })
4363
+ ] });
4364
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
4365
+ dotContainer ? (0, import_react_dom2.createPortal)(buttonContent, dotContainer) : buttonContent,
4366
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4367
+ DevNotesOverlay,
4368
+ {
4369
+ openReportId,
4370
+ onOpenReportClose
4371
+ }
4372
+ )
4373
+ ] });
4374
+ }
4375
+
4376
+ // src/client.ts
4377
+ var DEFAULT_BASE_PATH = "/api/devnotes";
4378
+ var normalizeBasePath = (basePath) => {
4379
+ const trimmed = (basePath || DEFAULT_BASE_PATH).trim();
4380
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
4381
+ };
4382
+ var buildUrl = (basePath, path, query) => {
4383
+ const url = new URL(`${basePath}${path.startsWith("/") ? path : `/${path}`}`, "http://localhost");
4384
+ Object.entries(query || {}).forEach(([key, value]) => {
4385
+ if (value === void 0) return;
4386
+ url.searchParams.set(key, String(value));
4387
+ });
4388
+ return `${url.pathname}${url.search}`;
4389
+ };
4390
+ var defaultCapabilities = {
4391
+ ai: false,
4392
+ appLink: true
4393
+ };
4394
+ async function parseResponse(response) {
4395
+ const text = await response.text();
4396
+ const payload = text ? JSON.parse(text) : null;
4397
+ if (!response.ok) {
4398
+ const message = payload?.error?.message || payload?.error || payload?.message || `Request failed with status ${response.status}`;
4399
+ throw new Error(message);
4400
+ }
4401
+ return payload;
4402
+ }
4403
+ function createDevNotesClient(options) {
4404
+ const basePath = normalizeBasePath(options.basePath);
4405
+ const fetchImpl = options.fetch ?? globalThis.fetch;
4406
+ if (typeof fetchImpl !== "function") {
4407
+ throw new Error("createDevNotesClient requires a fetch implementation.");
4408
+ }
4409
+ const request = async (path, init = {}) => {
4410
+ const token = await options.getAuthToken();
4411
+ const headers = new Headers(init.headers || {});
4412
+ headers.set("Authorization", `Bearer ${token}`);
4413
+ if (!headers.has("Content-Type") && init.body) {
4414
+ headers.set("Content-Type", "application/json");
4415
+ }
4416
+ const response = await fetchImpl(buildUrl(basePath, path, init.query), {
4417
+ ...init,
4418
+ headers
4419
+ });
4420
+ return await parseResponse(response);
4421
+ };
4422
+ return {
4423
+ fetchBugReports: async () => await request("/reports"),
4424
+ createBugReport: async (data) => await request("/reports", {
4425
+ method: "POST",
4426
+ body: JSON.stringify(data)
4427
+ }),
4428
+ updateBugReport: async (id, data) => await request(`/reports/${encodeURIComponent(id)}`, {
4429
+ method: "PATCH",
4430
+ body: JSON.stringify(data)
4431
+ }),
4432
+ deleteBugReport: async (id) => {
4433
+ await request(`/reports/${encodeURIComponent(id)}`, { method: "DELETE" });
4434
+ },
4435
+ fetchBugReportTypes: async () => await request("/report-types"),
4436
+ createBugReportType: async (name) => await request("/report-types", {
4437
+ method: "POST",
4438
+ body: JSON.stringify({ name })
4439
+ }),
4440
+ deleteBugReportType: async (id) => {
4441
+ await request(`/report-types/${encodeURIComponent(id)}`, { method: "DELETE" });
4442
+ },
4443
+ fetchTaskLists: async () => await request("/task-lists"),
4444
+ createTaskList: async (name) => await request("/task-lists", {
4445
+ method: "POST",
4446
+ body: JSON.stringify({ name })
4447
+ }),
4448
+ fetchMessages: async (bugReportId) => await request(`/reports/${encodeURIComponent(bugReportId)}/messages`),
4449
+ createMessage: async (bugReportId, body) => await request(`/reports/${encodeURIComponent(bugReportId)}/messages`, {
4450
+ method: "POST",
4451
+ body: JSON.stringify({ body })
4452
+ }),
4453
+ updateMessage: async (id, body) => await request(`/messages/${encodeURIComponent(id)}`, {
4454
+ method: "PATCH",
4455
+ body: JSON.stringify({ body })
4456
+ }),
4457
+ deleteMessage: async (id) => {
4458
+ await request(`/messages/${encodeURIComponent(id)}`, { method: "DELETE" });
4459
+ },
4460
+ markMessagesAsRead: async (messageIds) => {
4461
+ await request("/messages/read", {
4462
+ method: "POST",
4463
+ body: JSON.stringify({ messageIds })
4464
+ });
4465
+ },
4466
+ fetchUnreadCounts: async () => await request("/unread-counts"),
4467
+ fetchCollaborators: async (ids) => await request("/collaborators", {
4468
+ query: ids && ids.length ? { ids: ids.join(",") } : void 0
4469
+ }),
4470
+ fetchProfiles: async (ids) => {
4471
+ if (ids.length === 0) return [];
4472
+ return await request("/collaborators", {
4473
+ query: { ids: ids.join(",") }
4474
+ });
4475
+ },
4476
+ fetchCapabilities: async () => {
4477
+ try {
4478
+ return await request("/capabilities");
4479
+ } catch {
4480
+ return defaultCapabilities;
4481
+ }
4482
+ },
4483
+ getAppLinkStatus: async () => await request("/app-link", { method: "GET" }),
4484
+ linkApp: async (input) => await request("/app-link", {
4485
+ method: "POST",
4486
+ body: JSON.stringify(input)
4487
+ }),
4488
+ unlinkApp: async () => {
4489
+ await request("/app-link", { method: "DELETE" });
4490
+ }
4491
+ };
4492
+ }
4493
+ // Annotate the CommonJS export names for ESM import in node:
4494
+ 0 && (module.exports = {
4495
+ DevNotesButton,
4496
+ DevNotesDiscussion,
4497
+ DevNotesDot,
4498
+ DevNotesForm,
4499
+ DevNotesMenu,
4500
+ DevNotesOverlay,
4501
+ DevNotesProvider,
4502
+ DevNotesTaskList,
4503
+ buildAiFixPayload,
4504
+ buildCaptureContext,
4505
+ calculateBugPositionFromPoint,
4506
+ createDevNotesClient,
4507
+ deriveRouteLabelFromUrl,
4508
+ detectBrowserName,
4509
+ formatAiFixPayloadForCopy,
4510
+ normalizePageUrl,
4511
+ resolveBugReportCoordinates,
4512
+ useBugReportPosition,
4513
+ useDevNotes
4514
+ });