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,304 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import type { RecentFile } from '../hooks/useRecentFiles';
3
+ import { getApiErrorMessage, readJsonResponse, type ApiErrorPayload } from '../lib/http';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ onOpenFile: (path: string) => void;
9
+ recentFiles: RecentFile[];
10
+ activeFilePath: string | null;
11
+ onClearRecent: () => void;
12
+ }
13
+
14
+ type PickFileResponse = {
15
+ path?: string;
16
+ } & ApiErrorPayload;
17
+
18
+ function looksLikeDirectPath(value: string): boolean {
19
+ return (
20
+ value.startsWith('/') ||
21
+ value.startsWith('~/') ||
22
+ value.startsWith('~\\') ||
23
+ value.startsWith('./') ||
24
+ value.startsWith('.\\') ||
25
+ value.startsWith('../') ||
26
+ value.startsWith('..\\') ||
27
+ /^[a-zA-Z]:[\\/]/.test(value) ||
28
+ value.startsWith('\\\\')
29
+ );
30
+ }
31
+
32
+ export function FileOpener({
33
+ open,
34
+ onClose,
35
+ onOpenFile,
36
+ recentFiles,
37
+ activeFilePath,
38
+ onClearRecent,
39
+ }: Props) {
40
+ const [query, setQuery] = useState('');
41
+ const [selectedIndex, setSelectedIndex] = useState(0);
42
+ const [isKeyboardNav, setIsKeyboardNav] = useState(false);
43
+ const inputRef = useRef<HTMLInputElement>(null);
44
+ const listRef = useRef<HTMLDivElement>(null);
45
+
46
+ const filtered = query
47
+ ? recentFiles.filter(
48
+ (f) =>
49
+ f.name.toLowerCase().includes(query.toLowerCase()) ||
50
+ f.path.toLowerCase().includes(query.toLowerCase()),
51
+ )
52
+ : recentFiles;
53
+
54
+ // Items: "System file picker..." + recent files
55
+ const SYSTEM_INDEX = 0;
56
+ const itemCount = filtered.length + 1;
57
+
58
+ useEffect(() => {
59
+ if (open) {
60
+ setQuery('');
61
+ setSelectedIndex(0);
62
+ requestAnimationFrame(() => inputRef.current?.focus());
63
+ }
64
+ }, [open]);
65
+
66
+ useEffect(() => {
67
+ if (selectedIndex >= itemCount) {
68
+ setSelectedIndex(Math.max(0, itemCount - 1));
69
+ }
70
+ }, [itemCount, selectedIndex]);
71
+
72
+ useEffect(() => {
73
+ if (!listRef.current) return;
74
+ const selected = listRef.current.querySelector('[data-selected="true"]');
75
+ selected?.scrollIntoView({ block: 'nearest' });
76
+ }, [selectedIndex]);
77
+
78
+ const handleOpen = useCallback(
79
+ (path: string) => {
80
+ onOpenFile(path);
81
+ onClose();
82
+ },
83
+ [onOpenFile, onClose],
84
+ );
85
+
86
+ const handleSystemPicker = useCallback(async () => {
87
+ try {
88
+ const res = await fetch('/api/pick-file');
89
+ const data = await readJsonResponse<PickFileResponse>(res);
90
+ if (!res.ok || !data) {
91
+ throw new Error(getApiErrorMessage(res, data, 'Failed to open system file picker'));
92
+ }
93
+ if (data.path) {
94
+ handleOpen(data.path);
95
+ }
96
+ } catch {
97
+ // Cancelled or failed
98
+ }
99
+ }, [handleOpen]);
100
+
101
+ const handleSelect = useCallback(
102
+ (index: number) => {
103
+ if (index === SYSTEM_INDEX) {
104
+ handleSystemPicker();
105
+ } else if (index > SYSTEM_INDEX && index <= filtered.length) {
106
+ handleOpen(filtered[index - 1].path);
107
+ }
108
+ },
109
+ [filtered, SYSTEM_INDEX, handleOpen, handleSystemPicker],
110
+ );
111
+
112
+ const handleKeyDown = (e: React.KeyboardEvent) => {
113
+ if (e.key === 'ArrowDown' || (e.key === 'j' && e.ctrlKey)) {
114
+ e.preventDefault();
115
+ setIsKeyboardNav(true);
116
+ setSelectedIndex((i) => (i + 1) % itemCount);
117
+ } else if (e.key === 'ArrowUp' || (e.key === 'k' && e.ctrlKey)) {
118
+ e.preventDefault();
119
+ setIsKeyboardNav(true);
120
+ setSelectedIndex((i) => (i - 1 + itemCount) % itemCount);
121
+ } else if (e.key === 'Enter') {
122
+ e.preventDefault();
123
+ if (query.trim() && looksLikeDirectPath(query.trim())) {
124
+ // Looks like a path — open it directly
125
+ handleOpen(query.trim());
126
+ } else {
127
+ handleSelect(selectedIndex);
128
+ }
129
+ } else if (e.key === 'Escape') {
130
+ e.preventDefault();
131
+ onClose();
132
+ }
133
+ };
134
+
135
+ if (!open) return null;
136
+
137
+ return (
138
+ <div
139
+ className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]"
140
+ onClick={onClose}
141
+ >
142
+ <div className="absolute inset-0 bg-black/40" />
143
+
144
+ <div
145
+ className="relative w-full max-w-lg bg-surface-raised rounded-xl shadow-2xl border border-border overflow-hidden"
146
+ onClick={(e) => e.stopPropagation()}
147
+ >
148
+ {/* Input */}
149
+ <div className="flex items-center px-4 border-b border-border">
150
+ <svg
151
+ className="w-4 h-4 text-content-muted shrink-0"
152
+ fill="none"
153
+ viewBox="0 0 24 24"
154
+ stroke="currentColor"
155
+ strokeWidth={2}
156
+ >
157
+ <path
158
+ strokeLinecap="round"
159
+ strokeLinejoin="round"
160
+ d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
161
+ />
162
+ </svg>
163
+ <input
164
+ ref={inputRef}
165
+ value={query}
166
+ onChange={(e) => {
167
+ setQuery(e.target.value);
168
+ setSelectedIndex(0);
169
+ }}
170
+ onKeyDown={handleKeyDown}
171
+ placeholder="File path or name..."
172
+ className="flex-1 px-3 py-3 text-sm bg-transparent text-content focus:outline-none placeholder:text-content-muted"
173
+ />
174
+ <kbd className="text-[10px] px-1.5 py-0.5 rounded border border-border-subtle text-content-muted bg-surface">
175
+ esc
176
+ </kbd>
177
+ </div>
178
+
179
+ {/* List */}
180
+ <div ref={listRef} className="max-h-80 overflow-y-auto py-1">
181
+ {/* Actions */}
182
+ <div className={filtered.length > 0 ? 'border-b border-border-subtle mb-1' : ''}>
183
+ <button
184
+ data-selected={selectedIndex === SYSTEM_INDEX}
185
+ onClick={handleSystemPicker}
186
+ onMouseMove={() => {
187
+ if (isKeyboardNav) setIsKeyboardNav(false);
188
+ else setSelectedIndex(SYSTEM_INDEX);
189
+ }}
190
+ className={`w-full text-left px-4 py-2.5 flex items-center gap-3 text-sm transition-colors ${
191
+ selectedIndex === SYSTEM_INDEX
192
+ ? 'bg-primary-bg text-primary-text'
193
+ : 'text-content hover:bg-tint'
194
+ }`}
195
+ >
196
+ <svg
197
+ className={`w-4 h-4 shrink-0 ${selectedIndex === SYSTEM_INDEX ? 'text-primary-text' : 'text-content-muted'}`}
198
+ fill="none"
199
+ viewBox="0 0 24 24"
200
+ stroke="currentColor"
201
+ strokeWidth={2}
202
+ >
203
+ <path
204
+ strokeLinecap="round"
205
+ strokeLinejoin="round"
206
+ d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"
207
+ />
208
+ </svg>
209
+ System file picker...
210
+ </button>
211
+ </div>
212
+
213
+ {/* Recent files */}
214
+ {filtered.length > 0 && (
215
+ <>
216
+ <div className="px-4 pt-2 pb-1 flex items-center justify-between">
217
+ <span className="text-[10px] font-semibold text-content-muted uppercase tracking-wider">
218
+ Recent
219
+ </span>
220
+ <button
221
+ onClick={(e) => {
222
+ e.stopPropagation();
223
+ onClearRecent();
224
+ }}
225
+ className="text-[10px] text-content-muted hover:text-danger transition-colors"
226
+ >
227
+ Clear
228
+ </button>
229
+ </div>
230
+ {filtered.map((file, i) => {
231
+ const itemIndex = i + 1;
232
+ const isSelected = itemIndex === selectedIndex;
233
+ const isActive = file.path === activeFilePath;
234
+ return (
235
+ <button
236
+ key={file.path}
237
+ data-selected={isSelected}
238
+ onClick={() => handleOpen(file.path)}
239
+ onMouseMove={() => {
240
+ if (isKeyboardNav) setIsKeyboardNav(false);
241
+ else setSelectedIndex(itemIndex);
242
+ }}
243
+ className={`w-full text-left px-4 py-2 flex items-center gap-3 transition-colors ${
244
+ isSelected ? 'bg-primary-bg text-primary-text' : 'text-content hover:bg-tint'
245
+ }`}
246
+ >
247
+ <svg
248
+ className={`w-4 h-4 shrink-0 ${isSelected ? 'text-primary-text' : 'text-content-muted'}`}
249
+ fill="none"
250
+ viewBox="0 0 24 24"
251
+ stroke="currentColor"
252
+ strokeWidth={2}
253
+ >
254
+ <path
255
+ strokeLinecap="round"
256
+ strokeLinejoin="round"
257
+ d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
258
+ />
259
+ </svg>
260
+ <div className="flex-1 min-w-0">
261
+ <div className="text-sm font-medium truncate">
262
+ {file.name}
263
+ {isActive && (
264
+ <span
265
+ className={`ml-2 text-[10px] font-normal ${isSelected ? 'text-primary-text/70' : 'text-content-muted'}`}
266
+ >
267
+ active
268
+ </span>
269
+ )}
270
+ </div>
271
+ <div
272
+ className={`text-xs truncate ${isSelected ? 'text-primary-text/60' : 'text-content-muted'}`}
273
+ >
274
+ {file.path}
275
+ </div>
276
+ </div>
277
+ </button>
278
+ );
279
+ })}
280
+ </>
281
+ )}
282
+
283
+ {/* Empty state when filtering yields no results */}
284
+ {filtered.length === 0 && query && !looksLikeDirectPath(query) && (
285
+ <div className="px-4 py-6 text-center text-sm text-content-muted">
286
+ No matching files
287
+ </div>
288
+ )}
289
+
290
+ {/* Path hint when typing a path */}
291
+ {query && looksLikeDirectPath(query) && (
292
+ <div className="px-4 py-3 text-xs text-content-muted border-b border-border-subtle">
293
+ Press{' '}
294
+ <kbd className="px-1 py-0.5 rounded border border-border-subtle bg-surface text-[10px]">
295
+ Enter
296
+ </kbd>{' '}
297
+ to open <span className="text-content font-medium">{query}</span>
298
+ </div>
299
+ )}
300
+ </div>
301
+ </div>
302
+ </div>
303
+ );
304
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode, ButtonHTMLAttributes } from 'react';
2
+ import { variantClasses, type Variant } from './iconButtonVariants';
3
+
4
+ interface Props extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
5
+ /** Visual variant: neutral (default muted), active (primary toggle), success (green toggle) */
6
+ variant?: Variant;
7
+ /** Whether the toggle is currently on — only relevant for 'active' and 'success' variants */
8
+ active?: boolean;
9
+ /** Icon size class — defaults to 'w-3.5 h-3.5' (tab bar), use 'w-4 h-4' for toolbar */
10
+ size?: 'sm' | 'md';
11
+ children: ReactNode;
12
+ }
13
+
14
+ const baseClasses =
15
+ 'p-1 rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary';
16
+
17
+ export function IconButton({
18
+ variant = 'neutral',
19
+ active = false,
20
+ size = 'sm',
21
+ children,
22
+ ...rest
23
+ }: Props) {
24
+ const v = variantClasses[variant];
25
+ const stateClass = active ? v.on : v.off;
26
+
27
+ return (
28
+ <button className={`${baseClasses} ${stateClass}`} {...rest}>
29
+ <span className={size === 'sm' ? 'block w-3.5 h-3.5' : 'block w-4 h-4'}>{children}</span>
30
+ </button>
31
+ );
32
+ }
@@ -0,0 +1,136 @@
1
+ import { useEffect } from 'react';
2
+ import { isApplePlatform } from '../lib/platform';
3
+ import { StyledShortcut } from '../lib/shortcut-label';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ resolveEnabled: boolean;
9
+ }
10
+
11
+ const mod = isApplePlatform() ? '\u2318' : 'Ctrl+';
12
+ const shift = isApplePlatform() ? '\u21e7' : 'Shift+';
13
+
14
+ interface Shortcut {
15
+ keys: string;
16
+ label: string;
17
+ condition?: string;
18
+ }
19
+
20
+ interface Section {
21
+ title: string;
22
+ shortcuts: Shortcut[];
23
+ }
24
+
25
+ const sections: Section[] = [
26
+ {
27
+ title: 'Navigation',
28
+ shortcuts: [
29
+ { keys: 'N / J', label: 'Next comment' },
30
+ { keys: 'P / K', label: 'Previous comment' },
31
+ { keys: `${mod}F`, label: 'Find in document' },
32
+ { keys: `${mod}K`, label: 'Command palette' },
33
+ { keys: `${mod}${shift}[`, label: 'Previous tab' },
34
+ { keys: `${mod}${shift}]`, label: 'Next tab' },
35
+ ],
36
+ },
37
+ {
38
+ title: 'Comments',
39
+ shortcuts: [
40
+ { keys: `${mod}Enter`, label: 'Start commenting on selection' },
41
+ { keys: `${mod}${shift}M`, label: 'Lock selection for commenting' },
42
+ { keys: 'D', label: 'Delete active comment' },
43
+ { keys: 'A / X', label: 'Resolve active comment', condition: 'resolve' },
44
+ { keys: 'U', label: 'Reopen active comment', condition: 'resolve' },
45
+ ],
46
+ },
47
+ {
48
+ title: 'View',
49
+ shortcuts: [
50
+ { keys: `${mod}\\`, label: 'Toggle comment sidebar' },
51
+ { keys: `${mod}B`, label: 'Toggle file explorer' },
52
+ { keys: `${mod}${shift}O`, label: 'Toggle document outline' },
53
+ ],
54
+ },
55
+ {
56
+ title: 'File',
57
+ shortcuts: [
58
+ { keys: `${mod}O`, label: 'Open file' },
59
+ { keys: `${mod},`, label: 'Open settings' },
60
+ { keys: '?', label: 'This help panel' },
61
+ ],
62
+ },
63
+ ];
64
+
65
+ export function KeyboardShortcutsPanel({ open, onClose, resolveEnabled }: Props) {
66
+ useEffect(() => {
67
+ if (!open) return;
68
+ const handler = (e: KeyboardEvent) => {
69
+ if (e.key === 'Escape') {
70
+ e.preventDefault();
71
+ onClose();
72
+ }
73
+ };
74
+ document.addEventListener('keydown', handler);
75
+ return () => document.removeEventListener('keydown', handler);
76
+ }, [open, onClose]);
77
+
78
+ if (!open) return null;
79
+
80
+ return (
81
+ <div className="fixed inset-0 z-[100] flex items-center justify-center" onClick={onClose}>
82
+ <div className="absolute inset-0 bg-black/40" />
83
+ <div
84
+ className="relative w-full max-w-lg bg-surface-raised rounded-xl shadow-2xl border border-border overflow-hidden"
85
+ onClick={(e) => e.stopPropagation()}
86
+ >
87
+ {/* Header */}
88
+ <div className="flex items-center justify-between px-5 py-3 border-b border-border">
89
+ <h2 className="text-sm font-semibold text-content">Keyboard Shortcuts</h2>
90
+ <button
91
+ onClick={onClose}
92
+ className="text-content-muted hover:text-content transition-colors"
93
+ >
94
+ <svg
95
+ className="w-4 h-4"
96
+ fill="none"
97
+ viewBox="0 0 24 24"
98
+ stroke="currentColor"
99
+ strokeWidth={2}
100
+ >
101
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
102
+ </svg>
103
+ </button>
104
+ </div>
105
+
106
+ {/* Body */}
107
+ <div className="px-5 py-4 max-h-[70vh] overflow-y-auto space-y-5">
108
+ {sections.map((section) => {
109
+ const visibleShortcuts = section.shortcuts.filter(
110
+ (s) => !s.condition || (s.condition === 'resolve' && resolveEnabled),
111
+ );
112
+ if (visibleShortcuts.length === 0) return null;
113
+
114
+ return (
115
+ <div key={section.title}>
116
+ <h3 className="text-[10px] font-semibold text-content-muted uppercase tracking-wider mb-2">
117
+ {section.title}
118
+ </h3>
119
+ <div className="space-y-1">
120
+ {visibleShortcuts.map((shortcut) => (
121
+ <div key={shortcut.keys} className="flex items-center justify-between py-1.5">
122
+ <span className="text-sm text-content">{shortcut.label}</span>
123
+ <kbd className="text-[11px] px-2 py-0.5 rounded border border-border-subtle text-content-muted bg-surface font-mono min-w-[2rem] text-center">
124
+ <StyledShortcut text={shortcut.keys} />
125
+ </kbd>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ );
131
+ })}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }