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