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,159 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { useThemePersistence } from '../hooks/useThemePersistence';
3
+ import { LIGHT_THEMES, DARK_THEMES } from '../lib/themes';
4
+
5
+ function ThemeButton({
6
+ t,
7
+ theme,
8
+ onClick,
9
+ }: {
10
+ t: { key: string; label: string; colors: string[] };
11
+ theme: string | undefined;
12
+ onClick: () => void;
13
+ }) {
14
+ return (
15
+ <button
16
+ onClick={onClick}
17
+ className={`w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 transition-colors ${
18
+ theme === t.key
19
+ ? 'bg-primary-bg text-primary-text font-medium'
20
+ : 'text-content-secondary hover:bg-tint'
21
+ }`}
22
+ >
23
+ <div className="flex gap-0.5">
24
+ {t.colors.map((c, i) => (
25
+ <div
26
+ key={i}
27
+ className="w-3 h-3 rounded-full border border-border-subtle"
28
+ style={{ backgroundColor: c }}
29
+ />
30
+ ))}
31
+ </div>
32
+ {t.label}
33
+ {theme === t.key && (
34
+ <svg
35
+ className="w-3.5 h-3.5 ml-auto"
36
+ fill="none"
37
+ viewBox="0 0 24 24"
38
+ stroke="currentColor"
39
+ strokeWidth={2}
40
+ >
41
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
42
+ </svg>
43
+ )}
44
+ </button>
45
+ );
46
+ }
47
+
48
+ export function ThemeSelector() {
49
+ const { theme, setTheme } = useThemePersistence();
50
+ const [open, setOpen] = useState(false);
51
+ const ref = useRef<HTMLDivElement>(null);
52
+
53
+ useEffect(() => {
54
+ if (!open) return;
55
+ const handler = (e: MouseEvent) => {
56
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
57
+ };
58
+ document.addEventListener('mousedown', handler);
59
+ return () => document.removeEventListener('mousedown', handler);
60
+ }, [open]);
61
+
62
+ return (
63
+ <div className="relative" ref={ref}>
64
+ <button
65
+ onClick={() => setOpen(!open)}
66
+ className="text-content-muted hover:text-content-secondary transition-colors p-1 rounded hover:bg-tint"
67
+ title="Switch theme"
68
+ >
69
+ <svg
70
+ className="w-4 h-4"
71
+ fill="none"
72
+ viewBox="0 0 24 24"
73
+ stroke="currentColor"
74
+ strokeWidth={2}
75
+ >
76
+ <path
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"
80
+ />
81
+ </svg>
82
+ </button>
83
+ {open && (
84
+ <div className="absolute right-0 mt-1 w-44 bg-surface-raised rounded-lg shadow-lg border border-border overflow-hidden z-50 max-h-[70vh] overflow-y-auto">
85
+ {/* System */}
86
+ <button
87
+ onClick={() => {
88
+ setTheme('system');
89
+ setOpen(false);
90
+ }}
91
+ className={`w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 transition-colors ${
92
+ theme === 'system'
93
+ ? 'bg-primary-bg text-primary-text font-medium'
94
+ : 'text-content-secondary hover:bg-tint'
95
+ }`}
96
+ >
97
+ <svg
98
+ className="w-4 h-4"
99
+ fill="none"
100
+ viewBox="0 0 24 24"
101
+ stroke="currentColor"
102
+ strokeWidth={1.5}
103
+ >
104
+ <path
105
+ strokeLinecap="round"
106
+ strokeLinejoin="round"
107
+ d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z"
108
+ />
109
+ </svg>
110
+ System
111
+ {theme === 'system' && (
112
+ <svg
113
+ className="w-3.5 h-3.5 ml-auto"
114
+ fill="none"
115
+ viewBox="0 0 24 24"
116
+ stroke="currentColor"
117
+ strokeWidth={2}
118
+ >
119
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
120
+ </svg>
121
+ )}
122
+ </button>
123
+
124
+ <div className="border-t border-border-subtle my-1" />
125
+ <div className="px-3 py-1 text-[10px] font-medium text-content-muted uppercase tracking-wider">
126
+ Light
127
+ </div>
128
+ {LIGHT_THEMES.map((t) => (
129
+ <ThemeButton
130
+ key={t.key}
131
+ t={t}
132
+ theme={theme}
133
+ onClick={() => {
134
+ setTheme(t.key);
135
+ setOpen(false);
136
+ }}
137
+ />
138
+ ))}
139
+
140
+ <div className="border-t border-border-subtle my-1" />
141
+ <div className="px-3 py-1 text-[10px] font-medium text-content-muted uppercase tracking-wider">
142
+ Dark
143
+ </div>
144
+ {DARK_THEMES.map((t) => (
145
+ <ThemeButton
146
+ key={t.key}
147
+ t={t}
148
+ theme={theme}
149
+ onClick={() => {
150
+ setTheme(t.key);
151
+ setOpen(false);
152
+ }}
153
+ />
154
+ ))}
155
+ </div>
156
+ )}
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,99 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { ToastAction } from '../hooks/useToast';
3
+
4
+ interface Props {
5
+ message: string;
6
+ visible: boolean;
7
+ onDismiss: () => void;
8
+ action?: ToastAction;
9
+ }
10
+
11
+ export function Toast({ message, visible, onDismiss, action }: Props) {
12
+ const [show, setShow] = useState(false);
13
+ const fadeTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
14
+
15
+ // Clean up fade timer on unmount
16
+ useEffect(() => {
17
+ return () => {
18
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
19
+ };
20
+ }, []);
21
+
22
+ useEffect(() => {
23
+ if (visible) {
24
+ // Trigger enter animation
25
+ requestAnimationFrame(() => setShow(true));
26
+ // Reset the auto-dismiss timer on every message update so coalesced
27
+ // toasts stay visible while new events keep arriving.
28
+ const timer = setTimeout(() => {
29
+ setShow(false);
30
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
31
+ fadeTimerRef.current = setTimeout(onDismiss, 200);
32
+ }, 5000);
33
+ return () => {
34
+ clearTimeout(timer);
35
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
36
+ };
37
+ } else {
38
+ setShow(false);
39
+ }
40
+ }, [visible, message, onDismiss]);
41
+
42
+ if (!visible) return null;
43
+
44
+ return (
45
+ <div
46
+ className={`fixed bottom-12 right-4 z-50 transition-all duration-200 ${
47
+ show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
48
+ }`}
49
+ >
50
+ <div className="flex items-center gap-2 px-4 py-2.5 bg-primary text-on-primary text-sm font-medium rounded-lg shadow-lg">
51
+ <svg
52
+ className="w-4 h-4 shrink-0"
53
+ fill="none"
54
+ viewBox="0 0 24 24"
55
+ stroke="currentColor"
56
+ strokeWidth={2}
57
+ >
58
+ <path
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
62
+ />
63
+ </svg>
64
+ {message}
65
+ {action && (
66
+ <button
67
+ onClick={() => {
68
+ action.onClick();
69
+ setShow(false);
70
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
71
+ fadeTimerRef.current = setTimeout(onDismiss, 200);
72
+ }}
73
+ className="ml-1 px-2 py-0.5 rounded text-xs font-semibold bg-on-primary/20 hover:bg-on-primary/30 transition-colors"
74
+ >
75
+ {action.label}
76
+ </button>
77
+ )}
78
+ <button
79
+ onClick={() => {
80
+ setShow(false);
81
+ if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
82
+ fadeTimerRef.current = setTimeout(onDismiss, 200);
83
+ }}
84
+ className="ml-2 opacity-70 hover:opacity-100 transition-opacity"
85
+ >
86
+ <svg
87
+ className="w-3.5 h-3.5"
88
+ fill="none"
89
+ viewBox="0 0 24 24"
90
+ stroke="currentColor"
91
+ strokeWidth={2}
92
+ >
93
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
94
+ </svg>
95
+ </button>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,161 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { getPrimaryModifierLabel } from '../lib/platform';
3
+ import { IconButton } from './IconButton';
4
+ import { Separator } from './Separator';
5
+
6
+ export type ViewMode = 'rendered' | 'raw';
7
+
8
+ interface Props {
9
+ error: string | null;
10
+ isLoading: boolean;
11
+ showExplorer: boolean;
12
+ sidebarVisible: boolean;
13
+ author: string;
14
+ onAuthorChange: (name: string) => void;
15
+ onToggleExplorer: () => void;
16
+ onToggleSidebar: () => void;
17
+ onOpenSettings: () => void;
18
+ }
19
+
20
+ export function Toolbar({
21
+ error,
22
+ isLoading,
23
+ showExplorer,
24
+ sidebarVisible,
25
+ author,
26
+ onAuthorChange,
27
+ onToggleExplorer,
28
+ onToggleSidebar,
29
+ onOpenSettings,
30
+ }: Props) {
31
+ const [editingAuthor, setEditingAuthor] = useState(false);
32
+ const [authorDraft, setAuthorDraft] = useState(author);
33
+ const authorInputRef = useRef<HTMLInputElement>(null);
34
+ const modLabel = getPrimaryModifierLabel();
35
+
36
+ // Sync draft when author changes externally (e.g. from Settings panel)
37
+ useEffect(() => {
38
+ if (!editingAuthor) setAuthorDraft(author);
39
+ }, [author, editingAuthor]);
40
+
41
+ useEffect(() => {
42
+ if (editingAuthor && authorInputRef.current) {
43
+ authorInputRef.current.focus();
44
+ authorInputRef.current.select();
45
+ }
46
+ }, [editingAuthor]);
47
+
48
+ const commitAuthor = () => {
49
+ onAuthorChange(authorDraft);
50
+ setEditingAuthor(false);
51
+ };
52
+
53
+ return (
54
+ <div className="h-12 border-b border-border bg-surface flex items-center px-4 gap-3 shrink-0">
55
+ {/* Explorer toggle (far left) */}
56
+ <IconButton
57
+ variant="active"
58
+ active={showExplorer}
59
+ size="md"
60
+ onClick={onToggleExplorer}
61
+ title={`Toggle file explorer (${modLabel}+B)`}
62
+ >
63
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
64
+ <path
65
+ strokeLinecap="round"
66
+ strokeLinejoin="round"
67
+ 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"
68
+ />
69
+ </svg>
70
+ </IconButton>
71
+
72
+ <Separator />
73
+
74
+ {/* App logo + name — keep in sync with public/favicon.svg */}
75
+ <div className="flex items-center gap-2">
76
+ <svg className="w-5 h-5" viewBox="0 0 100 116" style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}>
77
+ <g transform="matrix(1,0,0,1,-10,-2)">
78
+ <path d="M100,18L100,102C100,107.519 95.519,112 90,112L30,112C24.481,112 20,107.519 20,102L20,18C20,12.481 24.481,8 30,8L90,8C95.519,8 100,12.481 100,18Z" style={{ fill: 'white', stroke: 'currentColor', strokeWidth: 4, opacity: 0.8 }} />
79
+ </g>
80
+ <g transform="matrix(0.935484,0,0,1.6,-6.129032,-30.8)">
81
+ <path d="M91,36.125L91,39.875C91,41.6 88.605,43 85.655,43L34.345,43C31.395,43 29,41.6 29,39.875L29,36.125C29,34.4 31.395,33 34.345,33L85.655,33C88.605,33 91,34.4 91,36.125Z" style={{ fill: 'rgb(55,55,55)' }} />
82
+ </g>
83
+ <g transform="matrix(0.935484,0,0,1.6,-6.129032,-34.8)">
84
+ <path d="M91,56.125L91,59.875C91,61.6 88.605,63 85.655,63L34.345,63C31.395,63 29,61.6 29,59.875L29,56.125C29,54.4 31.395,53 34.345,53L85.655,53C88.605,53 91,54.4 91,56.125Z" style={{ fill: 'rgb(220,38,38)' }} />
85
+ </g>
86
+ <g transform="matrix(0.916667,0,0,1.6,-5.583333,-38.8)">
87
+ <path d="M77,76.125L77,79.875C77,81.6 74.556,83 71.545,83L34.455,83C31.444,83 29,81.6 29,79.875L29,76.125C29,74.4 31.444,73 34.455,73L71.545,73C74.556,73 77,74.4 77,76.125Z" style={{ fill: 'rgb(55,55,55)' }} />
88
+ </g>
89
+ </svg>
90
+ <span className="text-sm font-semibold text-content">md-redline</span>
91
+ </div>
92
+
93
+ {/* Center spacer with status */}
94
+ <div className="flex-1 flex items-center justify-center gap-2 min-w-0">
95
+ {error && <span className="text-xs text-danger font-medium">{error}</span>}
96
+ {isLoading && <span className="text-xs text-content-muted">Loading...</span>}
97
+ </div>
98
+
99
+ {/* Author name */}
100
+ {editingAuthor ? (
101
+ <input
102
+ ref={authorInputRef}
103
+ value={authorDraft}
104
+ onChange={(e) => setAuthorDraft(e.target.value)}
105
+ onBlur={commitAuthor}
106
+ onKeyDown={(e) => {
107
+ if (e.key === 'Enter') commitAuthor();
108
+ if (e.key === 'Escape') {
109
+ setAuthorDraft(author);
110
+ setEditingAuthor(false);
111
+ }
112
+ }}
113
+ className="text-xs w-24 px-1.5 py-0.5 rounded border border-primary bg-surface text-content focus:outline-none focus:ring-1 focus:ring-primary"
114
+ placeholder="Your name"
115
+ />
116
+ ) : (
117
+ <button
118
+ onClick={() => {
119
+ setAuthorDraft(author);
120
+ setEditingAuthor(true);
121
+ }}
122
+ className="flex items-center gap-1.5 text-xs text-content-secondary hover:text-content transition-colors px-1.5 py-0.5 rounded hover:bg-tint"
123
+ title="Click to change author name"
124
+ >
125
+ {author}
126
+ </button>
127
+ )}
128
+
129
+ {/* Settings */}
130
+ <IconButton size="md" onClick={onOpenSettings} title={`Settings (${modLabel}+,)`}>
131
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
132
+ <path
133
+ strokeLinecap="round"
134
+ strokeLinejoin="round"
135
+ d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"
136
+ />
137
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
138
+ </svg>
139
+ </IconButton>
140
+
141
+ <Separator />
142
+
143
+ {/* Comments sidebar toggle (far right, mirrors explorer) */}
144
+ <IconButton
145
+ variant="active"
146
+ active={sidebarVisible}
147
+ size="md"
148
+ onClick={onToggleSidebar}
149
+ title={`Toggle comments sidebar (${modLabel}+\\)`}
150
+ >
151
+ <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
152
+ <path
153
+ strokeLinecap="round"
154
+ strokeLinejoin="round"
155
+ 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"
156
+ />
157
+ </svg>
158
+ </IconButton>
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,19 @@
1
+ export type Variant = 'neutral' | 'active' | 'success';
2
+
3
+ export const variantClasses: Record<Variant, { on: string; off: string; coordinated: string }> = {
4
+ neutral: {
5
+ on: 'text-content-muted hover:text-content-secondary hover:bg-tint',
6
+ off: 'text-content-muted hover:text-content-secondary hover:bg-tint',
7
+ coordinated: 'text-content-secondary bg-tint',
8
+ },
9
+ active: {
10
+ on: 'text-primary-text bg-primary-bg',
11
+ off: 'text-content-muted hover:text-content-secondary hover:bg-tint',
12
+ coordinated: 'text-content-secondary bg-tint',
13
+ },
14
+ success: {
15
+ on: 'text-success-text hover:text-success hover:bg-tint-success',
16
+ off: 'text-content-muted hover:text-content-secondary hover:bg-tint',
17
+ coordinated: 'text-content-secondary bg-tint',
18
+ },
19
+ };