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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/md-redline +255 -0
- package/bin/test-windows.ps1 +70 -0
- package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
- package/dist/assets/arc-DI2g9LXK.js +1 -0
- package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
- package/dist/assets/array-DOVTz2Mq.js +1 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
- package/dist/assets/channel-DPCihw7y.js +1 -0
- package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
- package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
- package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
- package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
- package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
- package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
- package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
- package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
- package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
- package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
- package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
- package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
- package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
- package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
- package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
- package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
- package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
- package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
- package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
- package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
- package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
- package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
- package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
- package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
- package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
- package/dist/assets/clone-CI9aUwHe.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
- package/dist/assets/dagre-CilMRazv.js +1 -0
- package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
- package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
- package/dist/assets/dist-C_eddq6m.js +1 -0
- package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
- package/dist/assets/flatten-B8XIuT0x.js +1 -0
- package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
- package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
- package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
- package/dist/assets/index-BoggyWS0.css +2 -0
- package/dist/assets/index-aLvjHQW4.js +104 -0
- package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
- package/dist/assets/init-Bft5Ffpj.js +1 -0
- package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
- package/dist/assets/katex-Uj9wLT16.js +265 -0
- package/dist/assets/line-CRxEwpOv.js +1 -0
- package/dist/assets/linear-PDPfFByd.js +1 -0
- package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
- package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
- package/dist/assets/ordinal-DIg8h6NI.js +1 -0
- package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
- package/dist/assets/path-DfRbCp9y.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
- package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
- package/dist/assets/reduce-CV2X8n1a.js +1 -0
- package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
- package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
- package/dist/assets/src-PKe5NtkK.js +1 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
- package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/screenshot.png +0 -0
- package/index.html +13 -0
- package/package.json +105 -0
- package/public/favicon.svg +15 -0
- package/public/screenshot.png +0 -0
- package/server/index.test.ts +814 -0
- package/server/index.ts +736 -0
- package/server/preferences.test.ts +126 -0
- package/server/preferences.ts +76 -0
- package/src/App.tsx +1620 -0
- package/src/components/ActionButton.tsx +41 -0
- package/src/components/CommandPalette.tsx +191 -0
- package/src/components/CommentCard.tsx +556 -0
- package/src/components/CommentForm.tsx +285 -0
- package/src/components/CommentSidebar.tsx +428 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/ContextMenu.tsx +220 -0
- package/src/components/DragHandles.tsx +48 -0
- package/src/components/FileExplorer.tsx +251 -0
- package/src/components/FileOpener.tsx +304 -0
- package/src/components/IconButton.tsx +32 -0
- package/src/components/KeyboardShortcutsPanel.tsx +136 -0
- package/src/components/MarkdownViewer.tsx +682 -0
- package/src/components/RawView.tsx +798 -0
- package/src/components/SearchBar.tsx +129 -0
- package/src/components/Separator.tsx +7 -0
- package/src/components/SettingsPanel.tsx +813 -0
- package/src/components/SplitIconButton.tsx +133 -0
- package/src/components/TabBar.tsx +594 -0
- package/src/components/TableOfContents.tsx +70 -0
- package/src/components/ThemeSelector.tsx +159 -0
- package/src/components/Toast.tsx +99 -0
- package/src/components/Toolbar.tsx +161 -0
- package/src/components/iconButtonVariants.ts +19 -0
- package/src/components/rawView.test.ts +291 -0
- package/src/contexts/SettingsContext.tsx +120 -0
- package/src/hooks/useAuthor.test.ts +58 -0
- package/src/hooks/useAuthor.ts +69 -0
- package/src/hooks/useAutoResize.ts +20 -0
- package/src/hooks/useCommentCardTriggers.ts +20 -0
- package/src/hooks/useComments.test.ts +773 -0
- package/src/hooks/useComments.ts +332 -0
- package/src/hooks/useContextMenu.ts +48 -0
- package/src/hooks/useContextMenuItems.ts +392 -0
- package/src/hooks/useDiffSnapshot.test.ts +130 -0
- package/src/hooks/useDiffSnapshot.ts +67 -0
- package/src/hooks/useDragHandles.ts +417 -0
- package/src/hooks/useFileWatcher.ts +45 -0
- package/src/hooks/useHeadingTracking.ts +84 -0
- package/src/hooks/useMermaidRenderer.ts +75 -0
- package/src/hooks/useModalState.ts +22 -0
- package/src/hooks/usePageVisible.test.ts +69 -0
- package/src/hooks/usePageVisible.ts +19 -0
- package/src/hooks/usePaneLayout.test.ts +108 -0
- package/src/hooks/usePaneLayout.ts +102 -0
- package/src/hooks/useRecentFiles.test.ts +103 -0
- package/src/hooks/useRecentFiles.ts +99 -0
- package/src/hooks/useResizablePanel.test.ts +84 -0
- package/src/hooks/useResizablePanel.ts +118 -0
- package/src/hooks/useSearch.test.ts +72 -0
- package/src/hooks/useSearch.ts +53 -0
- package/src/hooks/useSelection.ts +48 -0
- package/src/hooks/useSessionPersistence.test.ts +59 -0
- package/src/hooks/useSessionPersistence.ts +43 -0
- package/src/hooks/useTabs.test.ts +127 -0
- package/src/hooks/useTabs.ts +561 -0
- package/src/hooks/useThemePersistence.ts +41 -0
- package/src/hooks/useToast.ts +27 -0
- package/src/index.css +1047 -0
- package/src/lib/agent-prompts.test.ts +34 -0
- package/src/lib/agent-prompts.ts +68 -0
- package/src/lib/comment-editor-state.ts +6 -0
- package/src/lib/comment-parser.test.ts +1959 -0
- package/src/lib/comment-parser.ts +1021 -0
- package/src/lib/diff.test.ts +164 -0
- package/src/lib/diff.ts +139 -0
- package/src/lib/heading-slugs.test.ts +85 -0
- package/src/lib/heading-slugs.ts +44 -0
- package/src/lib/http.test.ts +43 -0
- package/src/lib/http.ts +29 -0
- package/src/lib/mermaid-highlights.test.ts +517 -0
- package/src/lib/mermaid-highlights.ts +936 -0
- package/src/lib/mermaid-renderer.test.ts +114 -0
- package/src/lib/mermaid-renderer.ts +89 -0
- package/src/lib/path-utils.test.ts +17 -0
- package/src/lib/path-utils.ts +7 -0
- package/src/lib/platform.test.ts +58 -0
- package/src/lib/platform.ts +14 -0
- package/src/lib/preferences-client.test.ts +177 -0
- package/src/lib/preferences-client.ts +94 -0
- package/src/lib/selection-resolver.test.ts +118 -0
- package/src/lib/selection-resolver.ts +37 -0
- package/src/lib/settings.test.ts +152 -0
- package/src/lib/settings.ts +78 -0
- package/src/lib/shortcut-label.tsx +18 -0
- package/src/lib/themes.ts +21 -0
- package/src/lib/visible-text.test.ts +86 -0
- package/src/lib/visible-text.ts +77 -0
- package/src/main.tsx +22 -0
- package/src/markdown/pipeline.test.ts +82 -0
- package/src/markdown/pipeline.ts +33 -0
- package/src/types.test.ts +43 -0
- package/src/types.ts +46 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- 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
|
+
}
|