md-redline 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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/md-redline +255 -0
  4. package/bin/test-windows.ps1 +70 -0
  5. package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
  6. package/dist/assets/arc-DI2g9LXK.js +1 -0
  7. package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
  8. package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
  9. package/dist/assets/array-DOVTz2Mq.js +1 -0
  10. package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
  11. package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
  12. package/dist/assets/channel-DPCihw7y.js +1 -0
  13. package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
  14. package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
  15. package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
  16. package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
  17. package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
  18. package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
  19. package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
  20. package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
  21. package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
  22. package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
  23. package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
  24. package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
  25. package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
  26. package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
  27. package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
  28. package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
  29. package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
  30. package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
  31. package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
  32. package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
  33. package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
  34. package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
  35. package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
  36. package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
  37. package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
  38. package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
  39. package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
  40. package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
  41. package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
  42. package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
  43. package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
  44. package/dist/assets/clone-CI9aUwHe.js +1 -0
  45. package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
  46. package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
  47. package/dist/assets/dagre-CilMRazv.js +1 -0
  48. package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
  49. package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
  50. package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
  51. package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
  52. package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
  53. package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
  54. package/dist/assets/dist-C_eddq6m.js +1 -0
  55. package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
  56. package/dist/assets/flatten-B8XIuT0x.js +1 -0
  57. package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
  58. package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
  59. package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
  60. package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
  61. package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
  62. package/dist/assets/index-BoggyWS0.css +2 -0
  63. package/dist/assets/index-aLvjHQW4.js +104 -0
  64. package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
  65. package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
  66. package/dist/assets/init-Bft5Ffpj.js +1 -0
  67. package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
  68. package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
  69. package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
  70. package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
  71. package/dist/assets/katex-Uj9wLT16.js +265 -0
  72. package/dist/assets/line-CRxEwpOv.js +1 -0
  73. package/dist/assets/linear-PDPfFByd.js +1 -0
  74. package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
  75. package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
  76. package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
  77. package/dist/assets/ordinal-DIg8h6NI.js +1 -0
  78. package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
  79. package/dist/assets/path-DfRbCp9y.js +1 -0
  80. package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
  81. package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
  82. package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
  83. package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
  84. package/dist/assets/reduce-CV2X8n1a.js +1 -0
  85. package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
  86. package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
  87. package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
  88. package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
  89. package/dist/assets/src-PKe5NtkK.js +1 -0
  90. package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
  91. package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
  92. package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
  93. package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
  94. package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
  95. package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
  96. package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
  97. package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
  98. package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
  99. package/dist/favicon.svg +15 -0
  100. package/dist/index.html +14 -0
  101. package/dist/screenshot.png +0 -0
  102. package/index.html +13 -0
  103. package/package.json +105 -0
  104. package/public/favicon.svg +15 -0
  105. package/public/screenshot.png +0 -0
  106. package/server/index.test.ts +814 -0
  107. package/server/index.ts +736 -0
  108. package/server/preferences.test.ts +126 -0
  109. package/server/preferences.ts +76 -0
  110. package/src/App.tsx +1620 -0
  111. package/src/components/ActionButton.tsx +41 -0
  112. package/src/components/CommandPalette.tsx +191 -0
  113. package/src/components/CommentCard.tsx +556 -0
  114. package/src/components/CommentForm.tsx +285 -0
  115. package/src/components/CommentSidebar.tsx +428 -0
  116. package/src/components/ConfirmDialog.tsx +64 -0
  117. package/src/components/ContextMenu.tsx +220 -0
  118. package/src/components/DragHandles.tsx +48 -0
  119. package/src/components/FileExplorer.tsx +251 -0
  120. package/src/components/FileOpener.tsx +304 -0
  121. package/src/components/IconButton.tsx +32 -0
  122. package/src/components/KeyboardShortcutsPanel.tsx +136 -0
  123. package/src/components/MarkdownViewer.tsx +682 -0
  124. package/src/components/RawView.tsx +798 -0
  125. package/src/components/SearchBar.tsx +129 -0
  126. package/src/components/Separator.tsx +7 -0
  127. package/src/components/SettingsPanel.tsx +813 -0
  128. package/src/components/SplitIconButton.tsx +133 -0
  129. package/src/components/TabBar.tsx +594 -0
  130. package/src/components/TableOfContents.tsx +70 -0
  131. package/src/components/ThemeSelector.tsx +159 -0
  132. package/src/components/Toast.tsx +99 -0
  133. package/src/components/Toolbar.tsx +161 -0
  134. package/src/components/iconButtonVariants.ts +19 -0
  135. package/src/components/rawView.test.ts +291 -0
  136. package/src/contexts/SettingsContext.tsx +120 -0
  137. package/src/hooks/useAuthor.test.ts +58 -0
  138. package/src/hooks/useAuthor.ts +69 -0
  139. package/src/hooks/useAutoResize.ts +20 -0
  140. package/src/hooks/useCommentCardTriggers.ts +20 -0
  141. package/src/hooks/useComments.test.ts +773 -0
  142. package/src/hooks/useComments.ts +332 -0
  143. package/src/hooks/useContextMenu.ts +48 -0
  144. package/src/hooks/useContextMenuItems.ts +392 -0
  145. package/src/hooks/useDiffSnapshot.test.ts +130 -0
  146. package/src/hooks/useDiffSnapshot.ts +67 -0
  147. package/src/hooks/useDragHandles.ts +417 -0
  148. package/src/hooks/useFileWatcher.ts +45 -0
  149. package/src/hooks/useHeadingTracking.ts +84 -0
  150. package/src/hooks/useMermaidRenderer.ts +75 -0
  151. package/src/hooks/useModalState.ts +22 -0
  152. package/src/hooks/usePageVisible.test.ts +69 -0
  153. package/src/hooks/usePageVisible.ts +19 -0
  154. package/src/hooks/usePaneLayout.test.ts +108 -0
  155. package/src/hooks/usePaneLayout.ts +102 -0
  156. package/src/hooks/useRecentFiles.test.ts +103 -0
  157. package/src/hooks/useRecentFiles.ts +99 -0
  158. package/src/hooks/useResizablePanel.test.ts +84 -0
  159. package/src/hooks/useResizablePanel.ts +118 -0
  160. package/src/hooks/useSearch.test.ts +72 -0
  161. package/src/hooks/useSearch.ts +53 -0
  162. package/src/hooks/useSelection.ts +48 -0
  163. package/src/hooks/useSessionPersistence.test.ts +59 -0
  164. package/src/hooks/useSessionPersistence.ts +43 -0
  165. package/src/hooks/useTabs.test.ts +127 -0
  166. package/src/hooks/useTabs.ts +561 -0
  167. package/src/hooks/useThemePersistence.ts +41 -0
  168. package/src/hooks/useToast.ts +27 -0
  169. package/src/index.css +1047 -0
  170. package/src/lib/agent-prompts.test.ts +34 -0
  171. package/src/lib/agent-prompts.ts +68 -0
  172. package/src/lib/comment-editor-state.ts +6 -0
  173. package/src/lib/comment-parser.test.ts +1959 -0
  174. package/src/lib/comment-parser.ts +1021 -0
  175. package/src/lib/diff.test.ts +164 -0
  176. package/src/lib/diff.ts +139 -0
  177. package/src/lib/heading-slugs.test.ts +85 -0
  178. package/src/lib/heading-slugs.ts +44 -0
  179. package/src/lib/http.test.ts +43 -0
  180. package/src/lib/http.ts +29 -0
  181. package/src/lib/mermaid-highlights.test.ts +517 -0
  182. package/src/lib/mermaid-highlights.ts +936 -0
  183. package/src/lib/mermaid-renderer.test.ts +114 -0
  184. package/src/lib/mermaid-renderer.ts +89 -0
  185. package/src/lib/path-utils.test.ts +17 -0
  186. package/src/lib/path-utils.ts +7 -0
  187. package/src/lib/platform.test.ts +58 -0
  188. package/src/lib/platform.ts +14 -0
  189. package/src/lib/preferences-client.test.ts +177 -0
  190. package/src/lib/preferences-client.ts +94 -0
  191. package/src/lib/selection-resolver.test.ts +118 -0
  192. package/src/lib/selection-resolver.ts +37 -0
  193. package/src/lib/settings.test.ts +152 -0
  194. package/src/lib/settings.ts +78 -0
  195. package/src/lib/shortcut-label.tsx +18 -0
  196. package/src/lib/themes.ts +21 -0
  197. package/src/lib/visible-text.test.ts +86 -0
  198. package/src/lib/visible-text.ts +77 -0
  199. package/src/main.tsx +22 -0
  200. package/src/markdown/pipeline.test.ts +82 -0
  201. package/src/markdown/pipeline.ts +33 -0
  202. package/src/types.test.ts +43 -0
  203. package/src/types.ts +46 -0
  204. package/tsconfig.app.json +28 -0
  205. package/tsconfig.json +7 -0
  206. package/tsconfig.node.json +26 -0
  207. package/vite.config.ts +50 -0
@@ -0,0 +1,428 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import type { MdComment } from '../types';
3
+ import { getEffectiveStatus } from '../types';
4
+ import type { SidebarCommentEditorState } from '../lib/comment-editor-state';
5
+ import { CommentCard } from './CommentCard';
6
+ import { useSettings } from '../contexts/SettingsContext';
7
+ import { ActionButton } from './ActionButton';
8
+ import { ConfirmDialog } from './ConfirmDialog';
9
+
10
+ export interface SidebarContextMenuInfo {
11
+ commentId: string;
12
+ x: number;
13
+ y: number;
14
+ }
15
+
16
+ export interface SidebarCommentFocusRequest {
17
+ commentId: string;
18
+ token: number;
19
+ }
20
+
21
+ interface Props {
22
+ comments: MdComment[];
23
+ activeCommentId: string | null;
24
+ missingAnchors: Set<string>;
25
+ onActivate: (id: string) => void;
26
+ onResolve?: (id: string) => void;
27
+ onUnresolve?: (id: string) => void;
28
+ onDelete: (id: string) => void;
29
+ onEdit: (id: string, newText: string) => void;
30
+ onReply: (id: string, text: string) => void;
31
+ onEditReply: (commentId: string, replyId: string, newText: string) => void;
32
+ onDeleteReply: (commentId: string, replyId: string) => void;
33
+ onBulkDelete: () => void;
34
+ onBulkResolve?: () => void;
35
+ onBulkDeleteResolved?: () => void;
36
+ onContextMenu?: (info: SidebarContextMenuInfo) => void;
37
+ requestedEditor?: SidebarCommentEditorState;
38
+ requestedFocus?: SidebarCommentFocusRequest | null;
39
+ onFocusHandled?: () => void;
40
+ }
41
+
42
+ type FilterMode = 'all' | 'open' | 'resolved';
43
+
44
+ export function CommentSidebar({
45
+ comments,
46
+ activeCommentId,
47
+ missingAnchors,
48
+ onActivate,
49
+ onResolve,
50
+ onUnresolve,
51
+ onDelete,
52
+ onEdit,
53
+ onReply,
54
+ onEditReply,
55
+ onDeleteReply,
56
+ onBulkDelete,
57
+ onBulkResolve,
58
+ onBulkDeleteResolved,
59
+ onContextMenu: onCtxMenu,
60
+ requestedEditor,
61
+ requestedFocus,
62
+ onFocusHandled,
63
+ }: Props) {
64
+ const activeRef = useRef<HTMLDivElement>(null);
65
+ const commentRefs = useRef(new Map<string, HTMLDivElement>());
66
+ const [search, setSearch] = useState('');
67
+ const [filter, setFilter] = useState<FilterMode>('all');
68
+ const [activeEditor, setActiveEditor] = useState<SidebarCommentEditorState>(null);
69
+ const [confirmDeleteAll, setConfirmDeleteAll] = useState(false);
70
+ const { settings } = useSettings();
71
+ const resolveEnabled = settings.enableResolve;
72
+
73
+ // Scroll to active comment
74
+ useEffect(() => {
75
+ if (activeCommentId && activeRef.current) {
76
+ activeRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
77
+ }
78
+ }, [activeCommentId]);
79
+
80
+ useEffect(() => {
81
+ if (!requestedFocus) return;
82
+
83
+ if (search) {
84
+ setSearch('');
85
+ return;
86
+ }
87
+
88
+ if (resolveEnabled && filter === 'resolved') {
89
+ setFilter('open');
90
+ return;
91
+ }
92
+
93
+ const node = commentRefs.current.get(requestedFocus.commentId);
94
+ if (!node) return;
95
+
96
+ node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
97
+ requestAnimationFrame(() => {
98
+ node.focus({ preventScroll: true });
99
+ onFocusHandled?.();
100
+ });
101
+ }, [requestedFocus, resolveEnabled, filter, search, onFocusHandled]);
102
+
103
+ useEffect(() => {
104
+ if (requestedEditor) {
105
+ setActiveEditor(requestedEditor);
106
+ }
107
+ }, [requestedEditor]);
108
+
109
+ useEffect(() => {
110
+ if (!activeEditor) return;
111
+
112
+ const comment = comments.find((candidate) => candidate.id === activeEditor.commentId);
113
+ if (!comment) {
114
+ setActiveEditor(null);
115
+ return;
116
+ }
117
+
118
+ if (
119
+ activeEditor.mode === 'reply-edit' &&
120
+ !comment.replies?.some((reply) => reply.id === activeEditor.replyId)
121
+ ) {
122
+ setActiveEditor(null);
123
+ }
124
+ }, [comments, activeEditor]);
125
+
126
+ const openCommentEdit = (commentId: string) => {
127
+ setActiveEditor({ mode: 'comment-edit', commentId, token: Date.now() });
128
+ };
129
+
130
+ const openReplyCompose = (commentId: string) => {
131
+ setActiveEditor({ mode: 'reply-compose', commentId, token: Date.now() });
132
+ };
133
+
134
+ const openReplyEdit = (commentId: string, replyId: string) => {
135
+ setActiveEditor({ mode: 'reply-edit', commentId, replyId, token: Date.now() });
136
+ };
137
+
138
+ const closeEditor = () => {
139
+ setActiveEditor(null);
140
+ };
141
+
142
+ // Count by status (only meaningful when resolve is enabled)
143
+ const openCount = resolveEnabled
144
+ ? comments.filter((c) => getEffectiveStatus(c) === 'open').length
145
+ : comments.length;
146
+ const resolvedCount = resolveEnabled
147
+ ? comments.filter((c) => getEffectiveStatus(c) === 'resolved').length
148
+ : 0;
149
+
150
+ // Filter and search
151
+ const filtered = comments.filter((c) => {
152
+ // Status filter (only when resolve enabled)
153
+ if (resolveEnabled) {
154
+ const status = getEffectiveStatus(c);
155
+ if (filter === 'open' && status !== 'open') return false;
156
+ if (filter === 'resolved' && status !== 'resolved') return false;
157
+ }
158
+
159
+ // Text search
160
+ if (search) {
161
+ const q = search.toLowerCase();
162
+ const matchesText = c.text.toLowerCase().includes(q);
163
+ const matchesAnchor = c.anchor.toLowerCase().includes(q);
164
+ const matchesAuthor = c.author.toLowerCase().includes(q);
165
+ const matchesReply = c.replies?.some((r) => r.text.toLowerCase().includes(q)) ?? false;
166
+ if (!matchesText && !matchesAnchor && !matchesAuthor && !matchesReply) return false;
167
+ }
168
+
169
+ return true;
170
+ });
171
+
172
+ // Sort: open first, then resolved (only when resolve enabled)
173
+ const activeComments = resolveEnabled
174
+ ? filtered.filter((c) => getEffectiveStatus(c) !== 'resolved')
175
+ : filtered;
176
+ const resolvedComments = resolveEnabled
177
+ ? filtered.filter((c) => getEffectiveStatus(c) === 'resolved')
178
+ : [];
179
+
180
+ if (comments.length === 0) {
181
+ return (
182
+ <div className="flex flex-col items-center justify-center h-full text-content-muted px-6">
183
+ <svg
184
+ className="w-12 h-12 mb-3 text-content-faint"
185
+ fill="none"
186
+ viewBox="0 0 24 24"
187
+ stroke="currentColor"
188
+ strokeWidth={1.5}
189
+ >
190
+ <path
191
+ strokeLinecap="round"
192
+ strokeLinejoin="round"
193
+ d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
194
+ />
195
+ </svg>
196
+ <p className="text-sm font-medium text-content-secondary mb-1">No comments yet</p>
197
+ <p className="text-xs text-center leading-relaxed">
198
+ Select text in the document to add your first comment
199
+ </p>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ const FILTER_TABS: { key: FilterMode; label: string }[] = [
205
+ { key: 'all', label: 'All' },
206
+ { key: 'open', label: 'Open' },
207
+ { key: 'resolved', label: 'Resolved' },
208
+ ];
209
+
210
+ return (
211
+ <div className="flex flex-col h-full">
212
+ {/* Filter tabs (only when resolve enabled) */}
213
+ {resolveEnabled && (
214
+ <div className="px-3 pt-3 pb-1">
215
+ <div className="flex gap-1">
216
+ {FILTER_TABS.map(({ key, label }) => {
217
+ const count =
218
+ key === 'all' ? comments.length : key === 'open' ? openCount : resolvedCount;
219
+ return (
220
+ <button
221
+ key={key}
222
+ onClick={() => setFilter(key)}
223
+ className={`text-[10px] px-2 py-1 rounded-md font-medium transition-colors ${
224
+ filter === key
225
+ ? 'bg-primary-bg-strong text-primary-text'
226
+ : 'text-content-secondary hover:bg-tint'
227
+ }`}
228
+ >
229
+ {label}
230
+ {count > 0 && (
231
+ <span
232
+ className={`ml-1 ${filter === key ? 'text-primary-text' : 'text-content-muted'}`}
233
+ >
234
+ {count}
235
+ </span>
236
+ )}
237
+ </button>
238
+ );
239
+ })}
240
+ </div>
241
+ </div>
242
+ )}
243
+
244
+ {/* Search */}
245
+ <div className={`px-3 ${resolveEnabled ? 'pb-2' : 'pt-3 pb-2'}`}>
246
+ <input
247
+ type="text"
248
+ value={search}
249
+ onChange={(e) => setSearch(e.target.value)}
250
+ placeholder="Search comments..."
251
+ className="w-full text-xs border border-border-subtle rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent placeholder:text-content-muted bg-surface text-content"
252
+ />
253
+ </div>
254
+
255
+ {/* Comment list */}
256
+ <div className="flex-1 overflow-y-auto px-3 pb-3 space-y-2">
257
+ {activeComments.map((comment) => (
258
+ <div
259
+ key={comment.id}
260
+ ref={(node) => {
261
+ if (node) {
262
+ commentRefs.current.set(comment.id, node);
263
+ } else {
264
+ commentRefs.current.delete(comment.id);
265
+ }
266
+ if (comment.id === activeCommentId) {
267
+ activeRef.current = node;
268
+ }
269
+ }}
270
+ tabIndex={-1}
271
+ data-comment-card-id={comment.id}
272
+ >
273
+ <CommentCard
274
+ comment={comment}
275
+ isActive={comment.id === activeCommentId}
276
+ anchorMissing={missingAnchors.has(comment.id)}
277
+ onActivate={onActivate}
278
+ onResolve={resolveEnabled ? onResolve : undefined}
279
+ onUnresolve={resolveEnabled ? onUnresolve : undefined}
280
+ onDelete={onDelete}
281
+ onEdit={onEdit}
282
+ onReply={onReply}
283
+ onEditReply={onEditReply}
284
+ onDeleteReply={onDeleteReply}
285
+ editor={activeEditor?.commentId === comment.id ? activeEditor : null}
286
+ onRequestCommentEdit={openCommentEdit}
287
+ onRequestReplyCompose={openReplyCompose}
288
+ onRequestReplyEdit={openReplyEdit}
289
+ onCloseEditor={closeEditor}
290
+ onContextMenu={
291
+ onCtxMenu ? (id, x, y) => onCtxMenu({ commentId: id, x, y }) : undefined
292
+ }
293
+ />
294
+ </div>
295
+ ))}
296
+
297
+ {resolveEnabled && resolvedComments.length > 0 && filter !== 'resolved' && (
298
+ <div className="flex items-center gap-2 pt-3 pb-1">
299
+ <div className="h-px flex-1 bg-border-subtle" />
300
+ <span className="text-xs text-content-muted font-medium">
301
+ Resolved ({resolvedComments.length})
302
+ </span>
303
+ <div className="h-px flex-1 bg-border-subtle" />
304
+ </div>
305
+ )}
306
+ {resolvedComments.map((comment) => (
307
+ <div
308
+ key={comment.id}
309
+ ref={(node) => {
310
+ if (node) {
311
+ commentRefs.current.set(comment.id, node);
312
+ } else {
313
+ commentRefs.current.delete(comment.id);
314
+ }
315
+ if (comment.id === activeCommentId) {
316
+ activeRef.current = node;
317
+ }
318
+ }}
319
+ tabIndex={-1}
320
+ data-comment-card-id={comment.id}
321
+ >
322
+ <CommentCard
323
+ comment={comment}
324
+ isActive={comment.id === activeCommentId}
325
+ anchorMissing={missingAnchors.has(comment.id)}
326
+ onActivate={onActivate}
327
+ onResolve={resolveEnabled ? onResolve : undefined}
328
+ onUnresolve={resolveEnabled ? onUnresolve : undefined}
329
+ onDelete={onDelete}
330
+ onEdit={onEdit}
331
+ onReply={onReply}
332
+ onEditReply={onEditReply}
333
+ onDeleteReply={onDeleteReply}
334
+ editor={activeEditor?.commentId === comment.id ? activeEditor : null}
335
+ onRequestCommentEdit={openCommentEdit}
336
+ onRequestReplyCompose={openReplyCompose}
337
+ onRequestReplyEdit={openReplyEdit}
338
+ onCloseEditor={closeEditor}
339
+ onContextMenu={
340
+ onCtxMenu ? (id, x, y) => onCtxMenu({ commentId: id, x, y }) : undefined
341
+ }
342
+ />
343
+ </div>
344
+ ))}
345
+
346
+ {filtered.length === 0 && (
347
+ <div className="text-center py-6">
348
+ <p className="text-xs text-content-muted">
349
+ {search ? 'No comments match your search' : 'No comments in this category'}
350
+ </p>
351
+ </div>
352
+ )}
353
+ </div>
354
+
355
+ {/* Footer: summary + bulk actions */}
356
+ <div className="border-t border-border px-4 py-2 bg-surface-secondary flex flex-col gap-1.5">
357
+ <div className="flex items-center justify-between">
358
+ <span className="text-xs text-content-secondary whitespace-nowrap">
359
+ {resolveEnabled ? (
360
+ <>
361
+ {openCount} open {'\u00b7'} {resolvedCount} resolved
362
+ </>
363
+ ) : (
364
+ <>
365
+ {comments.length} comment{comments.length !== 1 ? 's' : ''}
366
+ </>
367
+ )}
368
+ </span>
369
+ {!resolveEnabled && comments.length > 0 && (
370
+ <ActionButton
371
+ intent="danger"
372
+ size="sm"
373
+ onClick={(e) => {
374
+ e.stopPropagation();
375
+ setConfirmDeleteAll(true);
376
+ }}
377
+ title="Delete all comments"
378
+ >
379
+ Delete All
380
+ </ActionButton>
381
+ )}
382
+ </div>
383
+ {resolveEnabled && comments.length > 0 && (
384
+ <div className="flex gap-1.5 justify-end">
385
+ {openCount > 0 && onBulkResolve && (
386
+ <ActionButton
387
+ intent="success"
388
+ size="sm"
389
+ onClick={(e) => {
390
+ e.stopPropagation();
391
+ onBulkResolve();
392
+ }}
393
+ title="Resolve all open comments"
394
+ >
395
+ Resolve All
396
+ </ActionButton>
397
+ )}
398
+ {resolvedCount > 0 && onBulkDeleteResolved && (
399
+ <ActionButton
400
+ intent="danger"
401
+ size="sm"
402
+ onClick={(e) => {
403
+ e.stopPropagation();
404
+ onBulkDeleteResolved();
405
+ }}
406
+ title="Delete all resolved comments"
407
+ >
408
+ Clear Resolved
409
+ </ActionButton>
410
+ )}
411
+ </div>
412
+ )}
413
+ </div>
414
+
415
+ <ConfirmDialog
416
+ open={confirmDeleteAll}
417
+ title="Delete all comments"
418
+ message="This will permanently delete all comments. This cannot be undone."
419
+ confirmLabel="Delete All"
420
+ onConfirm={() => {
421
+ setConfirmDeleteAll(false);
422
+ onBulkDelete();
423
+ }}
424
+ onCancel={() => setConfirmDeleteAll(false)}
425
+ />
426
+ </div>
427
+ );
428
+ }
@@ -0,0 +1,64 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { ActionButton } from './ActionButton';
3
+
4
+ interface Props {
5
+ open: boolean;
6
+ title: string;
7
+ message: string;
8
+ confirmLabel?: string;
9
+ cancelLabel?: string;
10
+ onConfirm: () => void;
11
+ onCancel: () => void;
12
+ }
13
+
14
+ export function ConfirmDialog({
15
+ open,
16
+ title,
17
+ message,
18
+ confirmLabel = 'Delete',
19
+ cancelLabel = 'Cancel',
20
+ onConfirm,
21
+ onCancel,
22
+ }: Props) {
23
+ const confirmRef = useRef<HTMLButtonElement>(null);
24
+
25
+ useEffect(() => {
26
+ if (!open) return;
27
+ confirmRef.current?.focus();
28
+ const handler = (e: KeyboardEvent) => {
29
+ if (e.key === 'Escape') {
30
+ e.preventDefault();
31
+ onCancel();
32
+ }
33
+ };
34
+ document.addEventListener('keydown', handler);
35
+ return () => document.removeEventListener('keydown', handler);
36
+ }, [open, onCancel]);
37
+
38
+ if (!open) return null;
39
+
40
+ return (
41
+ <div className="fixed inset-0 z-[100] flex items-center justify-center" onClick={onCancel}>
42
+ {/* Backdrop */}
43
+ <div className="absolute inset-0 bg-black/40" />
44
+
45
+ {/* Dialog */}
46
+ <div
47
+ className="relative w-full max-w-sm bg-surface-raised rounded-xl shadow-2xl border border-border p-5"
48
+ onClick={(e) => e.stopPropagation()}
49
+ >
50
+ <h3 className="text-sm font-semibold text-content mb-1">{title}</h3>
51
+ <p className="text-xs text-content-secondary mb-5">{message}</p>
52
+
53
+ <div className="flex justify-end gap-2">
54
+ <ActionButton size="sm" intent="neutral" onClick={onCancel}>
55
+ {cancelLabel}
56
+ </ActionButton>
57
+ <ActionButton ref={confirmRef} size="sm" intent="danger" onClick={onConfirm}>
58
+ {confirmLabel}
59
+ </ActionButton>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ }