@clidey/dory 1.0.5 → 1.0.7

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 (63) hide show
  1. package/bin/dist/dory.js +36 -2
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +0 -1
  4. package/package.json +16 -22
  5. package/src/components/hooks.tsx +2 -1
  6. package/src/components/store.ts +5 -4
  7. package/src/config/mdx.js +8 -3
  8. package/src/config/mdx.ts +8 -3
  9. package/src/embed/widget-iframe.ts +10 -3
  10. package/src/entry-server.tsx +11 -7
  11. package/src/main.tsx +5 -1
  12. package/src/mdx/accordion.tsx +11 -8
  13. package/src/mdx/api.tsx +2 -352
  14. package/src/mdx/async-api.tsx +124 -295
  15. package/src/mdx/callout.tsx +22 -24
  16. package/src/mdx/card.tsx +20 -22
  17. package/src/mdx/chip.tsx +14 -0
  18. package/src/mdx/code.tsx +133 -110
  19. package/src/mdx/expandable.tsx +8 -8
  20. package/src/mdx/fence-meta.test.ts +39 -0
  21. package/src/mdx/fence-meta.ts +61 -0
  22. package/src/mdx/fence.tsx +155 -44
  23. package/src/mdx/icon-grid.tsx +29 -0
  24. package/src/mdx/icon.tsx +2 -2
  25. package/src/mdx/latex.tsx +14 -7
  26. package/src/mdx/list.tsx +3 -3
  27. package/src/mdx/mdx.tsx +7 -1
  28. package/src/mdx/mermaid.tsx +81 -11
  29. package/src/mdx/open-api.tsx +396 -239
  30. package/src/mdx/source.tsx +148 -40
  31. package/src/mdx/specimen.tsx +69 -0
  32. package/src/mdx/step.tsx +22 -30
  33. package/src/mdx/table.tsx +4 -9
  34. package/src/mdx/tabs.tsx +39 -0
  35. package/src/mdx/tag.tsx +13 -21
  36. package/src/mdx/text.tsx +17 -9
  37. package/src/mdx/type-scale.tsx +28 -0
  38. package/src/mdx/websocket-playground.tsx +221 -704
  39. package/src/plugins/code-meta.ts +23 -0
  40. package/src/plugins/frontmatter-dev-server.ts +6 -2
  41. package/src/plugins/html-files-middleware.ts +9 -2
  42. package/src/plugins/html-metadata-injector.ts +4 -3
  43. package/src/plugins/llm-txt-dev-server.ts +5 -2
  44. package/src/plugins/llm-txt-generator.ts +2 -1
  45. package/src/plugins/prerender.ts +7 -4
  46. package/src/plugins/relative-assets.test.ts +26 -0
  47. package/src/plugins/relative-assets.ts +80 -0
  48. package/src/plugins/robots-generator.ts +6 -4
  49. package/src/plugins/sanitize.test.ts +5 -0
  50. package/src/plugins/sanitize.ts +2 -0
  51. package/src/plugins/shared.ts +17 -0
  52. package/src/plugins/sitemap-generator.ts +2 -1
  53. package/src/theme.scss +282 -3
  54. package/src/types/config.ts +3 -0
  55. package/src/ui/header.tsx +4 -3
  56. package/src/ui/layout.tsx +22 -4
  57. package/src/ui/page-meta.tsx +37 -0
  58. package/src/ui/prev-next-link.tsx +4 -3
  59. package/src/ui/table-of-content.tsx +11 -3
  60. package/src/utils/base-path.test.ts +46 -0
  61. package/src/utils/base-path.ts +41 -0
  62. package/vite.config.ssr.ts +3 -0
  63. package/vite.config.ts +3 -0
package/src/mdx/fence.tsx CHANGED
@@ -1,12 +1,24 @@
1
- import { Fragment, Suspense, useContext } from 'preact/compat'
2
- import { Highlight, themes } from 'prism-react-renderer'
3
- import { Code, CodeGroup, CodeGroupContext, supportedLanguages } from './code'
1
+ import { Suspense, useContext } from 'preact/compat'
2
+ import { Highlight, type PrismTheme } from 'prism-react-renderer'
3
+ import { CodeBody, CodeFrame, CodeGroupContext, FenceHeader, fencePropsOfCode } from './code'
4
+ import { diffBaseLanguage, isTerminalSession, parseFenceMeta, type FenceMeta } from './fence-meta'
4
5
  import { MermaidRenderer } from './mermaid'
5
6
  import { Loading } from '../components/loading'
