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,594 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { ViewMode } from './Toolbar';
|
|
3
|
+
import { getPrimaryModifierLabel } from '../lib/platform';
|
|
4
|
+
import { IconButton } from './IconButton';
|
|
5
|
+
import { SplitIconButton } from './SplitIconButton';
|
|
6
|
+
import { getPathBasename } from '../lib/path-utils';
|
|
7
|
+
|
|
8
|
+
interface Tab {
|
|
9
|
+
filePath: string;
|
|
10
|
+
error: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TabContextMenuInfo {
|
|
14
|
+
filePath: string;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
tabs: Tab[];
|
|
21
|
+
activeFilePath: string | null;
|
|
22
|
+
commentCounts: Map<string, number>;
|
|
23
|
+
resolvedCommentCounts?: Map<string, number>;
|
|
24
|
+
onSwitchTab: (path: string) => void;
|
|
25
|
+
onCloseTab: (path: string) => void;
|
|
26
|
+
onOpenFile: () => void;
|
|
27
|
+
onTabContextMenu?: (info: TabContextMenuInfo) => void;
|
|
28
|
+
// Document actions (moved from Toolbar)
|
|
29
|
+
viewMode: ViewMode;
|
|
30
|
+
diffPending?: boolean;
|
|
31
|
+
commentCount: number;
|
|
32
|
+
enableResolve?: boolean;
|
|
33
|
+
onViewModeChange: (mode: ViewMode) => void;
|
|
34
|
+
onSearch: () => void;
|
|
35
|
+
searchActive: boolean;
|
|
36
|
+
onCopyAgentPrompt?: (filePaths: string[]) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tabControlButtonClass =
|
|
40
|
+
'flex h-full w-8 items-center justify-center shrink-0 border-l border-border text-content-muted transition-colors hover:bg-tint hover:text-content-secondary disabled:pointer-events-none disabled:opacity-35';
|
|
41
|
+
|
|
42
|
+
const tabActionButtonClass =
|
|
43
|
+
'sticky right-0 z-10 flex h-full items-center justify-center bg-surface-secondary px-2.5 shrink-0 border-r border-border text-content-muted transition-colors hover:bg-tint hover:text-content';
|
|
44
|
+
|
|
45
|
+
const handOffIcon = (
|
|
46
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
47
|
+
{/* antenna */}
|
|
48
|
+
<line x1="12" y1="1.5" x2="12" y2="4.5" strokeLinecap="round" />
|
|
49
|
+
<circle cx="12" cy="1.5" r="1" fill="currentColor" stroke="none" />
|
|
50
|
+
{/* head */}
|
|
51
|
+
<rect x="3" y="4.5" width="18" height="17" rx="3" strokeLinejoin="round" />
|
|
52
|
+
{/* eyes */}
|
|
53
|
+
<circle cx="8" cy="11.5" r="2" fill="currentColor" stroke="none" />
|
|
54
|
+
<circle cx="16" cy="11.5" r="2" fill="currentColor" stroke="none" />
|
|
55
|
+
{/* mouth */}
|
|
56
|
+
<line x1="8.5" y1="17.5" x2="15.5" y2="17.5" strokeLinecap="round" />
|
|
57
|
+
{/* ears */}
|
|
58
|
+
<line x1="0.5" y1="13" x2="3" y2="13" strokeLinecap="round" />
|
|
59
|
+
<line x1="21" y1="13" x2="23.5" y2="13" strokeLinecap="round" />
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
function HandOffButton({
|
|
64
|
+
activeFilePath,
|
|
65
|
+
commentCounts,
|
|
66
|
+
onCopyAgentPrompt,
|
|
67
|
+
}: {
|
|
68
|
+
activeFilePath: string;
|
|
69
|
+
commentCounts: Map<string, number>;
|
|
70
|
+
onCopyAgentPrompt: (filePaths: string[]) => void;
|
|
71
|
+
}) {
|
|
72
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
73
|
+
|
|
74
|
+
// All files with comments
|
|
75
|
+
const filesWithComments = Array.from(commentCounts.entries())
|
|
76
|
+
.filter(([, count]) => count > 0)
|
|
77
|
+
.map(([path, count]) => ({ path, count }));
|
|
78
|
+
|
|
79
|
+
const hasMultipleFiles = filesWithComments.length > 1;
|
|
80
|
+
|
|
81
|
+
const toggle = (path: string) => {
|
|
82
|
+
setSelected((prev) => {
|
|
83
|
+
const next = new Set(prev);
|
|
84
|
+
if (next.has(path)) next.delete(path);
|
|
85
|
+
else next.add(path);
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!hasMultipleFiles) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="relative flex items-center" data-testid="handoff-group">
|
|
93
|
+
<IconButton
|
|
94
|
+
variant="neutral"
|
|
95
|
+
onClick={() => onCopyAgentPrompt([activeFilePath])}
|
|
96
|
+
title="Hand off to agent — copy instructions for this file"
|
|
97
|
+
data-testid="handoff-button"
|
|
98
|
+
>
|
|
99
|
+
{handOffIcon}
|
|
100
|
+
</IconButton>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
title="Hand off multiple files"
|
|
104
|
+
data-testid="handoff-chevron"
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
tabIndex={-1}
|
|
107
|
+
className="pl-0 pr-0.5 self-stretch flex items-center rounded-r opacity-0 pointer-events-none text-content-muted"
|
|
108
|
+
>
|
|
109
|
+
<svg
|
|
110
|
+
className="w-2 h-2"
|
|
111
|
+
fill="none"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth={3}
|
|
115
|
+
>
|
|
116
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="relative flex items-center" data-testid="handoff-group">
|
|
125
|
+
<SplitIconButton
|
|
126
|
+
icon={handOffIcon}
|
|
127
|
+
onClick={() => onCopyAgentPrompt([activeFilePath])}
|
|
128
|
+
title="Hand off to agent — copy instructions for this file"
|
|
129
|
+
testId="handoff-button"
|
|
130
|
+
chevronTestId="handoff-chevron"
|
|
131
|
+
chevronTitle="Hand off multiple files"
|
|
132
|
+
onOpen={() => setSelected(new Set(filesWithComments.map((f) => f.path)))}
|
|
133
|
+
dropdown={(close) => {
|
|
134
|
+
const handleCopySelected = () => {
|
|
135
|
+
const paths = Array.from(selected);
|
|
136
|
+
if (paths.length === 0) return;
|
|
137
|
+
onCopyAgentPrompt(paths);
|
|
138
|
+
close();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="absolute right-0 top-full mt-1 z-50 bg-surface border border-border rounded-lg shadow-lg py-1.5 min-w-[240px]">
|
|
143
|
+
{filesWithComments.map(({ path, count }) => {
|
|
144
|
+
const isSelected = selected.has(path);
|
|
145
|
+
const isActive = path === activeFilePath;
|
|
146
|
+
return (
|
|
147
|
+
<button
|
|
148
|
+
key={path}
|
|
149
|
+
onClick={() => toggle(path)}
|
|
150
|
+
className="w-full px-3 py-1.5 flex items-center gap-2 hover:bg-tint transition-colors"
|
|
151
|
+
title={path}
|
|
152
|
+
>
|
|
153
|
+
<span className="w-3 h-3 shrink-0 flex items-center justify-center">
|
|
154
|
+
{isSelected && (
|
|
155
|
+
<svg
|
|
156
|
+
className="w-3 h-3 text-primary-text"
|
|
157
|
+
fill="none"
|
|
158
|
+
viewBox="0 0 24 24"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
strokeWidth={2.5}
|
|
161
|
+
>
|
|
162
|
+
<path
|
|
163
|
+
strokeLinecap="round"
|
|
164
|
+
strokeLinejoin="round"
|
|
165
|
+
d="M4.5 12.75l6 6 9-13.5"
|
|
166
|
+
/>
|
|
167
|
+
</svg>
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
170
|
+
<span
|
|
171
|
+
className={`text-xs truncate flex-1 text-left ${isActive ? 'text-content font-medium' : 'text-content'}`}
|
|
172
|
+
>
|
|
173
|
+
{getPathBasename(path)}
|
|
174
|
+
</span>
|
|
175
|
+
<span className="text-[10px] text-content-muted">{count}</span>
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
|
|
180
|
+
{/* Action button */}
|
|
181
|
+
<div className="border-t border-border-subtle mt-1.5 pt-1.5 px-3 pb-1">
|
|
182
|
+
<button
|
|
183
|
+
onClick={handleCopySelected}
|
|
184
|
+
disabled={selected.size === 0}
|
|
185
|
+
className={`w-full text-xs px-2 py-1.5 rounded-md font-medium transition-opacity ${
|
|
186
|
+
selected.size > 0
|
|
187
|
+
? 'bg-primary-bg-strong text-primary-text hover:opacity-90'
|
|
188
|
+
: 'bg-surface-inset text-content-muted cursor-not-allowed'
|
|
189
|
+
}`}
|
|
190
|
+
>
|
|
191
|
+
{selected.size === 0
|
|
192
|
+
? 'Select files to hand off'
|
|
193
|
+
: `Copy handoff for ${selected.size} file${selected.size !== 1 ? 's' : ''}`}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function TabBar({
|
|
205
|
+
tabs,
|
|
206
|
+
activeFilePath,
|
|
207
|
+
commentCounts,
|
|
208
|
+
resolvedCommentCounts,
|
|
209
|
+
onSwitchTab,
|
|
210
|
+
onCloseTab,
|
|
211
|
+
onOpenFile,
|
|
212
|
+
onTabContextMenu,
|
|
213
|
+
viewMode,
|
|
214
|
+
diffPending,
|
|
215
|
+
commentCount,
|
|
216
|
+
onViewModeChange,
|
|
217
|
+
onSearch,
|
|
218
|
+
searchActive,
|
|
219
|
+
onCopyAgentPrompt,
|
|
220
|
+
}: Props) {
|
|
221
|
+
const modLabel = getPrimaryModifierLabel();
|
|
222
|
+
const tabsViewportRef = useRef<HTMLDivElement>(null);
|
|
223
|
+
const tabsContentRef = useRef<HTMLDivElement>(null);
|
|
224
|
+
const tabListRef = useRef<HTMLDivElement>(null);
|
|
225
|
+
const tabButtonRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
226
|
+
const [tabListOpen, setTabListOpen] = useState(false);
|
|
227
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
228
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
229
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
230
|
+
|
|
231
|
+
const updateTabOverflow = useCallback(() => {
|
|
232
|
+
const viewport = tabsViewportRef.current;
|
|
233
|
+
if (!viewport) return;
|
|
234
|
+
|
|
235
|
+
const maxScrollLeft = Math.max(viewport.scrollWidth - viewport.clientWidth, 0);
|
|
236
|
+
setIsOverflowing(maxScrollLeft > 1);
|
|
237
|
+
setCanScrollLeft(viewport.scrollLeft > 1);
|
|
238
|
+
setCanScrollRight(viewport.scrollLeft < maxScrollLeft - 1);
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
const scrollTabsBy = useCallback((direction: 'left' | 'right') => {
|
|
242
|
+
const viewport = tabsViewportRef.current;
|
|
243
|
+
if (!viewport) return;
|
|
244
|
+
|
|
245
|
+
const distance = Math.max(Math.round(viewport.clientWidth * 0.6), 160);
|
|
246
|
+
viewport.scrollBy({
|
|
247
|
+
left: direction === 'left' ? -distance : distance,
|
|
248
|
+
behavior: 'smooth',
|
|
249
|
+
});
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const viewport = tabsViewportRef.current;
|
|
254
|
+
if (!viewport) return;
|
|
255
|
+
|
|
256
|
+
updateTabOverflow();
|
|
257
|
+
|
|
258
|
+
const handleScroll = () => updateTabOverflow();
|
|
259
|
+
viewport.addEventListener('scroll', handleScroll, { passive: true });
|
|
260
|
+
|
|
261
|
+
const resizeObserver =
|
|
262
|
+
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => updateTabOverflow()) : null;
|
|
263
|
+
|
|
264
|
+
resizeObserver?.observe(viewport);
|
|
265
|
+
if (tabsContentRef.current) resizeObserver?.observe(tabsContentRef.current);
|
|
266
|
+
|
|
267
|
+
return () => {
|
|
268
|
+
viewport.removeEventListener('scroll', handleScroll);
|
|
269
|
+
resizeObserver?.disconnect();
|
|
270
|
+
};
|
|
271
|
+
}, [tabs.length, updateTabOverflow]);
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!activeFilePath) return;
|
|
275
|
+
|
|
276
|
+
const frame = window.requestAnimationFrame(() => {
|
|
277
|
+
tabButtonRefs.current
|
|
278
|
+
.get(activeFilePath)
|
|
279
|
+
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
280
|
+
updateTabOverflow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return () => window.cancelAnimationFrame(frame);
|
|
284
|
+
}, [activeFilePath, updateTabOverflow]);
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!tabListOpen) return;
|
|
288
|
+
|
|
289
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
290
|
+
if (tabListRef.current && !tabListRef.current.contains(event.target as Node)) {
|
|
291
|
+
setTabListOpen(false);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
296
|
+
if (event.key === 'Escape') {
|
|
297
|
+
setTabListOpen(false);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
302
|
+
document.addEventListener('keydown', handleEscape);
|
|
303
|
+
return () => {
|
|
304
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
305
|
+
document.removeEventListener('keydown', handleEscape);
|
|
306
|
+
};
|
|
307
|
+
}, [tabListOpen]);
|
|
308
|
+
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (!isOverflowing) setTabListOpen(false);
|
|
311
|
+
}, [isOverflowing]);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div className="h-9 bg-surface-secondary border-b border-border flex items-stretch shrink-0">
|
|
315
|
+
<div className="min-w-0 flex-1 flex items-stretch">
|
|
316
|
+
{isOverflowing && (
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={() => scrollTabsBy('left')}
|
|
320
|
+
disabled={!canScrollLeft}
|
|
321
|
+
className={`${tabControlButtonClass} border-r border-l-0`}
|
|
322
|
+
title="Scroll tabs left"
|
|
323
|
+
aria-label="Scroll tabs left"
|
|
324
|
+
>
|
|
325
|
+
<svg
|
|
326
|
+
className="w-3.5 h-3.5"
|
|
327
|
+
fill="none"
|
|
328
|
+
viewBox="0 0 24 24"
|
|
329
|
+
stroke="currentColor"
|
|
330
|
+
strokeWidth={2}
|
|
331
|
+
>
|
|
332
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
|
333
|
+
</svg>
|
|
334
|
+
</button>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
<div ref={tabsViewportRef} className="min-w-0 flex-1 h-full overflow-x-auto no-scrollbar">
|
|
338
|
+
<div ref={tabsContentRef} className="flex h-full items-stretch min-w-max">
|
|
339
|
+
{tabs.map((tab) => {
|
|
340
|
+
const isActive = tab.filePath === activeFilePath;
|
|
341
|
+
const fileName = getPathBasename(tab.filePath) || tab.filePath;
|
|
342
|
+
const count = commentCounts.get(tab.filePath) ?? 0;
|
|
343
|
+
const resolvedCount = resolvedCommentCounts?.get(tab.filePath) ?? 0;
|
|
344
|
+
return (
|
|
345
|
+
<button
|
|
346
|
+
key={tab.filePath}
|
|
347
|
+
ref={(node) => {
|
|
348
|
+
if (node) tabButtonRefs.current.set(tab.filePath, node);
|
|
349
|
+
else tabButtonRefs.current.delete(tab.filePath);
|
|
350
|
+
}}
|
|
351
|
+
onClick={() => onSwitchTab(tab.filePath)}
|
|
352
|
+
onMouseDown={(e) => {
|
|
353
|
+
if (e.button === 1) {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
onCloseTab(tab.filePath);
|
|
356
|
+
}
|
|
357
|
+
}}
|
|
358
|
+
onContextMenu={(e) => {
|
|
359
|
+
if (onTabContextMenu) {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
onTabContextMenu({ filePath: tab.filePath, x: e.clientX, y: e.clientY });
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
className={`group flex h-full items-center gap-1.5 px-3 text-xs leading-none border-r border-border border-b-2 shrink-0 max-w-[200px] transition-colors ${
|
|
365
|
+
isActive
|
|
366
|
+
? 'bg-surface text-content font-medium border-b-primary'
|
|
367
|
+
: 'border-b-transparent text-content-secondary hover:text-content hover:bg-tint'
|
|
368
|
+
}`}
|
|
369
|
+
title={tab.filePath}
|
|
370
|
+
>
|
|
371
|
+
<span className="truncate">{fileName}</span>
|
|
372
|
+
{count > 0 ? (
|
|
373
|
+
<span
|
|
374
|
+
className={`text-[10px] font-medium px-1 min-w-[16px] text-center rounded-full shrink-0 ${
|
|
375
|
+
isActive
|
|
376
|
+
? 'bg-primary-bg-strong text-primary-text'
|
|
377
|
+
: 'bg-surface-inset text-content-secondary'
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
{count}
|
|
381
|
+
</span>
|
|
382
|
+
) : resolvedCount > 0 ? (
|
|
383
|
+
<span
|
|
384
|
+
className="text-[10px] font-medium px-1 min-w-[16px] text-center rounded-full shrink-0 border border-border-subtle text-content-muted"
|
|
385
|
+
title={`${resolvedCount} resolved`}
|
|
386
|
+
>
|
|
387
|
+
{resolvedCount}
|
|
388
|
+
</span>
|
|
389
|
+
) : null}
|
|
390
|
+
{tab.error && <span className="w-1.5 h-1.5 rounded-full bg-danger shrink-0" />}
|
|
391
|
+
<span
|
|
392
|
+
role="button"
|
|
393
|
+
onClick={(e) => {
|
|
394
|
+
e.stopPropagation();
|
|
395
|
+
onCloseTab(tab.filePath);
|
|
396
|
+
}}
|
|
397
|
+
className={`ml-1 p-0.5 rounded hover:bg-tint shrink-0 transition-opacity ${
|
|
398
|
+
isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
399
|
+
}`}
|
|
400
|
+
aria-label={`Close ${getPathBasename(tab.filePath)}`}
|
|
401
|
+
tabIndex={-1}
|
|
402
|
+
>
|
|
403
|
+
<svg
|
|
404
|
+
className="w-3 h-3"
|
|
405
|
+
viewBox="0 0 24 24"
|
|
406
|
+
fill="none"
|
|
407
|
+
stroke="currentColor"
|
|
408
|
+
strokeWidth={2}
|
|
409
|
+
>
|
|
410
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
411
|
+
</svg>
|
|
412
|
+
</span>
|
|
413
|
+
</button>
|
|
414
|
+
);
|
|
415
|
+
})}
|
|
416
|
+
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
onClick={onOpenFile}
|
|
420
|
+
className={tabActionButtonClass}
|
|
421
|
+
title="Open file"
|
|
422
|
+
aria-label="Open file"
|
|
423
|
+
>
|
|
424
|
+
<svg
|
|
425
|
+
className="w-4 h-4"
|
|
426
|
+
viewBox="0 0 24 24"
|
|
427
|
+
fill="none"
|
|
428
|
+
stroke="currentColor"
|
|
429
|
+
strokeWidth={2}
|
|
430
|
+
>
|
|
431
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
432
|
+
</svg>
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
{isOverflowing && (
|
|
438
|
+
<>
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
onClick={() => scrollTabsBy('right')}
|
|
442
|
+
disabled={!canScrollRight}
|
|
443
|
+
className={tabControlButtonClass}
|
|
444
|
+
title="Scroll tabs right"
|
|
445
|
+
aria-label="Scroll tabs right"
|
|
446
|
+
>
|
|
447
|
+
<svg
|
|
448
|
+
className="w-3.5 h-3.5"
|
|
449
|
+
fill="none"
|
|
450
|
+
viewBox="0 0 24 24"
|
|
451
|
+
stroke="currentColor"
|
|
452
|
+
strokeWidth={2}
|
|
453
|
+
>
|
|
454
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
455
|
+
</svg>
|
|
456
|
+
</button>
|
|
457
|
+
|
|
458
|
+
<div ref={tabListRef} className="relative flex items-stretch shrink-0">
|
|
459
|
+
<button
|
|
460
|
+
type="button"
|
|
461
|
+
onClick={() => setTabListOpen((open) => !open)}
|
|
462
|
+
className={`${tabControlButtonClass} ${
|
|
463
|
+
tabListOpen ? 'bg-surface-inset text-content-secondary' : ''
|
|
464
|
+
}`}
|
|
465
|
+
title="Show all open tabs"
|
|
466
|
+
aria-label="Show all open tabs"
|
|
467
|
+
aria-haspopup="menu"
|
|
468
|
+
aria-expanded={tabListOpen}
|
|
469
|
+
data-testid="tab-list-button"
|
|
470
|
+
>
|
|
471
|
+
<svg
|
|
472
|
+
className="w-3.5 h-3.5"
|
|
473
|
+
fill="none"
|
|
474
|
+
viewBox="0 0 24 24"
|
|
475
|
+
stroke="currentColor"
|
|
476
|
+
strokeWidth={2}
|
|
477
|
+
>
|
|
478
|
+
<path
|
|
479
|
+
strokeLinecap="round"
|
|
480
|
+
strokeLinejoin="round"
|
|
481
|
+
d="M5.25 6.75h13.5M5.25 12h13.5M5.25 17.25h13.5"
|
|
482
|
+
/>
|
|
483
|
+
</svg>
|
|
484
|
+
</button>
|
|
485
|
+
|
|
486
|
+
{tabListOpen && (
|
|
487
|
+
<div
|
|
488
|
+
className="absolute right-0 top-full z-50 mt-1 w-72 rounded-lg border border-border bg-surface shadow-lg py-1.5"
|
|
489
|
+
data-testid="tab-list-menu"
|
|
490
|
+
>
|
|
491
|
+
{tabs.map((tab) => {
|
|
492
|
+
const isActive = tab.filePath === activeFilePath;
|
|
493
|
+
const fileName = getPathBasename(tab.filePath) || tab.filePath;
|
|
494
|
+
const count = commentCounts.get(tab.filePath) ?? 0;
|
|
495
|
+
const resolvedCount = resolvedCommentCounts?.get(tab.filePath) ?? 0;
|
|
496
|
+
return (
|
|
497
|
+
<button
|
|
498
|
+
key={tab.filePath}
|
|
499
|
+
type="button"
|
|
500
|
+
onClick={() => {
|
|
501
|
+
onSwitchTab(tab.filePath);
|
|
502
|
+
setTabListOpen(false);
|
|
503
|
+
}}
|
|
504
|
+
className={`w-full px-3 py-2 flex items-center gap-2 text-left transition-colors ${
|
|
505
|
+
isActive ? 'bg-surface-inset' : 'hover:bg-tint'
|
|
506
|
+
}`}
|
|
507
|
+
title={tab.filePath}
|
|
508
|
+
>
|
|
509
|
+
<span
|
|
510
|
+
className={`w-1.5 h-1.5 rounded-full shrink-0 ${isActive ? 'bg-primary' : 'bg-transparent'}`}
|
|
511
|
+
/>
|
|
512
|
+
<span
|
|
513
|
+
className={`text-xs truncate flex-1 ${isActive ? 'text-content font-medium' : 'text-content'}`}
|
|
514
|
+
>
|
|
515
|
+
{fileName}
|
|
516
|
+
</span>
|
|
517
|
+
{count > 0 ? (
|
|
518
|
+
<span
|
|
519
|
+
className={`text-[10px] font-medium px-1 min-w-[16px] text-center rounded-full shrink-0 ${
|
|
520
|
+
isActive
|
|
521
|
+
? 'bg-primary-bg-strong text-primary-text'
|
|
522
|
+
: 'bg-surface-inset text-content-secondary'
|
|
523
|
+
}`}
|
|
524
|
+
>
|
|
525
|
+
{count}
|
|
526
|
+
</span>
|
|
527
|
+
) : resolvedCount > 0 ? (
|
|
528
|
+
<span
|
|
529
|
+
className="text-[10px] font-medium px-1 min-w-[16px] text-center rounded-full shrink-0 border border-border-subtle text-content-muted"
|
|
530
|
+
title={`${resolvedCount} resolved`}
|
|
531
|
+
>
|
|
532
|
+
{resolvedCount}
|
|
533
|
+
</span>
|
|
534
|
+
) : null}
|
|
535
|
+
{tab.error && (
|
|
536
|
+
<span className="w-1.5 h-1.5 rounded-full bg-danger shrink-0" />
|
|
537
|
+
)}
|
|
538
|
+
</button>
|
|
539
|
+
);
|
|
540
|
+
})}
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
{/* Document actions (right side) */}
|
|
549
|
+
<div className="flex items-center gap-0.5 px-2 shrink-0 border-l border-border">
|
|
550
|
+
{/* Search */}
|
|
551
|
+
<IconButton
|
|
552
|
+
variant="active"
|
|
553
|
+
active={searchActive}
|
|
554
|
+
onClick={onSearch}
|
|
555
|
+
title={`Find in document (${modLabel}+F)`}
|
|
556
|
+
>
|
|
557
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
558
|
+
<circle cx="11" cy="11" r="8" />
|
|
559
|
+
<path strokeLinecap="round" d="m21 21-4.35-4.35" />
|
|
560
|
+
</svg>
|
|
561
|
+
</IconButton>
|
|
562
|
+
|
|
563
|
+
<IconButton
|
|
564
|
+
variant="active"
|
|
565
|
+
active={viewMode === 'raw'}
|
|
566
|
+
onClick={() => onViewModeChange(viewMode === 'raw' ? 'rendered' : 'raw')}
|
|
567
|
+
title={viewMode === 'raw' ? 'Switch to rendered view' : 'View raw markdown'}
|
|
568
|
+
>
|
|
569
|
+
<span className="relative">
|
|
570
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
571
|
+
<path
|
|
572
|
+
strokeLinecap="round"
|
|
573
|
+
strokeLinejoin="round"
|
|
574
|
+
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
|
|
575
|
+
/>
|
|
576
|
+
</svg>
|
|
577
|
+
{diffPending && viewMode !== 'raw' && (
|
|
578
|
+
<span className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary animate-pulse" />
|
|
579
|
+
)}
|
|
580
|
+
</span>
|
|
581
|
+
</IconButton>
|
|
582
|
+
|
|
583
|
+
{/* Hand off (primary action with multi-file dropdown) */}
|
|
584
|
+
{commentCount > 0 && onCopyAgentPrompt && activeFilePath && (
|
|
585
|
+
<HandOffButton
|
|
586
|
+
activeFilePath={activeFilePath}
|
|
587
|
+
commentCounts={commentCounts}
|
|
588
|
+
onCopyAgentPrompt={onCopyAgentPrompt}
|
|
589
|
+
/>
|
|
590
|
+
)}
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
import type { TocHeading } from './MarkdownViewer';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
headings: TocHeading[];
|
|
6
|
+
activeHeadingId: string | null;
|
|
7
|
+
onHeadingClick: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const INDENT: Record<number, string> = {
|
|
11
|
+
1: 'pl-3',
|
|
12
|
+
2: 'pl-6',
|
|
13
|
+
3: 'pl-9',
|
|
14
|
+
4: 'pl-12',
|
|
15
|
+
5: 'pl-12',
|
|
16
|
+
6: 'pl-12',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function TableOfContents({ headings, activeHeadingId, onHeadingClick }: Props) {
|
|
20
|
+
const activeRef = useRef<HTMLButtonElement>(null);
|
|
21
|
+
|
|
22
|
+
// Auto-scroll the active heading into view within the TOC panel
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
activeRef.current?.scrollIntoView({ block: 'nearest' });
|
|
25
|
+
}, [activeHeadingId]);
|
|
26
|
+
|
|
27
|
+
if (headings.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex-1 flex flex-col items-center justify-center text-content-muted px-4">
|
|
30
|
+
<svg
|
|
31
|
+
className="w-8 h-8 mb-2 opacity-40"
|
|
32
|
+
fill="none"
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth={1.5}
|
|
36
|
+
>
|
|
37
|
+
<path
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
<span className="text-[10px]">No headings</span>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex-1 overflow-y-auto py-1">
|
|
50
|
+
{headings.map((h) => {
|
|
51
|
+
const isActive = h.id === activeHeadingId;
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
key={h.id}
|
|
55
|
+
ref={isActive ? activeRef : undefined}
|
|
56
|
+
onClick={() => onHeadingClick(h.id)}
|
|
57
|
+
className={`w-full text-left py-1.5 pr-3 text-xs transition-colors ${INDENT[h.level] || 'pl-3'} ${
|
|
58
|
+
isActive
|
|
59
|
+
? 'bg-primary-bg text-primary-text font-medium'
|
|
60
|
+
: 'text-content hover:bg-tint'
|
|
61
|
+
}`}
|
|
62
|
+
title={h.text}
|
|
63
|
+
>
|
|
64
|
+
<span className="block truncate">{h.text}</span>
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|