@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.
- package/bin/dist/dory.js +36 -2
- package/bin/dist/dory.js.map +1 -1
- package/index.html +0 -1
- package/package.json +16 -22
- package/src/components/hooks.tsx +2 -1
- package/src/components/store.ts +5 -4
- package/src/config/mdx.js +8 -3
- package/src/config/mdx.ts +8 -3
- package/src/embed/widget-iframe.ts +10 -3
- package/src/entry-server.tsx +11 -7
- package/src/main.tsx +5 -1
- package/src/mdx/accordion.tsx +11 -8
- package/src/mdx/api.tsx +2 -352
- package/src/mdx/async-api.tsx +124 -295
- package/src/mdx/callout.tsx +22 -24
- package/src/mdx/card.tsx +20 -22
- package/src/mdx/chip.tsx +14 -0
- package/src/mdx/code.tsx +133 -110
- package/src/mdx/expandable.tsx +8 -8
- package/src/mdx/fence-meta.test.ts +39 -0
- package/src/mdx/fence-meta.ts +61 -0
- package/src/mdx/fence.tsx +155 -44
- package/src/mdx/icon-grid.tsx +29 -0
- package/src/mdx/icon.tsx +2 -2
- package/src/mdx/latex.tsx +14 -7
- package/src/mdx/list.tsx +3 -3
- package/src/mdx/mdx.tsx +7 -1
- package/src/mdx/mermaid.tsx +81 -11
- package/src/mdx/open-api.tsx +396 -239
- package/src/mdx/source.tsx +148 -40
- package/src/mdx/specimen.tsx +69 -0
- package/src/mdx/step.tsx +22 -30
- package/src/mdx/table.tsx +4 -9
- package/src/mdx/tabs.tsx +39 -0
- package/src/mdx/tag.tsx +13 -21
- package/src/mdx/text.tsx +17 -9
- package/src/mdx/type-scale.tsx +28 -0
- package/src/mdx/websocket-playground.tsx +221 -704
- package/src/plugins/code-meta.ts +23 -0
- package/src/plugins/frontmatter-dev-server.ts +6 -2
- package/src/plugins/html-files-middleware.ts +9 -2
- package/src/plugins/html-metadata-injector.ts +4 -3
- package/src/plugins/llm-txt-dev-server.ts +5 -2
- package/src/plugins/llm-txt-generator.ts +2 -1
- package/src/plugins/prerender.ts +7 -4
- package/src/plugins/relative-assets.test.ts +26 -0
- package/src/plugins/relative-assets.ts +80 -0
- package/src/plugins/robots-generator.ts +6 -4
- package/src/plugins/sanitize.test.ts +5 -0
- package/src/plugins/sanitize.ts +2 -0
- package/src/plugins/shared.ts +17 -0
- package/src/plugins/sitemap-generator.ts +2 -1
- package/src/theme.scss +282 -3
- package/src/types/config.ts +3 -0
- package/src/ui/header.tsx +4 -3
- package/src/ui/layout.tsx +22 -4
- package/src/ui/page-meta.tsx +37 -0
- package/src/ui/prev-next-link.tsx +4 -3
- package/src/ui/table-of-content.tsx +11 -3
- package/src/utils/base-path.test.ts +46 -0
- package/src/utils/base-path.ts +41 -0
- package/vite.config.ssr.ts +3 -0
- package/vite.config.ts +3 -0
package/src/mdx/fence.tsx
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Highlight,
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
42
|
-
code={
|
|
43
|
-
|
|
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
|
-
|
|
177
|
+
export function Pre(props: { children: preact.JSX.Element }) {
|
|
178
|
+
const fence = fencePropsOfCode(props.children)
|
|
73
179
|
const isInCodeGroup = useContext(CodeGroupContext);
|
|
74
180
|
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
return <CodeGroup title={language}>{fence}</CodeGroup>
|
|
185
|
+
if (isInCodeGroup || fence.language === 'mermaid') {
|
|
186
|
+
return rendered
|
|
80
187
|
}
|
|
81
188
|
|
|
82
|
-
return
|
|
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:
|
|
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-
|
|
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-
|
|
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="
|
|
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="
|
|
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="
|
|
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-
|
|
74
|
+
export const hr = () => <hr className="my-6 border-0 border-t border-[var(--border)]" />;
|
|
69
75
|
|
|
70
76
|
export const br = () => <br />;
|
package/src/mdx/mermaid.tsx
CHANGED
|
@@ -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:
|
|
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-
|
|
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,
|