Repository navigation
Migrate from Docusaurus to Fumadocs on React Router #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a30175f
b23c630
caaa9af
8b0e54d
6152786
4b100cf
4edc03d
88de6ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,7 @@ | ||
| # Dependencies | ||
| /node_modules | ||
|
|
||
| # Production | ||
| /build | ||
|
|
||
| # Generated files | ||
| .docusaurus | ||
| .cache-loader | ||
|
|
||
| # Misc | ||
| .DS_Store | ||
| .env.local | ||
| .env.development.local | ||
| .env.test.local | ||
| .env.production.local | ||
| /node_modules/ | ||
|
|
||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| # React Router | ||
| /.react-router/ | ||
| /build/ | ||
| .source |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| @import 'tailwindcss'; | ||
| @import 'fumadocs-ui/css/neutral.css'; | ||
| @import 'fumadocs-ui/css/preset.css'; | ||
| /* the spacious layout (docs pages) isn't part of the preset */ | ||
| @import 'fumadocs-ui/css/generated/spacious.css'; | ||
|
|
||
| /* | ||
| * Theme derived from the PoliNetwork blue #1156ae, oklch(0.466 0.155 257.4): | ||
| * every neutral carries a little of its hue. | ||
| */ | ||
| @theme { | ||
| --color-fd-background: oklch(0.985 0.004 257); | ||
| --color-fd-foreground: oklch(0.21 0.03 257); | ||
| --color-fd-muted: oklch(0.955 0.01 257); | ||
| --color-fd-muted-foreground: oklch(0.5 0.025 257); | ||
| --color-fd-popover: oklch(0.995 0.002 257); | ||
| --color-fd-popover-foreground: oklch(0.21 0.03 257); | ||
| --color-fd-card: oklch(0.97 0.008 257); | ||
| --color-fd-card-foreground: oklch(0.21 0.03 257); | ||
| --color-fd-border: oklch(0.88 0.018 257 / 70%); | ||
| --color-fd-primary: #1156ae; | ||
| --color-fd-primary-foreground: oklch(0.99 0.003 257); | ||
| --color-fd-secondary: oklch(0.95 0.012 257); | ||
| --color-fd-secondary-foreground: oklch(0.21 0.03 257); | ||
| --color-fd-accent: oklch(0.92 0.03 257 / 60%); | ||
| --color-fd-accent-foreground: oklch(0.3 0.08 257); | ||
| --color-fd-ring: oklch(0.466 0.155 257.4 / 50%); | ||
| } | ||
|
|
||
| /* the dark primary is lighter, so links stay readable on the dark background */ | ||
| .dark { | ||
| --color-fd-background: oklch(0.16 0.012 257); | ||
| --color-fd-foreground: oklch(0.94 0.008 257); | ||
| --color-fd-muted: oklch(0.22 0.015 257); | ||
| --color-fd-muted-foreground: oklch(0.7 0.02 257); | ||
| --color-fd-popover: oklch(0.2 0.015 257); | ||
| --color-fd-popover-foreground: oklch(0.92 0.008 257); | ||
| --color-fd-card: oklch(0.19 0.014 257); | ||
| --color-fd-card-foreground: oklch(0.94 0.008 257); | ||
| --color-fd-border: oklch(0.42 0.03 257 / 35%); | ||
| --color-fd-primary: oklch(0.68 0.14 257); | ||
| --color-fd-primary-foreground: oklch(0.16 0.03 257); | ||
| --color-fd-secondary: oklch(0.23 0.016 257); | ||
| --color-fd-secondary-foreground: oklch(0.94 0.008 257); | ||
| --color-fd-accent: oklch(0.36 0.06 257 / 40%); | ||
| --color-fd-accent-foreground: oklch(0.93 0.02 257); | ||
| --color-fd-ring: oklch(0.68 0.14 257 / 60%); | ||
| } | ||
|
|
||
| .dark #nd-sidebar { | ||
| --color-fd-muted: oklch(0.24 0.016 257); | ||
| --color-fd-secondary: oklch(0.25 0.018 257); | ||
| --color-fd-muted-foreground: oklch(0.72 0.02 257); | ||
| } | ||
|
|
||
| ::selection { | ||
| background-color: color-mix(in oklab, var(--color-fd-primary) 25%, transparent); | ||
| } | ||
|
|
||
| /* `[!code error]` lines */ | ||
| .shiki .highlighted.error { | ||
| --fd-counter-color: var(--color-red-500); | ||
| background-color: color-mix(in oklab, var(--color-red-500) 12%, transparent); | ||
| } | ||
|
|
||
| .shiki .highlighted.error::after { | ||
| border-color: color-mix(in oklab, var(--color-red-500) 60%, transparent); | ||
| } | ||
|
|
||
| html { | ||
| scrollbar-gutter: stable; | ||
| } | ||
|
|
||
| html > body[data-scroll-locked] { | ||
| margin-right: 0px !important; | ||
| --removed-body-scroll-bar-size: 0px !important; | ||
| } | ||
|
|
||
| /* Fumadocs callouts, tinted with their --callout-color */ | ||
| .callout { | ||
| background-color: color-mix(in oklab, var(--callout-color) 8%, var(--color-fd-card)); | ||
| border-color: color-mix(in oklab, var(--callout-color) 35%, transparent); | ||
| } | ||
|
|
||
| .dark .callout { | ||
| background-color: color-mix(in oklab, var(--callout-color) 12%, var(--color-fd-card)); | ||
| } | ||
|
|
||
| .callout > [role='none'] { | ||
| background-color: var(--callout-color); | ||
| } | ||
|
|
||
| .callout > div > p:first-child { | ||
| color: color-mix(in oklab, var(--callout-color) 70%, var(--color-fd-foreground)); | ||
| } | ||
|
|
||
| .callout > div > div { | ||
| color: color-mix(in oklab, var(--color-fd-foreground) 85%, transparent); | ||
| } | ||
|
|
||
| .callout :not(pre) > code { | ||
| background-color: color-mix(in oklab, var(--callout-color) 12%, transparent); | ||
| border-color: color-mix(in oklab, var(--callout-color) 25%, transparent); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import type { ComponentProps } from 'react' | ||
| import { FullSearchTrigger } from 'fumadocs-ui/layouts/shared/slots/search-trigger' | ||
| import { ThemeSwitch } from 'fumadocs-ui/layouts/shared/slots/theme-switch' | ||
| import { gitConfig } from '@/lib/shared' | ||
|
|
||
| function GitHubIcon(props: ComponentProps<'svg'>) { | ||
| return ( | ||
| <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden {...props}> | ||
| <path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.2c-3.3.7-4-1.4-4-1.4-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2 0 1.9 1.2 1.9 1.2 1 1.8 2.8 1.3 3.5 1 0-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.6.2 2.8 0 3.2.9.8 1.3 1.9 1.3 3.2 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1 .9 2.2v3.3c0 .3.1.7.8.6A12 12 0 0 0 12 .3" /> | ||
| </svg> | ||
| ) | ||
| } | ||
|
|
||
| /** Top right of the page panel: search, GitHub and the theme switch, all inline */ | ||
| export function HeaderActions({ className, ...props }: ComponentProps<'div'>) { | ||
| return ( | ||
| <div className={`flex items-center gap-2 ${className ?? ''}`} {...props}> | ||
| <FullSearchTrigger className="h-8 w-56 rounded-lg" /> | ||
| <a | ||
| href={`https://github.com/${gitConfig.user}/${gitConfig.repo}`} | ||
| aria-label="GitHub" | ||
| className="inline-flex size-8 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground" | ||
| > | ||
| <GitHubIcon className="size-4.5" /> | ||
| </a> | ||
| <ThemeSwitch mode="light-dark-system" /> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import defaultMdxComponents from 'fumadocs-ui/mdx' | ||
| import type { MDXComponents } from 'mdx/types' | ||
| import { Callout } from '@/components/mdx/callout' | ||
| import { Mermaid } from '@/components/mdx/mermaid' | ||
|
|
||
| export function getMDXComponents(components?: MDXComponents) { | ||
| return { | ||
| ...defaultMdxComponents, | ||
| Callout, | ||
| Mermaid, | ||
| ...components, | ||
| } satisfies MDXComponents | ||
| } | ||
|
|
||
| export const useMDXComponents = getMDXComponents | ||
|
|
||
| declare global { | ||
| type MDXProvidedComponents = ReturnType<typeof getMDXComponents> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { ReactNode } from 'react' | ||
| import { CalloutContainer, CalloutDescription, CalloutTitle } from 'fumadocs-ui/components/callout' | ||
| import { CircleAlert, Info, Lightbulb } from 'lucide-react' | ||
|
|
||
| // Same filled style as the Fumadocs icons | ||
| const iconClass = 'size-5 -me-0.5 fill-(--callout-color) text-fd-card' | ||
|
|
||
| // Docusaurus admonition types on top of the Fumadocs callout. `color` is the | ||
| // Fumadocs color the callout is tinted with; `icon` replaces its default one. | ||
| const types = { | ||
| note: { fumadocs: 'info', color: 'var(--color-fd-muted-foreground)', icon: <Info className={iconClass} /> }, | ||
| tip: { fumadocs: 'success', color: 'var(--color-fd-success)', icon: <Lightbulb className="size-5 -me-0.5 fill-(--callout-color) text-(--callout-color)" /> }, | ||
| info: { fumadocs: 'info', color: 'var(--color-fd-info)' }, | ||
| important: { fumadocs: 'info', color: 'var(--color-fd-primary)', icon: <CircleAlert className={iconClass} /> }, | ||
| warning: { fumadocs: 'warning', color: 'var(--color-fd-warning)' }, | ||
| caution: { fumadocs: 'warning', color: 'var(--color-fd-warning)' }, | ||
| danger: { fumadocs: 'error', color: 'var(--color-fd-error)' }, | ||
| } as const | ||
|
|
||
| const aliases = { | ||
| idea: 'tip', | ||
| success: 'tip', | ||
| warn: 'warning', | ||
| error: 'danger', | ||
| } as const satisfies Record<string, keyof typeof types> | ||
|
|
||
| export type CalloutType = keyof typeof types | keyof typeof aliases | ||
|
|
||
| export function Callout({ | ||
| type = 'note', | ||
| title, | ||
| children, | ||
| }: { | ||
| type?: CalloutType | ||
| title?: ReactNode | ||
| children?: ReactNode | ||
| }) { | ||
| const resolved = type in aliases ? aliases[type as keyof typeof aliases] : (type as keyof typeof types) | ||
| const config: { fumadocs: string; color: string; icon?: ReactNode } = types[resolved] | ||
|
|
||
| return ( | ||
| <CalloutContainer | ||
| type={config.fumadocs as 'info'} | ||
| icon={config.icon} | ||
| className="callout" | ||
| style={{ '--callout-color': config.color } as React.CSSProperties} | ||
| > | ||
| {title && <CalloutTitle>{title}</CalloutTitle>} | ||
| <CalloutDescription>{children}</CalloutDescription> | ||
| </CalloutContainer> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { use, useId, useSyncExternalStore } from 'react' | ||
| import { useTheme } from 'fumadocs-ui/provider/base' | ||
|
|
||
| export function Mermaid({ chart }: { chart: string }) { | ||
| // `false` during prerendering and hydration: Mermaid needs the DOM | ||
| const isClient = useSyncExternalStore( | ||
| () => () => {}, | ||
| () => true, | ||
| () => false, | ||
| ) | ||
|
|
||
| if (!isClient) return null | ||
| return <MermaidContent chart={chart} /> | ||
| } | ||
|
|
||
| const cache = new Map<string, Promise<unknown>>() | ||
|
|
||
| function cachePromise<T>(key: string, setPromise: () => Promise<T>): Promise<T> { | ||
| const cached = cache.get(key) | ||
| if (cached) return cached as Promise<T> | ||
|
|
||
| const promise = setPromise() | ||
| cache.set(key, promise) | ||
| return promise | ||
| } | ||
|
|
||
| function MermaidContent({ chart }: { chart: string }) { | ||
| const id = useId() | ||
| const { resolvedTheme } = useTheme() | ||
| const { default: mermaid } = use(cachePromise('mermaid', () => import('mermaid'))) | ||
|
|
||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| securityLevel: 'loose', | ||
| fontFamily: 'inherit', | ||
| themeCSS: 'margin: 1.5rem auto 0;', | ||
| theme: resolvedTheme === 'dark' ? 'dark' : 'default', | ||
| }) | ||
|
|
||
| const { svg, bindFunctions } = use( | ||
| cachePromise(`${chart}-${resolvedTheme}`, () => mermaid.render(id, chart.replaceAll('\\n', '\n'))), | ||
| ) | ||
|
Comment on lines
+40
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Keep a failed diagram from replacing its documentation page. If a chart has invalid Mermaid syntax, 🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div | ||
| ref={(container) => { | ||
| if (container) bindFunctions?.(container) | ||
| }} | ||
| dangerouslySetInnerHTML={{ __html: svg }} | ||
| /> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { | ||
| SearchDialog, | ||
| SearchDialogClose, | ||
| SearchDialogContent, | ||
| SearchDialogHeader, | ||
| SearchDialogIcon, | ||
| SearchDialogInput, | ||
| SearchDialogList, | ||
| SearchDialogOverlay, | ||
| type SharedProps, | ||
| } from 'fumadocs-ui/components/dialog/search' | ||
| import { useStaticSearch } from 'fumadocs-core/search/client' | ||
| import { useI18n } from 'fumadocs-ui/contexts/i18n' | ||
| import { withBase } from '@/lib/base-url' | ||
|
|
||
| export default function DefaultSearchDialog(props: SharedProps) { | ||
| const { locale } = useI18n() | ||
| const search = useStaticSearch({ | ||
| locale, | ||
| // the index is prerendered under the base path in PR previews | ||
| from: withBase('/api/search'), | ||
| }) | ||
|
|
||
| return ( | ||
| <SearchDialog {...search} {...props}> | ||
| <SearchDialogOverlay /> | ||
| <SearchDialogContent> | ||
| <SearchDialogHeader> | ||
| <SearchDialogIcon /> | ||
| <SearchDialogInput /> | ||
| <SearchDialogClose /> | ||
| </SearchDialogHeader> | ||
| <SearchDialogList /> | ||
| </SearchDialogContent> | ||
| </SearchDialog> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * Prefix a root-relative path with the base path (set in PR previews). | ||
| * Only needed for plain URLs: React Router links add the basename themselves. | ||
| */ | ||
| export function withBase(path: string): string { | ||
| return `${import.meta.env.BASE_URL}${path.replace(/^\//, '')}` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Include the diagram instance ID in the render cache key.
If the same chart appears twice in one page, both instances receive the first
mermaid.render(id, chart)result. Their SVGs then reuse IDs, although each instance has a differentuseId()value. This can break SVG references and diagram accessibility labels. Includeidin the render cache key so each instance receives its own SVG. Mermaid-generated accessibility labels use IDs derived from the diagram ID. (mermaid.js.org)🤖 Prompt for AI Agents