@karmaniverous/jeeves-server 3.0.0-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 (260) hide show
  1. package/.env.local +13 -0
  2. package/.env.local.template +13 -0
  3. package/.tsbuildinfo +1 -0
  4. package/CHANGELOG.md +450 -0
  5. package/about.md +82 -0
  6. package/client/README.md +73 -0
  7. package/client/eslint.config.js +23 -0
  8. package/client/index.html +14 -0
  9. package/client/package-lock.json +5181 -0
  10. package/client/package.json +60 -0
  11. package/client/public/vite.svg +1 -0
  12. package/client/src/App.tsx +22 -0
  13. package/client/src/components/AccountMenu.tsx +167 -0
  14. package/client/src/components/ActionDropdown.tsx +120 -0
  15. package/client/src/components/CodeEditor.tsx +143 -0
  16. package/client/src/components/CodeViewer.tsx +113 -0
  17. package/client/src/components/ConfirmDialog.tsx +32 -0
  18. package/client/src/components/DirectoryRow.tsx +62 -0
  19. package/client/src/components/DirectoryTable.tsx +42 -0
  20. package/client/src/components/DownloadDropdown.tsx +116 -0
  21. package/client/src/components/DriveList.tsx +54 -0
  22. package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
  23. package/client/src/components/FileContentView.tsx +155 -0
  24. package/client/src/components/InlineSvgPanzoom.ts +60 -0
  25. package/client/src/components/LazyDiagram.ts +93 -0
  26. package/client/src/components/LinkDropdown.tsx +134 -0
  27. package/client/src/components/MarkdownView.tsx +115 -0
  28. package/client/src/components/MermaidViewer.tsx +21 -0
  29. package/client/src/components/PlantUmlViewer.tsx +21 -0
  30. package/client/src/components/SearchModal.tsx +424 -0
  31. package/client/src/components/SvgViewer.tsx +107 -0
  32. package/client/src/components/TabBar.tsx +96 -0
  33. package/client/src/components/layout/Header.tsx +270 -0
  34. package/client/src/components/panzoom.ts +203 -0
  35. package/client/src/components/renderableUtils.ts +15 -0
  36. package/client/src/components/runner/JobTable.tsx +153 -0
  37. package/client/src/components/runner/RunHistory.tsx +140 -0
  38. package/client/src/components/runner/StatsBar.tsx +43 -0
  39. package/client/src/components/runner/StatusPill.tsx +27 -0
  40. package/client/src/components/runner/jobTableUtils.ts +65 -0
  41. package/client/src/components/scrollUtils.ts +39 -0
  42. package/client/src/components/ui/alert-dialog.tsx +107 -0
  43. package/client/src/components/ui/button.tsx +40 -0
  44. package/client/src/components/ui/dropdown-menu.tsx +79 -0
  45. package/client/src/components/ui/input.tsx +26 -0
  46. package/client/src/components/useActionState.ts +43 -0
  47. package/client/src/hooks/useFileBrowser.ts +102 -0
  48. package/client/src/hooks/useFileData.ts +78 -0
  49. package/client/src/hooks/useScrollAnchor.ts +70 -0
  50. package/client/src/hooks/useShareSettings.ts +22 -0
  51. package/client/src/hooks/useTopBar.ts +27 -0
  52. package/client/src/index.css +281 -0
  53. package/client/src/lib/AuthContext.ts +27 -0
  54. package/client/src/lib/api.ts +239 -0
  55. package/client/src/lib/auth.tsx +50 -0
  56. package/client/src/lib/codeBlockCm6.ts +129 -0
  57. package/client/src/lib/codeBlockCopy.ts +43 -0
  58. package/client/src/lib/codemirror.ts +77 -0
  59. package/client/src/lib/runner-api.ts +172 -0
  60. package/client/src/lib/svg.ts +50 -0
  61. package/client/src/lib/theme.ts +34 -0
  62. package/client/src/lib/utils.ts +6 -0
  63. package/client/src/main.tsx +11 -0
  64. package/client/src/pages/FileBrowser.tsx +135 -0
  65. package/client/src/pages/Home.tsx +46 -0
  66. package/client/src/pages/Runner.tsx +151 -0
  67. package/client/src/pages/RunnerJob.tsx +170 -0
  68. package/client/tsconfig.app.json +32 -0
  69. package/client/tsconfig.json +7 -0
  70. package/client/tsconfig.node.json +26 -0
  71. package/client/vite.config.ts +35 -0
  72. package/content/privacy.md +61 -0
  73. package/content/terms.md +41 -0
  74. package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
  75. package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
  76. package/dist/client/assets/index--MBieNJA.js +1 -0
  77. package/dist/client/assets/index-BENeXQI_.js +1 -0
  78. package/dist/client/assets/index-BbBpoOxz.js +1 -0
  79. package/dist/client/assets/index-BdV9g5AM.js +6 -0
  80. package/dist/client/assets/index-BjAilRri.js +2 -0
  81. package/dist/client/assets/index-BqbhWo2I.js +3 -0
  82. package/dist/client/assets/index-CVbycZ0H.js +1 -0
  83. package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
  84. package/dist/client/assets/index-D8KZVveX.js +1 -0
  85. package/dist/client/assets/index-DC4HMHxY.js +13 -0
  86. package/dist/client/assets/index-DbMebkkd.css +1 -0
  87. package/dist/client/assets/index-DcY2RXqX.js +1 -0
  88. package/dist/client/assets/index-Duy-tZYV.js +1 -0
  89. package/dist/client/assets/index-Dw7rDFmE.js +7 -0
  90. package/dist/client/assets/index-FlCUvrjv.js +2 -0
  91. package/dist/client/assets/index-K6OVmfhg.js +1 -0
  92. package/dist/client/assets/index-LjwgzZ7F.js +62 -0
  93. package/dist/client/assets/index-MLwyFRN0.js +1 -0
  94. package/dist/client/assets/index-OpqBpSjn.js +1 -0
  95. package/dist/client/assets/index-SsHei0HE.js +1 -0
  96. package/dist/client/assets/index-uQa2yckk.js +1 -0
  97. package/dist/client/assets/index-udkXoIER.js +1 -0
  98. package/dist/client/index.html +15 -0
  99. package/dist/client/vite.svg +1 -0
  100. package/dist/src/auth/google.js +57 -0
  101. package/dist/src/auth/keys.js +185 -0
  102. package/dist/src/auth/resolve.js +102 -0
  103. package/dist/src/auth/session.js +57 -0
  104. package/dist/src/cli/commands/config.js +100 -0
  105. package/dist/src/cli/commands/config.test.js +84 -0
  106. package/dist/src/cli/commands/service.js +93 -0
  107. package/dist/src/cli/commands/start.js +24 -0
  108. package/dist/src/cli/index.js +20 -0
  109. package/dist/src/config/index.js +90 -0
  110. package/dist/src/config/loadConfig.test.js +127 -0
  111. package/dist/src/config/resolve.js +134 -0
  112. package/dist/src/config/resolve.test.js +148 -0
  113. package/dist/src/config/schema.js +159 -0
  114. package/dist/src/config/substituteEnvVars.js +45 -0
  115. package/dist/src/config/substituteEnvVars.test.js +51 -0
  116. package/dist/src/config/types.js +5 -0
  117. package/dist/src/routes/api/auth-status.js +56 -0
  118. package/dist/src/routes/api/diagrams.js +35 -0
  119. package/dist/src/routes/api/directory.js +93 -0
  120. package/dist/src/routes/api/drives.js +15 -0
  121. package/dist/src/routes/api/export.js +218 -0
  122. package/dist/src/routes/api/fileContent.js +286 -0
  123. package/dist/src/routes/api/index.js +33 -0
  124. package/dist/src/routes/api/linkInfo.js +71 -0
  125. package/dist/src/routes/api/linkInfo.test.js +104 -0
  126. package/dist/src/routes/api/middleware.js +117 -0
  127. package/dist/src/routes/api/raw.js +38 -0
  128. package/dist/src/routes/api/runner.js +59 -0
  129. package/dist/src/routes/api/search.js +236 -0
  130. package/dist/src/routes/api/sharing.js +203 -0
  131. package/dist/src/routes/api/status.js +68 -0
  132. package/dist/src/routes/api/status.test.js +62 -0
  133. package/dist/src/routes/auth.js +99 -0
  134. package/dist/src/routes/event.js +77 -0
  135. package/dist/src/routes/event.test.js +206 -0
  136. package/dist/src/routes/health.js +10 -0
  137. package/dist/src/routes/keys.js +129 -0
  138. package/dist/src/routes/path/index.js +17 -0
  139. package/dist/src/routes/static.js +30 -0
  140. package/dist/src/server.js +90 -0
  141. package/dist/src/services/deepShareLinks.js +163 -0
  142. package/dist/src/services/diagramCache.js +104 -0
  143. package/dist/src/services/embeddedDiagrams.js +136 -0
  144. package/dist/src/services/eventLog.js +55 -0
  145. package/dist/src/services/eventLog.test.js +113 -0
  146. package/dist/src/services/eventQueue.js +154 -0
  147. package/dist/src/services/eventQueue.test.js +104 -0
  148. package/dist/src/services/export.js +220 -0
  149. package/dist/src/services/exportCache.js +196 -0
  150. package/dist/src/services/markdown.js +147 -0
  151. package/dist/src/services/mermaid.js +97 -0
  152. package/dist/src/services/plantuml.js +145 -0
  153. package/dist/src/services/puppeteer.js +156 -0
  154. package/dist/src/util/breadcrumbs.js +22 -0
  155. package/dist/src/util/crypto.js +56 -0
  156. package/dist/src/util/crypto.test.js +99 -0
  157. package/dist/src/util/fileDetection.js +66 -0
  158. package/dist/src/util/fileDetection.test.js +89 -0
  159. package/dist/src/util/formatters.js +43 -0
  160. package/dist/src/util/formatters.test.js +83 -0
  161. package/dist/src/util/packageVersion.js +25 -0
  162. package/dist/src/util/platform.js +148 -0
  163. package/dist/src/util/state.js +46 -0
  164. package/dist/vitest.config.js +12 -0
  165. package/favicon.svg +3 -0
  166. package/guides/access-decision-flow.mmd +24 -0
  167. package/guides/access-decision-flow.svg +1 -0
  168. package/guides/api-integration.md +236 -0
  169. package/guides/deployment.md +287 -0
  170. package/guides/event-gateway.md +204 -0
  171. package/guides/event-gateway.mmd +17 -0
  172. package/guides/event-gateway.svg +1 -0
  173. package/guides/exports.md +239 -0
  174. package/guides/setup.md +313 -0
  175. package/guides/sharing.md +204 -0
  176. package/jeeves-server.config.template.json +25 -0
  177. package/package.json +124 -0
  178. package/scripts/download-plantuml.js +70 -0
  179. package/src/auth/google.ts +93 -0
  180. package/src/auth/keys.ts +252 -0
  181. package/src/auth/resolve.ts +157 -0
  182. package/src/auth/session.ts +77 -0
  183. package/src/cli/commands/config.test.ts +107 -0
  184. package/src/cli/commands/config.ts +113 -0
  185. package/src/cli/commands/service.ts +129 -0
  186. package/src/cli/commands/start.ts +27 -0
  187. package/src/cli/index.ts +25 -0
  188. package/src/config/index.ts +113 -0
  189. package/src/config/loadConfig.test.ts +155 -0
  190. package/src/config/resolve.test.ts +192 -0
  191. package/src/config/resolve.ts +173 -0
  192. package/src/config/schema.ts +179 -0
  193. package/src/config/substituteEnvVars.test.ts +64 -0
  194. package/src/config/substituteEnvVars.ts +52 -0
  195. package/src/config/types.ts +129 -0
  196. package/src/routes/api/auth-status.ts +85 -0
  197. package/src/routes/api/diagrams.ts +53 -0
  198. package/src/routes/api/directory.ts +123 -0
  199. package/src/routes/api/drives.ts +23 -0
  200. package/src/routes/api/export.ts +314 -0
  201. package/src/routes/api/fileContent.ts +414 -0
  202. package/src/routes/api/index.ts +37 -0
  203. package/src/routes/api/linkInfo.test.ts +132 -0
  204. package/src/routes/api/linkInfo.ts +83 -0
  205. package/src/routes/api/middleware.ts +156 -0
  206. package/src/routes/api/raw.ts +54 -0
  207. package/src/routes/api/runner.ts +107 -0
  208. package/src/routes/api/search.ts +321 -0
  209. package/src/routes/api/sharing.ts +259 -0
  210. package/src/routes/api/status.test.ts +72 -0
  211. package/src/routes/api/status.ts +82 -0
  212. package/src/routes/auth.ts +143 -0
  213. package/src/routes/event.test.ts +248 -0
  214. package/src/routes/event.ts +109 -0
  215. package/src/routes/health.ts +13 -0
  216. package/src/routes/keys.ts +192 -0
  217. package/src/routes/path/index.ts +24 -0
  218. package/src/routes/static.ts +54 -0
  219. package/src/server.ts +104 -0
  220. package/src/services/deepShareLinks.ts +203 -0
  221. package/src/services/diagramCache.ts +128 -0
  222. package/src/services/embeddedDiagrams.ts +168 -0
  223. package/src/services/eventLog.test.ts +144 -0
  224. package/src/services/eventLog.ts +68 -0
  225. package/src/services/eventQueue.test.ts +127 -0
  226. package/src/services/eventQueue.ts +196 -0
  227. package/src/services/export.ts +267 -0
  228. package/src/services/exportCache.ts +216 -0
  229. package/src/services/markdown.ts +189 -0
  230. package/src/services/mermaid.ts +113 -0
  231. package/src/services/plantuml.ts +172 -0
  232. package/src/services/puppeteer.ts +188 -0
  233. package/src/types/fastify.d.ts +13 -0
  234. package/src/types/jsonmap.d.ts +10 -0
  235. package/src/types/plantuml-encoder.d.ts +4 -0
  236. package/src/util/breadcrumbs.ts +33 -0
  237. package/src/util/crypto.test.ts +132 -0
  238. package/src/util/crypto.ts +79 -0
  239. package/src/util/fileDetection.test.ts +115 -0
  240. package/src/util/fileDetection.ts +70 -0
  241. package/src/util/formatters.test.ts +105 -0
  242. package/src/util/formatters.ts +44 -0
  243. package/src/util/packageVersion.ts +30 -0
  244. package/src/util/platform.ts +178 -0
  245. package/src/util/state.ts +55 -0
  246. package/test-docs/diagram-retry-test.md +18 -0
  247. package/test-docs/embedded-diagrams.md +52 -0
  248. package/test-docs/lazy-diagrams-test.md +333 -0
  249. package/test-docs/page-a.md +7 -0
  250. package/test-docs/page-b.md +7 -0
  251. package/test-docs/page-c.md +7 -0
  252. package/test-docs/sub/page-d.md +7 -0
  253. package/test-docs/test-diagram.puml +13 -0
  254. package/test-docs/validate-deep-share.js +318 -0
  255. package/tsconfig.json +37 -0
  256. package/tsdoc.json +13 -0
  257. package/vendor/.plantuml-version +1 -0
  258. package/vendor/plantuml.jar +0 -0
  259. package/vitest.config.js +12 -0
  260. package/vitest.config.ts +13 -0
