@raystack/chronicle 0.1.0-canary.323385a → 0.1.0-canary.4a4a3f8

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 (80) hide show
  1. package/dist/cli/index.js +268 -9914
  2. package/package.json +20 -12
  3. package/src/cli/commands/build.ts +27 -25
  4. package/src/cli/commands/dev.ts +24 -25
  5. package/src/cli/commands/init.ts +38 -132
  6. package/src/cli/commands/serve.ts +36 -49
  7. package/src/cli/commands/start.ts +20 -25
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +25 -26
  10. package/src/cli/utils/index.ts +3 -3
  11. package/src/cli/utils/resolve.ts +9 -3
  12. package/src/cli/utils/scaffold.ts +11 -124
  13. package/src/components/mdx/code.tsx +10 -1
  14. package/src/components/mdx/details.module.css +1 -26
  15. package/src/components/mdx/details.tsx +2 -3
  16. package/src/components/mdx/image.tsx +5 -34
  17. package/src/components/mdx/link.tsx +18 -15
  18. package/src/components/ui/breadcrumbs.tsx +8 -42
  19. package/src/components/ui/search.tsx +63 -51
  20. package/src/lib/api-routes.ts +6 -8
  21. package/src/lib/config.ts +31 -28
  22. package/src/lib/head.tsx +49 -0
  23. package/src/lib/mdx-loader.ts +21 -0
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +108 -0
  26. package/src/lib/source.ts +145 -56
  27. package/src/pages/ApiLayout.tsx +33 -0
  28. package/src/pages/ApiPage.tsx +73 -0
  29. package/src/pages/DocsLayout.tsx +18 -0
  30. package/src/pages/DocsPage.tsx +43 -0
  31. package/src/pages/NotFound.tsx +17 -0
  32. package/src/server/App.tsx +67 -0
  33. package/src/server/api/apis-proxy.ts +69 -0
  34. package/src/server/api/health.ts +5 -0
  35. package/src/server/api/page/[...slug].ts +17 -0
  36. package/src/server/api/search.ts +170 -0
  37. package/src/server/api/specs.ts +9 -0
  38. package/src/server/build-search-index.ts +117 -0
  39. package/src/server/entry-client.tsx +65 -0
  40. package/src/server/entry-server.tsx +95 -0
  41. package/src/server/routes/llms.txt.ts +61 -0
  42. package/src/server/routes/og.tsx +75 -0
  43. package/src/server/routes/robots.txt.ts +11 -0
  44. package/src/server/routes/sitemap.xml.ts +40 -0
  45. package/src/server/utils/safe-path.ts +17 -0
  46. package/src/server/vite-config.ts +111 -0
  47. package/src/themes/default/Layout.tsx +78 -48
  48. package/src/themes/default/Page.module.css +44 -0
  49. package/src/themes/default/Page.tsx +9 -11
  50. package/src/themes/default/Toc.tsx +25 -39
  51. package/src/themes/default/index.ts +7 -9
  52. package/src/themes/paper/ChapterNav.tsx +64 -45
  53. package/src/themes/paper/Layout.module.css +1 -1
  54. package/src/themes/paper/Layout.tsx +24 -12
  55. package/src/themes/paper/Page.module.css +16 -4
  56. package/src/themes/paper/Page.tsx +56 -63
  57. package/src/themes/paper/ReadingProgress.tsx +160 -139
  58. package/src/themes/paper/index.ts +5 -5
  59. package/src/themes/registry.ts +7 -7
  60. package/src/types/config.ts +11 -0
  61. package/src/types/content.ts +6 -21
  62. package/src/types/globals.d.ts +3 -0
  63. package/src/types/theme.ts +4 -3
  64. package/tsconfig.json +2 -3
  65. package/next.config.mjs +0 -10
  66. package/source.config.ts +0 -50
  67. package/src/app/[[...slug]]/layout.tsx +0 -15
  68. package/src/app/[[...slug]]/page.tsx +0 -57
  69. package/src/app/api/apis-proxy/route.ts +0 -59
  70. package/src/app/api/health/route.ts +0 -3
  71. package/src/app/api/search/route.ts +0 -90
  72. package/src/app/apis/[[...slug]]/layout.tsx +0 -26
  73. package/src/app/apis/[[...slug]]/page.tsx +0 -57
  74. package/src/app/layout.tsx +0 -26
  75. package/src/app/llms-full.txt/route.ts +0 -18
  76. package/src/app/llms.txt/route.ts +0 -15
  77. package/src/app/providers.tsx +0 -8
  78. package/src/cli/utils/process.ts +0 -7
  79. package/src/themes/default/font.ts +0 -6
  80. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -0,0 +1,95 @@