6
- import { useTheme } from '@clidey/ux'
7
7
  import classNames from 'classnames'
8
8
 
9
- function dedent(code: string): string {
9
+ // Colors resolve through CSS variables so one theme serves both light and dark mode.
10
+ const codeTheme: PrismTheme = {
11
+ plain: { color: 'var(--foreground)', backgroundColor: 'transparent' },
12
+ styles: [
13
+ { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--muted-foreground)', fontStyle: 'italic' } },
14
+ { types: ['keyword', 'builtin', 'function', 'tag', 'selector', 'number', 'boolean', 'constant', 'property', 'class-name'], style: { color: 'var(--primary)' } },
15
+ { types: ['string', 'char', 'attr-value', 'url', 'regex', 'inserted'], style: { color: 'var(--dory-code-string)' } },
16
+ { types: ['deleted'], style: { color: 'var(--destructive)' } },
17
+ { types: ['punctuation', 'operator'], style: { color: 'var(--foreground)' } },
18
+ ],
19
+ }
20
+
21
+ export function dedent(code: string): string {
10
22
  const lines = code.replace(/^\n+/, '').replace(/\s+$/, '').split('\n');
11
23
  const indents = lines.filter(l => l.trim().length > 0).map(l => l.match(/^(\s*)/)?.[1].length ?? 0);
12
24
  const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
@@ -14,17 +26,107 @@ function dedent(code: string): string {
14
26
  return lines.map(l => l.slice(minIndent)).join('\n');
15
27
  }
16
28
 
29
+ function Line({
30
+ number,
31
+ marker,
32
+ highlighted,
33
+ children,
34
+ }: {
35
+ number?: number
36
+ marker?: '+' | '-' | ' '
37
+ highlighted?: boolean
38
+ children: preact.ComponentChildren
39
+ }) {
40
+ return (
41
+ <span className={classNames('dory-line', {
42
+ 'dory-line-highlight': highlighted,
43
+ 'dory-line-add': marker === '+',
44
+ 'dory-line-del': marker === '-',
45
+ })}>
46
+ {number !== undefined && <span className="dory-line-number" aria-hidden="true">{number}</span>}
47
+ {marker !== undefined && <span className="dory-line-marker" aria-hidden="true">{marker === ' ' ? '' : marker}</span>}
48
+ {children}
49
+ {'\n'}
50
+ </span>
51
+ )
52
+ }
53
+
54
+ function TerminalLines({ code }: { code: string }) {
55
+ return (
56
+ <>
57
+ {code.split('\n').map((line, index) => {
58
+ if (!line.startsWith('$ ')) {
59
+ return <Line key={index}><span className="text-muted-foreground">{line}</span></Line>
60
+ }
61
+ const [command, ...args] = line.slice(2).split(' ')
62
+ return (
63
+ <Line key={index}>
64
+ <span className="select-none text-muted-foreground">$ </span>
65
+ <span className="text-[var(--primary)]">{command}</span>
66
+ {args.length > 0 && ` ${args.join(' ')}`}
67
+ </Line>
68
+ )
69
+ })}
70
+ </>
71
+ )
72
+ }
73
+
74
+ function HighlightedLines({
75
+ code,
76
+ language,
77
+ lineNumbers,
78
+ highlight,
79
+ markers,
80
+ startLine = 1,
81
+ }: {
82
+ code: string
83
+ language: string
84
+ lineNumbers: boolean
85
+ highlight: Set<number>
86
+ markers?: Array<'+' | '-' | ' '>
87
+ startLine?: number
88
+ }) {
89
+ return (
90
+ <Highlight code={code} language={language} theme={codeTheme}>
91
+ {({ tokens, getTokenProps }) => (
92
+ <>
93
+ {tokens.map((line, index) => (
94
+ <Line
95
+ key={index}
96
+ number={lineNumbers ? startLine + index : undefined}
97
+ marker={markers?.[index]}
98
+ highlighted={highlight.has(index + 1)}
99
+ >
100
+ {line
101
+ .filter((token) => !token.empty)
102
+ .map((token, tokenIndex) => (
103
+ <span key={tokenIndex} {...getTokenProps({ token })} />
104
+ ))}
105
+ </Line>
106
+ ))}
107
+ </>
108
+ )}
109
+ </Highlight>
110
+ )
111
+ }
17
112
 
113
+ /**
114
+ * Renders a code fence. Diffs (`diff`, `diff-<lang>`) get per-line add/remove
115
+ * markers, shell fences with `$ ` prompts render as a terminal session, and
116
+ * fences with a filename or `{lines}` get line numbers and highlighted lines.
117
+ */
18
118
  export function Fence({
19
119
  children,
20
120
  language,
121
+ meta = parseFenceMeta(),
122
+ startLine,
21
123
  }: {
22
124
  children: string
23
125
  language: string
126
+ meta?: FenceMeta
127
+ /** First line number to show; forces line numbers on. */
128
+ startLine?: number
24
129
  }) {
25
- const { theme } = useTheme();
26
- const isDark = theme === 'dark' || (theme === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches);
27
-
28
130
  if (language === 'mermaid') {
29
131
  return (
30
132
  <div className="mermaid-container">
@@ -35,49 +137,58 @@ export function Fence({
35
137
  )
36
138
  }
37
139
 
38
- const supportedLanguage = supportedLanguages[language as keyof typeof supportedLanguages] ?? language
140
+ const code = dedent(children)
141
+ const diffLanguage = diffBaseLanguage(language)
142
+
143
+ let body: preact.ComponentChildren
144
+ if (diffLanguage !== null) {
145
+ const lines = code.split('\n')
146
+ const markers = lines.map((line) => (line[0] === '+' || line[0] === '-' ? line[0] : ' '))
147
+ body = (
148
+ <HighlightedLines
149
+ code={lines.map((line) => line.slice(1)).join('\n')}
150
+ language={diffLanguage}
151
+ lineNumbers={false}
152
+ highlight={meta.highlight}
153
+ markers={markers}
154
+ />
155
+ )
156
+ } else if (isTerminalSession(language, code)) {
157
+ body = <TerminalLines code={code} />
158
+ } else {
159
+ body = (
160
+ <HighlightedLines
161
+ code={code}
162
+ language={language}
163
+ lineNumbers={Boolean(meta.title) || meta.highlight.size > 0 || startLine !== undefined}
164
+ highlight={meta.highlight}
165
+ startLine={startLine}
166
+ />
167
+ )
168
+ }
39
169
 
40
170
  return (
41
- <Highlight
42
- code={dedent(children)}
43
- language={supportedLanguage}
44
- theme={isDark ? themes.vsDark : themes.vsLight}>
45
- {({ className, style, tokens, getTokenProps }) => (
46
- <pre>
47
- <Code className={classNames(className, "whitespace-pre-wrap")} style={{ ...style, backgroundColor: 'transparent' }} title={supportedLanguage}>
48
- {tokens.map((line, lineIndex) => (
49
- <Fragment key={lineIndex}>
50
- {line
51
- .filter((token) => !token.empty)
52
- .map((token, tokenIndex) => (
53
- <span key={tokenIndex} {...getTokenProps({ token })} />
54
- ))}
55
- {'\n'}
56
- </Fragment>
57
- ))}
58
- </Code>
59
- </pre>
60
- )}
61
- </Highlight>
171
+ <pre className="whitespace-pre-wrap">
172
+ <code className={`language-${language}`}>{body}</code>
173
+ </pre>
62
174
  )
63
175
  }
64
- export function Pre(props: { children: { props: { className?: string; children?: string } } }) {
65
- const child = props.children?.props
66
-
67
- // Safely handle className that might not be a string
68
- const className = child?.className || ''
69
- const language = supportedLanguages[className.replace('language-', '') as keyof typeof supportedLanguages] ?? className.replace('language-', '')
70
- const code = child?.children || ''
71
176
 
72
- // Check if we're inside a CodeGroup context
177
+ export function Pre(props: { children: preact.JSX.Element }) {
178
+ const fence = fencePropsOfCode(props.children)
73
179
  const isInCodeGroup = useContext(CodeGroupContext);
74
180
 
75
- const fence = <Fence language={language}>{code}</Fence>
181
+ // Inside a CodeGroup the title is the tab label, not a filename, so it must not turn on line numbers.
182
+ const meta = isInCodeGroup ? { ...fence.meta, title: undefined } : fence.meta
183
+ const rendered = <Fence language={fence.language} meta={meta}>{fence.code}</Fence>
76
184
 
77
- // If we're not in a CodeGroup, wrap the Fence in one
78
- if (!isInCodeGroup && language !== 'mermaid') {
79
- return <CodeGroup title={language}>{fence}</CodeGroup>
185
+ if (isInCodeGroup || fence.language === 'mermaid') {
186
+ return rendered
80
187
  }
81
188
 
82
- return fence
83
- }
189
+ return (
190
+ <CodeFrame code={fence.code} header={<FenceHeader {...fence} />}>
191
+ <CodeBody>{rendered}</CodeBody>
192
+ </CodeFrame>
193
+ )
194
+ }
@@ -0,0 +1,29 @@
1
+ import { useNotification } from '../components/notification'
2
+ import { Icon } from './icon'
3
+
4
+ /** Grid of icon tiles. Clicking a tile copies its `<Icon icon="…" />` tag. */
5
+ export function IconGrid({ icons, cols = 4 }: { icons: string[]; cols?: number }) {
6
+ const { showNotification } = useNotification()
7
+
8
+ const copy = (name: string) => {
9
+ const tag = `<Icon icon="${name}" />`
10
+ navigator.clipboard.writeText(tag).then(() => showNotification(`Copied ${tag}`))
11
+ }
12
+
13
+ return (
14
+ <div className="not-prose my-2 grid gap-3" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
15
+ {icons.map((name) => (
16
+ <button
17
+ key={name}
18
+ type="button"
19
+ onClick={() => copy(name)}
20
+ title={`Copy <Icon icon="${name}" />`}
21
+ className="flex flex-col items-center gap-2.5 rounded-lg border border-[var(--border)] bg-muted/50 px-3 py-4 text-foreground transition hover:border-[var(--primary)] hover:bg-card"
22
+ >
23
+ <Icon icon={name} className="h-5 w-5 !stroke-current" />
24
+ <span className="font-mono text-[11px] text-muted-foreground">{name}</span>
25
+ </button>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
package/src/mdx/icon.tsx CHANGED
@@ -42,7 +42,7 @@ export function Icon({
42
42
  if (LucideComponent) {
43
43
  return (
44
44
  <LucideComponent
45
- className={classNames("stroke-black dark:stroke-white", className)}
45
+ className={classNames("inline-block align-[-0.2em] stroke-black dark:stroke-white", className)}
46
46
  color={color}
47
47
  size={size}
48
48
  {...rest}
@@ -66,7 +66,7 @@ export function Icon({
66
66
  return (
67
67
  <FontAwesomeIcon
68
68
  icon={[prefixes[setName] || 'fas', faIcon.iconName] as any}
69
- className={classNames("stroke-black dark:stroke-white", className)}
69
+ className={classNames("inline-block align-[-0.2em] stroke-black dark:stroke-white", className)}
70
70
  color={color as any}
71
71
  size={size as any}
72
72
  {...rest as any}
package/src/mdx/latex.tsx CHANGED
@@ -12,13 +12,15 @@ function extractText(node: React.ReactNode): string {
12
12
 
13
13
  interface LatexProps {
14
14
  children: React.ReactNode
15
+ /** Render inline with the surrounding text instead of as a display tile. */
16
+ inline?: boolean
15
17
  }
16
18
 
17
- export function Latex({ children }: LatexProps) {
19
+ export function Latex({ children, inline = false }: LatexProps) {
18
20
  const expression = extractText(children).trim();
19
21
 
20
22
  if (typeof window === 'undefined') {
21
- return <div className="my-4"><code>{expression}</code></div>
23
+ return inline ? <code>{expression}</code> : <div className="my-4"><code>{expression}</code></div>
22
24
  }
23
25
 
24
26
  const containerRef = useRef<HTMLDivElement>(null)
@@ -42,7 +44,7 @@ export function Latex({ children }: LatexProps) {
42
44
  if (!mounted || !containerRef.current) return
43
45
 
44
46
  katex.render(expression, containerRef.current, {
45
- displayMode: true,
47
+ displayMode: !inline,
46
48
  throwOnError: false,
47
49
  })
48
50
  setIsLoading(false)
@@ -60,7 +62,11 @@ export function Latex({ children }: LatexProps) {
60
62
  return () => {
61
63
  mounted = false
62
64
  }
63
- }, [expression])
65
+ }, [expression, inline])
66
+
67
+ if (inline) {
68
+ return <span ref={containerRef as unknown as preact.RefObject<HTMLSpanElement>} className="dory-latex-inline">{isLoading && expression}</span>
69
+ }
64
70
 
65
71
  if (error) {
66
72
  return (
@@ -86,13 +92,14 @@ export function Latex({ children }: LatexProps) {
86
92
  }
87
93
 
88
94
  return (
89
- <div className="my-4">
95
+ <div className="dory-latex not-prose my-5 rounded-lg border border-[var(--border)] bg-muted/50 px-5 pb-5 pt-3">
96
+ <span className="block font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">Display</span>
90
97
  {isLoading && (
91
98
  <div className="flex items-center justify-center py-4">
92
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500" />
99
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[var(--primary)]" />
93
100
  </div>
94
101
  )}
95
- <div ref={containerRef} className={isLoading ? 'hidden' : ''} />
102
+ <div ref={containerRef} className={isLoading ? 'hidden' : 'mt-3 text-center'} />
96
103
  </div>
97
104
  )
98
105
  }
package/src/mdx/list.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { JSX } from 'preact/jsx-runtime';
2
2
 
3
- export const UnorderedList = (props: JSX.IntrinsicElements['ul']) => <ul className="list-disc list-inside my-2" {...props} />;
3
+ export const UnorderedList = (props: JSX.IntrinsicElements['ul']) => <ul className="dory-ul my-3 space-y-1.5" {...props} />;
4
4
 
5
- export const OrderedList = (props: JSX.IntrinsicElements['ol']) => <ol className="list-decimal list-inside my-2" {...props} />;
5
+ export const OrderedList = (props: JSX.IntrinsicElements['ol']) => <ol className="dory-ol my-3 space-y-1.5" {...props} />;
6
6
 
7
- export const ListItem = (props: JSX.IntrinsicElements['li']) => <li className="ml-4 mt-2" {...props} />;
7
+ export const ListItem = (props: JSX.IntrinsicElements['li']) => <li className="relative pl-7" {...props} />;
package/src/mdx/mdx.tsx CHANGED
@@ -19,8 +19,14 @@ export { ResponseField, Properties, Property } from './open-api';
19
19
  export { Steps, Step } from './step';
20
20
  export { Table as table, Th as th, Td as td } from './table';
21
21
  export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
22
+ export { UnorderedList, OrderedList, ListItem } from './list';
22
23
  export { AsyncAPI } from './async-api';
23
24
  export { Source } from './source';
25
+ export { Specimen, SpecimenProps } from './specimen';
26
+ export { Chip, ChipGroup } from './chip';
27
+ export { Tabs, Tab } from './tabs';
28
+ export { IconGrid } from './icon-grid';
29
+ export { TypeScale } from './type-scale';
24
30
 
25
31
  // Lazy-loaded heavy components — only downloaded when a page uses them.
26
32
  // SSR guard at this level prevents Suspense from rendering in Node.js
@@ -65,6 +71,6 @@ export const wrapper = ({ children }: { children: preact.ComponentChildren }) =>
65
71
  };
66
72
 
67
73
 
68
- export const hr = () => <hr className="my-4 text-muted-foreground/25 dark:text-gray-200/25" />;
74
+ export const hr = () => <hr className="my-6 border-0 border-t border-[var(--border)]" />;
69
75
 
70
76
  export const br = () => <br />;
@@ -25,14 +25,90 @@ function hashContent(content: string): string {
25
25
  return hash.toString(16);
26
26
  }
27
27
 
28
+ /** Reads the site palette so diagrams follow dory.json colors in both themes. */
29
+ function paletteThemeVariables() {
30
+ const css = getComputedStyle(document.documentElement)
31
+ const v = (name: string) => css.getPropertyValue(name).trim()
32
+ return {
33
+ fontFamily: v('--font-body') || 'inherit',
34
+ fontSize: '13px',
35
+ background: v('--card'),
36
+ mainBkg: v('--accent'),
37
+ primaryColor: v('--accent'),
38
+ primaryTextColor: v('--foreground'),
39
+ primaryBorderColor: v('--primary'),
40
+ secondaryColor: v('--muted'),
41
+ secondaryTextColor: v('--foreground'),
42
+ secondaryBorderColor: v('--border'),
43
+ tertiaryColor: v('--card'),
44
+ tertiaryTextColor: v('--foreground'),
45
+ tertiaryBorderColor: v('--border'),
46
+ lineColor: v('--muted-foreground'),
47
+ textColor: v('--foreground'),
48
+ titleColor: v('--foreground'),
49
+ nodeBorder: v('--primary'),
50
+ nodeTextColor: v('--foreground'),
51
+ clusterBkg: v('--muted'),
52
+ clusterBorder: v('--border'),
53
+ edgeLabelBackground: v('--card'),
54
+ actorBkg: v('--accent'),
55
+ actorBorder: v('--primary'),
56
+ actorTextColor: v('--foreground'),
57
+ actorLineColor: v('--border'),
58
+ signalColor: v('--foreground'),
59
+ signalTextColor: v('--foreground'),
60
+ labelBoxBkgColor: v('--muted'),
61
+ labelBoxBorderColor: v('--border'),
62
+ noteBkgColor: v('--muted'),
63
+ noteBorderColor: v('--border'),
64
+ noteTextColor: v('--foreground'),
65
+ attributeBackgroundColorOdd: v('--card'),
66
+ attributeBackgroundColorEven: v('--muted'),
67
+ pie1: v('--primary'),
68
+ pie2: v('--highlight'),
69
+ pie3: v('--success-muted'),
70
+ pie4: v('--accent'),
71
+ pie5: v('--muted'),
72
+ pieStrokeColor: v('--card'),
73
+ pieTitleTextColor: v('--foreground'),
74
+ pieLegendTextColor: v('--foreground'),
75
+ pieSectionTextColor: v('--foreground'),
76
+ git0: v('--primary'),
77
+ git1: v('--brand-readable'),
78
+ git2: v('--highlight'),
79
+ git3: v('--muted-foreground'),
80
+ gitBranchLabel0: v('--card'),
81
+ gitBranchLabel1: v('--card'),
82
+ commitLabelColor: v('--foreground'),
83
+ commitLabelBackground: v('--muted'),
84
+ tagLabelBackground: v('--accent'),
85
+ tagLabelBorder: v('--primary'),
86
+ tagLabelColor: v('--foreground'),
87
+ nodePadding: 16,
88
+ nodeBorderRadius: 6,
89
+ }
90
+ }
91
+
92
+ /** Bumps whenever the html class list changes, so diagrams re-render on theme switch. */
93
+ function useThemeVersion() {
94
+ const [version, setVersion] = useState(0)
95
+ useEffect(() => {
96
+ const observer = new MutationObserver(() => setVersion((n) => n + 1))
97
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
98
+ return () => observer.disconnect()
99
+ }, [])
100
+ return version
101
+ }
102
+
28
103
  export function MermaidRenderer({ content }: { content: string }) {
104
+ const themeVersion = useThemeVersion()
29
105
  const mermaidRef = useRef<HTMLDivElement>(null)
30
106
  const [isFullscreen, setIsFullscreen] = useState(false)
31
107
  const [isLoaded, setIsLoaded] = useState(false)
32
108
  const [error, setError] = useState<string | null>(null)
33
109
  const [isValid, setIsValid] = useState<boolean | null>(null)
34
110
 
35
- const diagramId = useMemo(() => `mermaid-${hashContent(content)}`, [content])
111
+ const diagramId = useMemo(() => `mermaid-${hashContent(content)}-${themeVersion}`, [content, themeVersion])
36
112
 
37
113
  useEffect(() => {
38
114
  let mounted = true
@@ -44,15 +120,9 @@ export function MermaidRenderer({ content }: { content: string }) {
44
120
  if (!mounted || !mermaidRef.current) return
45
121
 
46
122
  mermaid.initialize({
47
- startOnLoad: true,
123
+ startOnLoad: false,
48
124
  theme: 'base',
49
- themeVariables: {
50
- fontSize: '14px',
51
- nodePadding: 20,
52
- notePadding: 15,
53
- padding: 10,
54
- nodeBorderRadius: 8,
55
- },
125
+ themeVariables: paletteThemeVariables(),
56
126
  flowchart: {
57
127
  nodeSpacing: 50,
58
128
  rankSpacing: 80,
@@ -105,7 +175,7 @@ export function MermaidRenderer({ content }: { content: string }) {
105
175
  return () => {
106
176
  mounted = false
107
177
  }
108
- }, [content, diagramId])
178
+ }, [content, diagramId, themeVersion])
109
179
 
110
180
  const toggleFullscreen = () => {
111
181
  setIsFullscreen(!isFullscreen)
@@ -144,7 +214,7 @@ export function MermaidRenderer({ content }: { content: string }) {
144
214
  )}
145
215
  <div
146
216
  ref={mermaidRef}
147
- className={classNames('mermaid cursor-pointer transition-all duration-300 rounded-lg p-4 bg-white', {
217
+ className={classNames('mermaid cursor-pointer transition-all duration-300 rounded-lg p-4 bg-card', {
148
218
  'fixed inset-0 z-50 bg-black/90 flex items-center justify-center backdrop-blur-sm': isFullscreen,
149
219
  'opacity-0': !isLoaded,
150
220
  'opacity-100': isLoaded,