@@ -0,0 +1,134 @@
1
+ import { Link as LinkIcon } from 'lucide-react';
2
+
3
+ import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
4
+ import { ActionDropdown, DropdownErrorBanner, type ActionState } from '@/components/ActionDropdown';
5
+ import { useActionState } from '@/components/useActionState';
6
+ import { getShareLink, type ShareSettings } from '@/lib/api';
7
+
8
+ interface LinkDropdownProps {
9
+ path: string;
10
+ shareSettings: ShareSettings;
11
+ onShareSettingsChange: (settings: ShareSettings) => void;
12
+ showEvent?: boolean;
13
+ showRaw?: boolean;
14
+ compact?: boolean;
15
+ isDirectory?: boolean;
16
+ variant?: 'header' | 'default' | 'menuItem';
17
+ onError?: (error: string) => void;
18
+ onStateChange?: (state: ActionState) => void;
19
+ }
20
+
21
+ type LinkType = 'page' | 'raw' | 'event';
22
+
23
+ async function copyShareLink(path: string, settings: ShareSettings, type: LinkType, isDirectory?: boolean) {
24
+ let expiryParam: string | undefined;
25
+ if (settings.expiry) {
26
+ const match = settings.expiry.match(/^(\d+)([hdw])$/i);
27
+ if (match) {
28
+ const val = parseInt(match[1], 10);
29
+ const unit = match[2].toLowerCase();
30
+ const multiplier: Record<string, number> = { h: 3_600_000, d: 86_400_000, w: 604_800_000 };
31
+ expiryParam = String(Date.now() + val * multiplier[unit]);
32
+ }
33
+ }
34
+
35
+ const depth = !isDirectory && settings.depth > 0 ? settings.depth : undefined;
36
+ const dirs = !isDirectory && settings.dirs ? true : undefined;
37
+
38
+ const data = await getShareLink(path, expiryParam, depth, dirs);
39
+ if (!data.url) throw new Error('No URL returned');
40
+
41
+ let fullUrl = window.location.origin + data.url;
42
+ if (type === 'raw') {
43
+ const shareUrl = new URL(fullUrl);
44
+ shareUrl.pathname = shareUrl.pathname.replace('/browse/', '/api/raw/');
45
+ fullUrl = shareUrl.toString();
46
+ } else if (type === 'event') {
47
+ fullUrl = window.location.origin + '/event?key=' + data.url.split('key=')[1];
48
+ }
49
+ await navigator.clipboard.writeText(fullUrl);
50
+ }
51
+
52
+ const EXPIRY_OPTIONS = [
53
+ { label: 'Never', value: '' },
54
+ { label: '1 hour', value: '1h' },
55
+ { label: '24 hours', value: '24h' },
56
+ { label: '7 days', value: '7d' },
57
+ { label: '30 days', value: '30d' },
58
+ ];
59
+
60
+ export function LinkDropdown({ path, shareSettings, onShareSettingsChange, showEvent, showRaw, compact, isDirectory, variant = 'default', onError, onStateChange }: LinkDropdownProps) {
61
+ const { state, errorMsg, handleAction, resetOnClose } = useActionState(onError, onStateChange);
62
+
63
+ const items: { label: string; type: LinkType }[] = [{ label: 'Page', type: 'page' }];
64
+ if (showRaw) items.push({ label: 'Raw', type: 'raw' });
65
+ if (showEvent) items.push({ label: 'Event', type: 'event' });
66
+
67
+ return (
68
+ <ActionDropdown
69
+ icon={LinkIcon}
70
+ label="Share"
71
+ title="Copy share link"
72
+ variant={variant}
73
+ compact={compact}
74
+ state={state}
75
+ contentClass="w-52"
76
+ onOpenChange={resetOnClose}
77
+ errorSlot={<DropdownErrorBanner message={errorMsg} />}
78
+ >
79
+ {items.map((item) => (
80
+ <DropdownMenuItem
81
+ key={item.type}
82
+ onSelect={() => void handleAction(() => copyShareLink(path, shareSettings, item.type, isDirectory))}
83
+ className="cursor-pointer"
84
+ >
85
+ Copy {item.label} Link
86
+ </DropdownMenuItem>
87
+ ))}
88
+
89
+ <DropdownMenuSeparator />
90
+
91
+ <div className="px-2 py-1 flex items-center justify-between gap-2">
92
+ <span className="text-xs text-muted-foreground whitespace-nowrap">Expires</span>
93
+ <select
94
+ className="text-xs bg-popover text-popover-foreground border border-border rounded px-1 py-0.5 focus:outline-none focus:ring-1 focus:ring-ring min-w-0"
95
+ value={shareSettings.expiry}
96
+ onChange={(e) => onShareSettingsChange({ ...shareSettings, expiry: e.target.value })}
97
+ onClick={(e) => e.stopPropagation()}
98
+ >
99
+ {EXPIRY_OPTIONS.map((opt) => (
100
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
101
+ ))}
102
+ </select>
103
+ </div>
104
+
105
+ {!isDirectory && (
106
+ <>
107
+ <div className="px-2 py-1 flex items-center justify-between gap-2">
108
+ <span className="text-xs text-muted-foreground whitespace-nowrap">Depth</span>
109
+ <input
110
+ type="number"
111
+ min={0}
112
+ max={10}
113
+ className="text-xs bg-popover text-popover-foreground border border-border rounded px-1 py-0.5 w-14 text-right focus:outline-none focus:ring-1 focus:ring-ring"
114
+ value={shareSettings.depth}
115
+ onChange={(e) => onShareSettingsChange({ ...shareSettings, depth: Math.max(0, parseInt(e.target.value, 10) || 0) })}
116
+ onClick={(e) => e.stopPropagation()}
117
+ />
118
+ </div>
119
+
120
+ <div className="px-2 py-1 flex items-center justify-between gap-2">
121
+ <span className="text-xs text-muted-foreground whitespace-nowrap">Directories</span>
122
+ <input
123
+ type="checkbox"
124
+ className="rounded border-border"
125
+ checked={shareSettings.dirs}
126
+ onChange={(e) => onShareSettingsChange({ ...shareSettings, dirs: e.target.checked })}
127
+ onClick={(e) => e.stopPropagation()}
128
+ />
129
+ </div>
130
+ </>
131
+ )}
132
+ </ActionDropdown>
133
+ );
134
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Markdown rendered view with TOC sidebar.
3
+ */
4
+ import type { FileContent } from '@/lib/api';
5
+ import { initEmbeddedDiagramPanzoom } from '@/components/EmbeddedDiagramPanzoom';
6
+ import { initLazyDiagrams } from '@/components/LazyDiagram';
7
+ import { initInlineSvgPanzoom } from '@/components/InlineSvgPanzoom';
8
+ import { initCodeBlockCm6 } from '@/lib/codeBlockCm6';
9
+ import { injectCopyButtons } from '@/lib/codeBlockCopy';
10
+ import { useTheme } from '@/lib/theme';
11
+ import { scrollToIdInContainer } from './scrollUtils';
12
+
13
+ interface MarkdownViewProps {
14
+ fileRendered: FileContent;
15
+ proseWidth: 'narrow' | 'medium' | 'wide';
16
+ topBarHeight: number;
17
+ mainRef: React.RefObject<HTMLElement | null>;
18
+ mobileTocOpen: boolean;
19
+ setMobileTocOpen: (open: boolean) => void;
20
+ }
21
+
22
+ export function MarkdownView({
23
+ fileRendered, proseWidth, topBarHeight, mainRef,
24
+ mobileTocOpen, setMobileTocOpen,
25
+ }: MarkdownViewProps) {
26
+ const [theme] = useTheme();
27
+ const plainCode = new URLSearchParams(window.location.search).has('plain_code');
28
+ const hasHeadings = fileRendered.headings && fileRendered.headings.length > 2;
29
+
30
+ return (
31
+ <>
32
+ {/* Mobile TOC overlay */}
33
+ {mobileTocOpen && hasHeadings && (
34
+ <>
35
+ <div className="lg:hidden fixed inset-0 z-40" onClick={() => setMobileTocOpen(false)} />
36
+ <div
37
+ className="lg:hidden fixed left-2 right-2 z-50 bg-popover text-popover-foreground border border-border rounded-lg shadow-lg max-h-[60vh] overflow-y-auto px-4 py-3"
38
+ style={{ top: `${topBarHeight + 4}px` }}
39
+ >
40
+ <nav>
41
+ {fileRendered.headings!.map((h) => (
42
+ <button
43
+ key={h.slug}
44
+ type="button"
45
+ onClick={() => { scrollToIdInContainer(mainRef.current, h.slug); setMobileTocOpen(false); }}
46
+ className="block text-left text-sm text-muted-foreground hover:text-foreground cursor-pointer py-1 transition-colors w-full"
47
+ style={{ paddingLeft: `${(h.level - 1) * 0.75}rem` }}
48
+ >
49
+ {h.text}
50
+ </button>
51
+ ))}
52
+ </nav>
53
+ </div>
54
+ </>
55
+ )}
56
+
57
+ <div className="flex gap-6">
58
+ {/* Desktop TOC sidebar */}
59
+ {hasHeadings && (
60
+ <aside
61
+ className="toc-sidebar hidden lg:block w-56 shrink-0"
62
+ style={{ maxHeight: `calc(100vh - ${topBarHeight + 32}px)` }}
63
+ >
64
+ <div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">Contents</div>
65
+ <nav className="border-l border-border pl-3">
66
+ {fileRendered.headings!.map((h) => (
67
+ <button
68
+ key={h.slug}
69
+ type="button"
70
+ onClick={() => scrollToIdInContainer(mainRef.current, h.slug)}
71
+ className="block text-left text-sm text-muted-foreground hover:text-foreground cursor-pointer py-0.5 transition-colors"
72
+ style={{ paddingLeft: `${(h.level - 1) * 0.75}rem` }}
73
+ >
74
+ {h.text}
75
+ </button>
76
+ ))}
77
+ </nav>
78
+ </aside>
79
+ )}
80
+
81
+ {/* Markdown article */}
82
+ <article
83
+ ref={(el) => { if (el) { if (!plainCode) initCodeBlockCm6(el, theme); injectCopyButtons(el); initInlineSvgPanzoom(el); initEmbeddedDiagramPanzoom(el); initLazyDiagrams(el); } }}
84
+ className={`prose bg-background p-6 rounded-lg border border-border min-w-0 flex-1 ${
85
+ proseWidth === 'narrow' ? 'max-w-prose' : proseWidth === 'medium' ? 'max-w-5xl' : 'max-w-none'
86
+ }`}
87
+ style={{
88
+ '--tw-prose-body': 'var(--foreground)',
89
+ '--tw-prose-headings': 'var(--foreground)',
90
+ '--tw-prose-bold': 'var(--foreground)',
91
+ '--tw-prose-links': '#3b82f6',
92
+ '--tw-prose-code': 'var(--foreground)',
93
+ '--tw-prose-pre-bg': 'var(--muted)',
94
+ '--tw-prose-pre-code': 'var(--foreground)',
95
+ '--tw-prose-hr': 'var(--border)',
96
+ '--tw-prose-quotes': 'var(--muted-foreground)',
97
+ '--tw-prose-quote-borders': 'var(--border)',
98
+ '--tw-prose-th-borders': 'var(--border)',
99
+ '--tw-prose-td-borders': 'var(--border)',
100
+ } as React.CSSProperties}
101
+ dangerouslySetInnerHTML={{ __html: fileRendered.html! }}
102
+ onClick={(e) => {
103
+ const target = e.target as HTMLElement;
104
+ const anchor = target.closest('a');
105
+ const href = anchor?.getAttribute('href');
106
+ if (href?.startsWith('#')) {
107
+ e.preventDefault();
108
+ scrollToIdInContainer(mainRef.current, href.slice(1));
109
+ }
110
+ }}
111
+ />
112
+ </div>
113
+ </>
114
+ );
115
+ }
@@ -0,0 +1,21 @@
1
+ import { SvgViewer } from './SvgViewer';
2
+
3
+ interface MermaidViewerProps {
4
+ /** Server-rendered SVG string */
5
+ html: string | null;
6
+ /** Raw mermaid source (fallback) */
7
+ content: string;
8
+ }
9
+
10
+ export function MermaidViewer({ html, content }: MermaidViewerProps) {
11
+ if (!html) {
12
+ return (
13
+ <div className="p-4 bg-red-950/20 border border-red-800 rounded-lg">
14
+ <div className="text-red-400 text-sm font-medium mb-2">Mermaid render failed</div>
15
+ <pre className="text-red-300 text-xs overflow-x-auto">{content}</pre>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return <SvgViewer content={html} />;
21
+ }
@@ -0,0 +1,21 @@
1
+ import { SvgViewer } from './SvgViewer';
2
+
3
+ interface PlantUmlViewerProps {
4
+ /** Server-rendered SVG string */
5
+ html: string | null;
6
+ /** Raw PlantUML source (fallback) */
7
+ content: string;
8
+ }
9
+
10
+ export function PlantUmlViewer({ html, content }: PlantUmlViewerProps) {
11
+ if (!html) {
12
+ return (
13
+ <div className="p-4 bg-red-950/20 border border-red-800 rounded-lg">
14
+ <div className="text-red-400 text-sm font-medium mb-2">PlantUML render failed</div>
15
+ <pre className="text-red-300 text-xs overflow-x-auto">{content}</pre>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ return <SvgViewer content={html} />;
21
+ }