1
+ import '@raystack/apsara/normalize.css';
2
+ import '@raystack/apsara/style.css';
3
+ import React from 'react';
4
+ import { renderToReadableStream } from 'react-dom/server.edge';
5
+ import { StaticRouter } from 'react-router';
6
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
+ import { mdxComponents } from '@/components/mdx';
8
+ import { loadConfig } from '@/lib/config';
9
+ import { loadApiSpecs } from '@/lib/openapi';
10
+ import { PageProvider } from '@/lib/page-context';
11
+ import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath } from '@/lib/source';
12
+ import { App } from './App';
13
+
14
+ import clientAssets from './entry-client?assets=client';
15
+ import serverAssets from './entry-server?assets=ssr';
16
+
17
+ export default {
18
+ async fetch(req: Request) {
19
+ const url = new URL(req.url);
20
+ const pathname = url.pathname;
21
+ const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
22
+
23
+ const config = loadConfig();
24
+ const apiSpecs = config.api?.length
25
+ ? await loadApiSpecs(config.api).catch(() => [])
26
+ : [];
27
+
28
+ const [tree, page] = await Promise.all([
29
+ getPageTree(),
30
+ getPage(slug),
31
+ ]);
32
+
33
+ const relativePath = page ? getRelativePath(page) : null;
34
+ const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
35
+
36
+ const pageData = page
37
+ ? {
38
+ slug,
39
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
40
+ content: mdxModule?.default
41
+ ? React.createElement(mdxModule.default, { components: mdxComponents })
42
+ : null,
43
+ toc: mdxModule?.toc ?? [],
44
+ }
45
+ : null;
46
+
47
+ const embeddedData = {
48
+ config,
49
+ tree,
50
+ slug,
51
+ frontmatter: pageData?.frontmatter ?? null,
52
+ relativePath,
53
+ };
54
+ const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
55
+
56
+ const assets = clientAssets.merge(serverAssets);
57
+
58
+ const stream = await renderToReadableStream(
59
+ <html lang="en">
60
+ <head>
61
+ <meta charSet="UTF-8" />
62
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
63
+ {assets.css.map((attr: { href: string }) => (
64
+ <link key={attr.href} rel="stylesheet" {...attr} />
65
+ ))}
66
+ {assets.js.map((attr: { href: string }) => (
67
+ <link key={attr.href} rel="modulepreload" {...attr} />
68
+ ))}
69
+ <script type="module" src={assets.entry} />
70
+ <script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
71
+ </head>
72
+ <body>
73
+ <div id="root">
74
+ <StaticRouter location={pathname}>
75
+ <ReactRouterProvider>
76
+ <PageProvider
77
+ initialConfig={config}
78
+ initialTree={tree}
79
+ initialPage={pageData}
80
+ initialApiSpecs={apiSpecs}
81
+ >
82
+ <App />
83
+ </PageProvider>
84
+ </ReactRouterProvider>
85
+ </StaticRouter>
86
+ </div>
87
+ </body>
88
+ </html>,
89
+ );
90
+
91
+ return new Response(stream, {
92
+ headers: { 'Content-Type': 'text/html;charset=utf-8' },
93
+ });
94
+ },
95
+ };
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { defineHandler, HTTPError } from 'nitro';
5
+ import { loadConfig } from '@/lib/config';
6
+
7
+ function getContentDir(): string {
8
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
9
+ }
10
+
11
+ async function scanPages(): Promise<{ title: string; url: string }[]> {
12
+ const contentDir = getContentDir();
13
+ const pages: { title: string; url: string }[] = [];
14
+
15
+ async function scan(dir: string, prefix: string[] = []) {
16
+ try {
17
+ const entries = await fs.readdir(dir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
20
+ continue;
21
+ const fullPath = path.join(dir, entry.name);
22
+
23
+ if (entry.isDirectory()) {
24
+ await scan(fullPath, [...prefix, entry.name]);
25
+ continue;
26
+ }
27
+
28
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
29
+ continue;
30
+
31
+ const raw = await fs.readFile(fullPath, 'utf-8');
32
+ const { data: fm } = matter(raw);
33
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
34
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
35
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
36
+
37
+ pages.push({ title: fm.title ?? baseName, url });
38
+ }
39
+ } catch {
40
+ /* directory not readable */
41
+ }
42
+ }
43
+
44
+ await scan(contentDir);
45
+ return pages;
46
+ }
47
+
48
+ export default defineHandler(async event => {
49
+ const config = loadConfig();
50
+
51
+ if (!config.llms?.enabled) {
52
+ throw new HTTPError({ status: 404, message: 'Not Found' });
53
+ }
54
+
55
+ const pages = await scanPages();
56
+ const index = pages.map(p => `- [${p.title}](${p.url})`).join('\n');
57
+ const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
58
+
59
+ event.res.headers.set('Content-Type', 'text/plain');
60
+ return body;
61
+ });
@@ -0,0 +1,75 @@
1
+ import { defineHandler } from 'nitro';
2
+ import React from 'react';
3
+ import satori from 'satori';
4
+ import { loadConfig } from '@/lib/config';
5
+
6
+ let fontData: ArrayBuffer | null = null;
7
+
8
+ async function loadFont(): Promise<ArrayBuffer> {
9
+ if (fontData) return fontData;
10
+
11
+ try {
12
+ const response = await fetch(
13
+ 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2'
14
+ );
15
+ fontData = await response.arrayBuffer();
16
+ } catch {
17
+ fontData = new ArrayBuffer(0);
18
+ }
19
+
20
+ return fontData;
21
+ }
22
+
23
+ export default defineHandler(async event => {
24
+ const config = loadConfig();
25
+ const title = event.url.searchParams.get('title') ?? config.title;
26
+ const description = event.url.searchParams.get('description') ?? '';
27
+ const siteName = config.title;
28
+
29
+ const font = await loadFont();
30
+
31
+ const svg = await satori(
32
+ <div
33
+ style={{
34
+ height: '100%',
35
+ width: '100%',
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ justifyContent: 'center',
39
+ padding: '60px 80px',
40
+ backgroundColor: '#0a0a0a',
41
+ color: '#fafafa',
42
+ }}
43
+ >
44
+ <div style={{ fontSize: 24, color: '#888', marginBottom: 16 }}>
45
+ {siteName}
46
+ </div>
47
+ <div
48
+ style={{
49
+ fontSize: 56,
50
+ fontWeight: 700,
51
+ lineHeight: 1.2,
52
+ marginBottom: 24,
53
+ }}
54
+ >
55
+ {title}
56
+ </div>
57
+ {description && (
58
+ <div style={{ fontSize: 24, color: '#999', lineHeight: 1.4 }}>
59
+ {description}
60
+ </div>
61
+ )}
62
+ </div>,
63
+ {
64
+ width: 1200,
65
+ height: 630,
66
+ fonts: [
67
+ { name: 'Inter', data: font, weight: 400, style: 'normal' as const },
68
+ ],
69
+ },
70
+ );
71
+
72
+ event.res.headers.set('Content-Type', 'image/svg+xml');
73
+ event.res.headers.set('Cache-Control', 'public, max-age=86400');
74
+ return svg;
75
+ });
@@ -0,0 +1,11 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+
4
+ export default defineHandler(event => {
5
+ const config = loadConfig();
6
+ const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
7
+ const body = `User-agent: *\nAllow: /${sitemap}`;
8
+
9
+ event.res.headers.set('Content-Type', 'text/plain');
10
+ return body;
11
+ });
@@ -0,0 +1,40 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { buildApiRoutes } from '@/lib/api-routes';
3
+ import { loadConfig } from '@/lib/config';
4
+ import { loadApiSpecs } from '@/lib/openapi';
5
+ import { getPages } from '@/lib/source';
6
+
7
+ export default defineHandler(async event => {
8
+ const config = loadConfig();
9
+
10
+ if (!config.url) {
11
+ event.res.headers.set('Content-Type', 'application/xml');
12
+ return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
13
+ }
14
+
15
+ const baseUrl = config.url.replace(/\/$/, '');
16
+
17
+ const pages = await getPages();
18
+ const docPages = pages.map(page => {
19
+ const data = page.data as Record<string, unknown>;
20
+ const lastmod = data.lastModified
21
+ ? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
22
+ : '';
23
+ return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
24
+ });
25
+
26
+ const apiPages = config.api?.length
27
+ ? buildApiRoutes(await loadApiSpecs(config.api)).map(
28
+ route => `<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
29
+ )
30
+ : [];
31
+
32
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
33
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
34
+ <url><loc>${baseUrl}</loc></url>
35
+ ${[...docPages, ...apiPages].join('\n')}
36
+ </urlset>`;
37
+
38
+ event.res.headers.set('Content-Type', 'application/xml');
39
+ return xml;
40
+ });
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+
3
+ /**
4
+ * Resolve a URL path within a base directory, preventing path traversal.
5
+ * Returns null if the resolved path escapes the base directory.
6
+ */
7
+ export function safePath(baseDir: string, urlPath: string): string | null {
8
+ const decoded = decodeURIComponent(urlPath.split('?')[0]);
9
+ const resolved = path.resolve(baseDir, '.' + decoded);
10
+ if (
11
+ !resolved.startsWith(path.resolve(baseDir) + path.sep) &&
12
+ resolved !== path.resolve(baseDir)
13
+ ) {
14
+ return null;
15
+ }
16
+ return resolved;
17
+ }
@@ -0,0 +1,111 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
3
+ import { defineConfig as defineFumadocsConfig } from 'fumadocs-mdx/config';
4
+ import mdx from 'fumadocs-mdx/vite';
5
+ import { nitro } from 'nitro/vite';
6
+ import path from 'node:path';
7
+ import remarkDirective from 'remark-directive';
8
+ import { type InlineConfig } from 'vite';
9
+ import remarkUnusedDirectives from '../lib/remark-unused-directives';
10
+
11
+ function resolveOutputDir(projectRoot: string, preset?: string): string {
12
+ if (preset === 'vercel' || preset === 'vercel-static') return path.resolve(projectRoot, '.vercel/output');
13
+ return path.resolve(projectRoot, '.output');
14
+ }
15
+
16
+ export interface ViteConfigOptions {
17
+ packageRoot: string;
18
+ projectRoot: string;
19
+ contentDir: string;
20
+ preset?: string;
21
+ }
22
+
23
+ export async function createViteConfig(
24
+ options: ViteConfigOptions
25
+ ): Promise<InlineConfig> {
26
+ const { packageRoot, projectRoot, contentDir, preset } = options;
27
+
28
+ return {
29
+ root: packageRoot,
30
+ configFile: false,
31
+ plugins: [
32
+ nitro({
33
+ serverDir: path.resolve(packageRoot, 'src/server'),
34
+ ...(preset && { preset }),
35
+ }),
36
+ mdx({
37
+ default: defineFumadocsConfig({
38
+ mdxOptions: {
39
+ remarkPlugins: [
40
+ remarkDirective,
41
+ [remarkDirectiveAdmonition, {
42
+ tags: {
43
+ CalloutContainer: 'Callout',
44
+ CalloutTitle: 'CalloutTitle',
45
+ CalloutDescription: 'CalloutDescription',
46
+ },
47
+ types: {
48
+ note: 'accent',
49
+ tip: 'accent',
50
+ info: 'accent',
51
+ warn: 'attention',
52
+ warning: 'attention',
53
+ danger: 'alert',
54
+ caution: 'alert',
55
+ success: 'success',
56
+ },
57
+ }],
58
+ remarkUnusedDirectives,
59
+ remarkMdxMermaid,
60
+ ],
61
+ },
62
+ }),
63
+ }, { index: false }),
64
+ react()
65
+ ],
66
+ resolve: {
67
+ alias: {
68
+ '@': path.resolve(packageRoot, 'src'),
69
+ },
70
+ conditions: ['module-sync', 'import', 'node'],
71
+ dedupe: [
72
+ 'react',
73
+ 'react-dom',
74
+ 'react/jsx-runtime',
75
+ 'react/jsx-dev-runtime',
76
+ 'react-router',
77
+ ]
78
+ },
79
+ server: {
80
+ fs: {
81
+ allow: [packageRoot, projectRoot, contentDir]
82
+ }
83
+ },
84
+ define: {
85
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
86
+ __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
87
+ },
88
+ css: {
89
+ modules: {
90
+ localsConvention: 'camelCase'
91
+ }
92
+ },
93
+ ssr: {
94
+ noExternal: ['@raystack/apsara', 'dayjs', 'fumadocs-core']
95
+ },
96
+ environments: {
97
+ client: {
98
+ build: {
99
+ rollupOptions: {
100
+ input: path.resolve(packageRoot, 'src/server/entry-client.tsx')
101
+ }
102
+ }
103
+ }
104
+ },
105
+ nitro: {
106
+ output: {
107
+ dir: resolveOutputDir(projectRoot, preset),
108
+ },
109
+ },
110
+ };
111
+ }
@@ -1,64 +1,86 @@
1
- "use client";
2
-
3
- import { useMemo, useEffect, useRef } from "react";
4
- import { usePathname } from "next/navigation";
5
- import NextLink from "next/link";
6
- import { cx } from "class-variance-authority";
7
- import { Flex, Navbar, Headline, Link, Sidebar, Button } from "@raystack/apsara";
8
- import { RectangleStackIcon } from "@heroicons/react/24/outline";
9
- import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
10
- import { Search } from "@/components/ui/search";
11
- import { Footer } from "@/components/ui/footer";
12
- import { MethodBadge } from "@/components/api/method-badge";
13
- import type { ThemeLayoutProps, PageTreeItem } from "@/types";
14
- import styles from "./Layout.module.css";
1
+ import { RectangleStackIcon } from '@heroicons/react/24/outline';
2
+ import {
3
+ Button,
4
+ Flex,
5
+ Headline,
6
+ Link,
7
+ Navbar,
8
+ Sidebar
9
+ } from '@raystack/apsara';
10
+ import { cx } from 'class-variance-authority';
11
+ import { useEffect, useMemo, useRef } from 'react';
12
+ import { Link as RouterLink, useLocation } from 'react-router';
13
+ import { MethodBadge } from '@/components/api/method-badge';
14
+ import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
+ import { Footer } from '@/components/ui/footer';
16
+ import { Search } from '@/components/ui/search';
17
+ import type { Node } from 'fumadocs-core/page-tree';
18
+ import type { ThemeLayoutProps } from '@/types';
19
+ import styles from './Layout.module.css';
15
20
 
16
21
  const iconMap: Record<string, React.ReactNode> = {
17
- "rectangle-stack": <RectangleStackIcon width={16} height={16} />,
18
- "method-get": <MethodBadge method="GET" size="micro" />,
19
- "method-post": <MethodBadge method="POST" size="micro" />,
20
- "method-put": <MethodBadge method="PUT" size="micro" />,
21
- "method-delete": <MethodBadge method="DELETE" size="micro" />,
22
- "method-patch": <MethodBadge method="PATCH" size="micro" />,
22
+ 'rectangle-stack': <RectangleStackIcon width={16} height={16} />,
23
+ 'method-get': <MethodBadge method='GET' size='micro' />,
24
+ 'method-post': <MethodBadge method='POST' size='micro' />,
25
+ 'method-put': <MethodBadge method='PUT' size='micro' />,
26
+ 'method-delete': <MethodBadge method='DELETE' size='micro' />,
27
+ 'method-patch': <MethodBadge method='PATCH' size='micro' />
23
28
  };
24
29
 
25
30
  let savedScrollTop = 0;
26
31
 
27
- export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
28
- const pathname = usePathname();
32
+ export function Layout({
33
+ children,
34
+ config,
35
+ tree,
36
+ classNames
37
+ }: ThemeLayoutProps) {
38
+ const { pathname } = useLocation();
29
39
  const scrollRef = useRef<HTMLDivElement>(null);
30
40
 
31
41
  useEffect(() => {
32
42
  const el = scrollRef.current;
33
43
  if (!el) return;
34
- const onScroll = () => { savedScrollTop = el.scrollTop; };
44
+ const onScroll = () => {
45
+ savedScrollTop = el.scrollTop;
46
+ };
35
47
  el.addEventListener('scroll', onScroll);
36
48
  return () => el.removeEventListener('scroll', onScroll);
37
49
  }, []);
38
50
 
39
51
  useEffect(() => {
40
52
  const el = scrollRef.current;
41
- if (el) requestAnimationFrame(() => { el.scrollTop = savedScrollTop; });
53
+ if (el)
54
+ requestAnimationFrame(() => {
55
+ el.scrollTop = savedScrollTop;
56
+ });
42
57
  }, [pathname]);
43
58
 
44
59
  return (
45
- <Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
60
+ <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
46
61
  <Navbar className={styles.header}>
47
62
  <Navbar.Start>
48
- <NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}>
49
- <Headline size="small" weight="medium" as="h1">
63
+ <RouterLink
64
+ to='/'
65
+ style={{ textDecoration: 'none', color: 'inherit' }}
66
+ >
67
+ <Headline size='small' weight='medium' as='h1'>
50
68
  {config.title}
51
69
  </Headline>
52
- </NextLink>
70
+ </RouterLink>
53
71
  </Navbar.Start>
54
72
  <Navbar.End>
55
- <Flex gap="medium" align="center" className={styles.navActions}>
56
- {config.api?.map((api) => (
57
- <NextLink key={api.basePath} href={api.basePath} className={styles.navButton}>
73
+ <Flex gap='medium' align='center' className={styles.navActions}>
74
+ {config.api?.map(api => (
75
+ <RouterLink
76
+ key={api.basePath}
77
+ to={api.basePath}
78
+ className={styles.navButton}
79
+ >
58
80
  {api.name} API
59
- </NextLink>
81
+ </RouterLink>
60
82
  ))}
61
- {config.navigation?.links?.map((link) => (
83
+ {config.navigation?.links?.map(link => (
62
84
  <Link key={link.href} href={link.href}>
63
85
  {link.label}
64
86
  </Link>
@@ -69,18 +91,24 @@ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps)
69
91
  </Navbar.End>
70
92
  </Navbar>
71
93
  <Flex className={cx(styles.body, classNames?.body)}>
72
- <Sidebar defaultOpen collapsible={false} className={cx(styles.sidebar, classNames?.sidebar)}>
94
+ <Sidebar
95
+ defaultOpen
96
+ collapsible={false}
97
+ className={cx(styles.sidebar, classNames?.sidebar)}
98
+ >
73
99
  <Sidebar.Main ref={scrollRef}>
74
- {tree.children.map((item) => (
100
+ {tree.children.map((item, i) => (
75
101
  <SidebarNode
76
- key={item.url ?? item.name}
102
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
77
103
  item={item}
78
104
  pathname={pathname}
79
105
  />
80
106
  ))}
81
107
  </Sidebar.Main>
82
108
  </Sidebar>
83
- <main className={cx(styles.content, classNames?.content)}>{children}</main>
109
+ <main className={cx(styles.content, classNames?.content)}>
110
+ {children}
111
+ </main>
84
112
  </Flex>
85
113
  <Footer config={config.footer} />
86
114
  </Flex>
@@ -89,25 +117,26 @@ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps)
89
117
 
90
118
  function SidebarNode({
91
119
  item,
92
- pathname,
120
+ pathname
93
121
  }: {
94
- item: PageTreeItem;
122
+ item: Node;
95
123
  pathname: string;
96
124
  }) {
97
- if (item.type === "separator") {
125
+ if (item.type === 'separator') {
98
126
  return null;
99
127
  }
100
128
 
101
- if (item.type === "folder" && item.children) {
129
+ if (item.type === 'folder') {
130
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
102
131
  return (
103
132
  <Sidebar.Group
104
- label={item.name}
105
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
133
+ label={item.name?.toString() ?? ''}
134
+ leadingIcon={icon ?? undefined}
106
135
  classNames={{ items: styles.groupItems }}
107
136
  >
108
- {item.children.map((child) => (
137
+ {item.children.map((child, i) => (
109
138
  <SidebarNode
110
- key={child.url ?? child.name}
139
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
111
140
  item={child}
112
141
  pathname={pathname}
113
142
  />
@@ -117,14 +146,15 @@ function SidebarNode({
117
146
  }
118
147
 
119
148
  const isActive = pathname === item.url;
120
- const href = item.url ?? "#";
121
- const link = useMemo(() => <NextLink href={href} scroll={false} />, [href]);
149
+ const href = item.url ?? '#';
150
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
151
+ const link = useMemo(() => <RouterLink to={href} />, [href]);
122
152
 
123
153
  return (
124
154
  <Sidebar.Item
125
155
  href={href}
126
156
  active={isActive}
127
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
157
+ leadingIcon={icon ?? undefined}
128
158
  as={link}
129
159
  >
130
160
  {item.name}
@@ -38,9 +38,53 @@
38
38
  margin-bottom: var(--rs-space-3);
39
39
  }
40
40
 
41
+ .content img {
42
+ max-width: 100%;
43
+ height: auto;
44
+ }
45
+
41
46
  .content table {
42
47
  display: block;
43
48
  max-width: 100%;
44
49
  overflow-x: auto;
45
50
  margin-bottom: var(--rs-space-5);
46
51
  }
52
+
53
+ .content details {
54
+ border: 1px solid var(--rs-color-border-base-primary);
55
+ border-radius: var(--rs-radius-2);
56
+ margin: var(--rs-space-5) 0;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .content details summary {
61
+ padding: var(--rs-space-4) var(--rs-space-5);
62
+ cursor: pointer;
63
+ font-weight: 500;
64
+ font-size: var(--rs-font-size-small);
65
+ color: var(--rs-color-text-base-primary);
66
+ background: var(--rs-color-background-base-secondary);
67
+ list-style: none;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--rs-space-3);
71
+ }
72
+
73
+ .content details summary::-webkit-details-marker {
74
+ display: none;
75
+ }
76
+
77
+ .content details summary::before {
78
+ content: '▶';
79
+ font-size: 10px;
80
+ transition: transform 0.2s ease;
81
+ color: var(--rs-color-text-base-secondary);
82
+ }
83
+
84
+ .content details[open] > summary::before {
85
+ transform: rotate(90deg);
86
+ }
87
+
88
+ .content details > :not(summary) {
89
+ padding: var(--rs-space-4) var(--rs-space-5);
90
+ }