@raystack/chronicle 0.1.0-canary.6511afe → 0.1.0-canary.67113f8

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/src/lib/source.ts CHANGED
@@ -1,16 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { loader } from 'fumadocs-core/source';
4
+ import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
4
5
  import matter from 'gray-matter';
5
6
  import type { MDXContent } from 'mdx/types';
6
- import type { Frontmatter, PageTree, PageTreeItem } from '@/types';
7
-
8
- export interface SourcePage {
9
- url: string;
10
- slugs: string[];
11
- filePath: string;
12
- frontmatter: Frontmatter;
13
- }
7
+ import type { TableOfContents } from 'fumadocs-core/toc';
8
+ import type { Frontmatter } from '@/types';
14
9
 
15
10
  function getContentDir(): string {
16
11
  return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
@@ -43,14 +38,18 @@ async function scanFiles(contentDir: string) {
43
38
  files.push({
44
39
  type: 'page',
45
40
  path: relativePath,
46
- data: { ...data, _absolutePath: fullPath }
41
+ data: { ...data, _relativePath: relativePath }
47
42
  });
48
43
  } else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
49
- const raw = await fs.readFile(fullPath, 'utf-8');
50
- const data = entry.name.endsWith('.json')
51
- ? JSON.parse(raw)
52
- : matter(raw).data;
53
- files.push({ type: 'meta', path: relativePath, data });
44
+ try {
45
+ const raw = await fs.readFile(fullPath, 'utf-8');
46
+ const data = entry.name.endsWith('.json')
47
+ ? JSON.parse(raw)
48
+ : matter(raw).data;
49
+ files.push({ type: 'meta', path: relativePath, data });
50
+ } catch {
51
+ /* malformed meta file */
52
+ }
54
53
  }
55
54
  }
56
55
  } catch {
@@ -63,7 +62,6 @@ async function scanFiles(contentDir: string) {
63
62
  }
64
63
 
65
64
  let cachedSource: ReturnType<typeof loader> | null = null;
66
- let cachedPages: SourcePage[] | null = null;
67
65
 
68
66
  async function getSource() {
69
67
  if (cachedSource) return cachedSource;
@@ -76,111 +74,87 @@ async function getSource() {
76
74
  return cachedSource;
77
75
  }
78
76
 
77
+ export { getSource as source };
78
+
79
79
  export function invalidate() {
80
80
  cachedSource = null;
81
- cachedPages = null;
82
81
  }
83
82
 
84
- export async function getPages(): Promise<SourcePage[]> {
85
- if (cachedPages) return cachedPages;
86
-
87
- const s = await getSource();
88
- cachedPages = s.getPages().map(page => {
89
- const data = page.data as Record<string, unknown>;
90
- return {
91
- url: page.url,
92
- slugs: page.slugs,
93
- filePath: (data._absolutePath as string) ?? '',
94
- frontmatter: {
95
- title:
96
- (data.title as string) ??
97
- page.slugs[page.slugs.length - 1] ??
98
- 'Untitled',
99
- description: data.description as string | undefined,
100
- order: data.order as number | undefined,
101
- icon: data.icon as string | undefined,
102
- lastModified: data.lastModified as string | undefined
103
- }
104
- };
105
- });
106
-
107
- return cachedPages;
83
+ function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
84
+ if (node.type === 'page') return orderMap.get(node.url);
85
+ if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
86
+ return undefined;
108
87
  }
109
88
 
110
- export async function getPage(slug?: string[]): Promise<SourcePage | null> {
111
- const pages = await getPages();
112
- const targetUrl = !slug || slug.length === 0 ? '/' : `/${slug.join('/')}`;
113
- return pages.find(p => p.url === targetUrl) ?? null;
89
+ function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
90
+ return [...nodes]
91
+ .map(n =>
92
+ n.type === 'folder'
93
+ ? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
94
+ : n
95
+ )
96
+ .sort(
97
+ (a, b) =>
98
+ (getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
99
+ (getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
100
+ );
114
101
  }
115
102
 
116
- export async function loadPageComponent(
117
- page: SourcePage
118
- ): Promise<MDXContent | null> {
119
- if (!page.filePath) return null;
120
- try {
121
- await fs.access(page.filePath);
122
- } catch {
123
- return null;
103
+ function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
104
+ const orderMap = new Map<string, number>();
105
+ for (const page of pages) {
106
+ const d = page.data as Record<string, unknown>;
107
+ const order = d.order as number | undefined;
108
+ if (order !== undefined) orderMap.set(page.url, order);
109
+ if (page.url === '/') orderMap.set('/', order ?? 0);
124
110
  }
125
- const contentDir = getContentDir();
126
- const relativePath = path.relative(contentDir, page.filePath);
127
- const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
128
- const mod = relativePath.endsWith('.md')
129
- ? await import(`@content/${withoutExt}.md`)
130
- : await import(`@content/${withoutExt}.mdx`);
131
- return mod.default;
111
+ return { ...tree, children: sortNodes(tree.children, orderMap) };
112
+ }
113
+
114
+ export async function getPageTree(): Promise<Root> {
115
+ const s = await getSource();
116
+ return sortTreeByOrder(s.pageTree as Root, s.getPages());
132
117
  }
133
118
 
134
- export async function buildPageTree(): Promise<PageTree> {
119
+ export async function getPages() {
135
120
  const s = await getSource();
136
- const pages = s.getPages();
137
- const folders = new Map<string, PageTreeItem[]>();
138
- const rootPages: PageTreeItem[] = [];
121
+ return s.getPages();
122
+ }
139
123
 
140
- for (const page of pages) {
141
- const data = page.data as Record<string, unknown>;
142
- const isIndex = page.url === '/';
143
- const item: PageTreeItem = {
144
- type: 'page',
145
- name: (data.title as string) ?? page.slugs.join('/') ?? 'Untitled',
146
- url: page.url,
147
- order: (data.order as number | undefined) ?? (isIndex ? 0 : undefined)
148
- };
149
-
150
- if (page.slugs.length > 1) {
151
- const folder = page.slugs[0];
152
- if (!folders.has(folder)) {
153
- folders.set(folder, []);
154
- }
155
- folders.get(folder)?.push(item);
156
- } else {
157
- rootPages.push(item);
158
- }
159
- }
124
+ export async function getPage(slugs?: string[]) {
125
+ const s = await getSource();
126
+ return s.getPage(slugs);
127
+ }
160
128
 
161
- const sortByOrder = (items: PageTreeItem[]) =>
162
- items.sort(
163
- (a, b) =>
164
- (a.order ?? Number.MAX_SAFE_INTEGER) -
165
- (b.order ?? Number.MAX_SAFE_INTEGER)
166
- );
129
+ export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
130
+ const d = page.data as Record<string, unknown>;
131
+ return {
132
+ title: (d.title as string) ?? fallbackTitle ?? 'Untitled',
133
+ description: d.description as string | undefined,
134
+ order: d.order as number | undefined,
135
+ icon: d.icon as string | undefined,
136
+ lastModified: d.lastModified as string | undefined,
137
+ };
138
+ }
167
139
 
168
- const children: PageTreeItem[] = sortByOrder(rootPages);
169
-
170
- const folderItems: PageTreeItem[] = [];
171
- for (const [folder, items] of folders) {
172
- const sorted = sortByOrder(items);
173
- const indexPage = items.find(item => item.url === `/${folder}`);
174
- const folderOrder = indexPage?.order ?? sorted[0]?.order;
175
- folderItems.push({
176
- type: 'folder',
177
- name: `${folder.charAt(0).toUpperCase()}${folder.slice(1)}`,
178
- order: folderOrder,
179
- children: sorted
180
- });
181
- }
140
+ export function getRelativePath(page: { data: unknown }): string {
141
+ return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
142
+ }
182
143
 
183
- children.push(...sortByOrder(folderItems));
144
+ const ssrModules = import.meta.glob<{ default?: MDXContent; toc?: TableOfContents }>(
145
+ '../../.content/**/*.{mdx,md}'
146
+ );
184
147
 
185
- return { name: 'root', children };
148
+ export async function loadPageModule(
149
+ relativePath: string
150
+ ): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
151
+ if (!relativePath || relativePath.includes('..')) return { default: null, toc: [] };
152
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
153
+ const key = relativePath.endsWith('.md')
154
+ ? `../../.content/${withoutExt}.md`
155
+ : `../../.content/${withoutExt}.mdx`;
156
+ const loader = ssrModules[key];
157
+ if (!loader) return { default: null, toc: [] };
158
+ const mod = await loader();
159
+ return { default: mod.default ?? null, toc: mod.toc ?? [] };
186
160
  }
@@ -33,7 +33,7 @@ export function DocsPage({ slug }: DocsPageProps) {
33
33
  slug,
34
34
  frontmatter: page.frontmatter,
35
35
  content: page.content,
36
- toc: []
36
+ toc: page.toc
37
37
  }}
38
38
  config={config}
39
39
  tree={tree}
@@ -1,6 +1,5 @@
1
- import path from 'node:path';
2
1
  import { defineHandler, HTTPError } from 'nitro';
3
- import { getPage } from '@/lib/source';
2
+ import { getPage, extractFrontmatter, getRelativePath } from '@/lib/source';
4
3
 
5
4
  export default defineHandler(async event => {
6
5
  const slugParam = event.context.params?.slug ?? '';
@@ -11,8 +10,8 @@ export default defineHandler(async event => {
11
10
  throw new HTTPError({ status: 404, message: 'Page not found' });
12
11
  }
13
12
 
14
- const contentDir = __CHRONICLE_CONTENT_DIR__;
15
- const relativePath = path.relative(contentDir, page.filePath);
16
-
17
- return { frontmatter: page.frontmatter, relativePath };
13
+ return {
14
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
15
+ relativePath: getRelativePath(page),
16
+ };
18
17
  });
@@ -1,17 +1,16 @@
1
1
  import '@vitejs/plugin-react/preamble';
2
- import React from 'react';
3
2
  import { hydrateRoot } from 'react-dom/client';
4
3
  import { BrowserRouter } from 'react-router';
5
- import { mdxComponents } from '@/components/mdx';
4
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
5
+ import { loadMdxModule } from '@/lib/mdx-loader';
6
6
  import { PageProvider } from '@/lib/page-context';
7
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types';
7
+ import type { ChronicleConfig, Frontmatter, Root } from '@/types';
8
8
  import type { ApiSpec } from '@/lib/openapi';
9
- import type { ReactNode } from 'react';
10
9
  import { App } from './App';
11
10
 
12
11
  interface EmbeddedData {
13
12
  config: ChronicleConfig;
14
- tree: PageTree;
13
+ tree: Root;
15
14
  slug: string[];
16
15
  frontmatter: Frontmatter;
17
16
  relativePath: string;
@@ -26,7 +25,7 @@ async function hydrate() {
26
25
  const config: ChronicleConfig = embedded?.config ?? {
27
26
  title: 'Documentation'
28
27
  };
29
- const tree: PageTree = embedded?.tree ?? { name: 'root', children: [] };
28
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
30
29
  const isApiPage =
31
30
  window.location.pathname.startsWith('/apis') && !!config.api?.length;
32
31
  const apiSpecs: ApiSpec[] = isApiPage
@@ -36,20 +35,27 @@ async function hydrate() {
36
35
  : [];
37
36
 
38
37
  const page = embedded?.relativePath
39
- ? await loadPage(embedded)
38
+ ? {
39
+ slug: embedded.slug,
40
+ frontmatter: embedded.frontmatter,
41
+ ...(await loadMdxModule(embedded.relativePath)),
42
+ }
40
43
  : null;
41
44
 
42
45
  hydrateRoot(
43
46
  document.getElementById('root') as HTMLElement,
44
47
  <BrowserRouter>
45
- <PageProvider
46
- initialConfig={config}
47
- initialTree={tree}
48
- initialPage={page}
49
- initialApiSpecs={apiSpecs}
50
- >
51
- <App />
52
- </PageProvider>
48
+ <ReactRouterProvider>
49
+ <PageProvider
50
+ initialConfig={config}
51
+ initialTree={tree}
52
+ initialPage={page}
53
+ initialApiSpecs={apiSpecs}
54
+ loadMdx={loadMdxModule}
55
+ >
56
+ <App />
57
+ </PageProvider>
58
+ </ReactRouterProvider>
53
59
  </BrowserRouter>
54
60
  );
55
61
  } catch (err) {
@@ -57,14 +63,4 @@ async function hydrate() {
57
63
  }
58
64
  }
59
65
 
60
- async function loadPage(
61
- embedded: EmbeddedData
62
- ): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode }> {
63
- const mod = await import(/* @vite-ignore */ `/.content/${embedded.relativePath}`);
64
- const content = mod.default
65
- ? React.createElement(mod.default, { components: mdxComponents })
66
- : null;
67
- return { slug: embedded.slug, frontmatter: embedded.frontmatter, content };
68
- }
69
-
70
66
  hydrate();
@@ -1,19 +1,17 @@
1
1
  import '@raystack/apsara/normalize.css';
2
2
  import '@raystack/apsara/style.css';
3
- import path from 'node:path';
4
3
  import React from 'react';
5
4
  import { renderToReadableStream } from 'react-dom/server.edge';
6
5
  import { StaticRouter } from 'react-router';
6
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
7
  import { mdxComponents } from '@/components/mdx';
8
8
  import { loadConfig } from '@/lib/config';
9
9
  import { loadApiSpecs } from '@/lib/openapi';
10
10
  import { PageProvider } from '@/lib/page-context';
11
- import { buildPageTree, getPage, loadPageComponent } from '@/lib/source';
11
+ import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath } from '@/lib/source';
12
12
  import { App } from './App';
13
13
 
14
- // @ts-expect-error virtual import from Nitro
15
14
  import clientAssets from './entry-client?assets=client';
16
- // @ts-expect-error virtual import from Nitro
17
15
  import serverAssets from './entry-server?assets=ssr';
18
16
 
19
17
  export default {
@@ -27,25 +25,25 @@ export default {
27
25
  ? await loadApiSpecs(config.api).catch(() => [])
28
26
  : [];
29
27
 
30
- const [tree, sourcePage] = await Promise.all([
31
- buildPageTree(),
28
+ const [tree, page] = await Promise.all([
29
+ getPageTree(),
32
30
  getPage(slug),
33
31
  ]);
34
32
 
35
- const pageData = sourcePage
33
+ const relativePath = page ? getRelativePath(page) : null;
34
+ const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
35
+
36
+ const pageData = page
36
37
  ? {
37
38
  slug,
38
- frontmatter: sourcePage.frontmatter,
39
- content: await loadPageComponent(sourcePage).then(component =>
40
- component ? React.createElement(component, { components: mdxComponents }) : null
41
- ),
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 ?? [],
42
44
  }
43
45
  : null;
44
46
 
45
- const relativePath = sourcePage
46
- ? path.relative(__CHRONICLE_CONTENT_DIR__, sourcePage.filePath)
47
- : null;
48
-
49
47
  const embeddedData = {
50
48
  config,
51
49
  tree,
@@ -74,21 +72,28 @@ export default {
74
72
  <body>
75
73
  <div id="root">
76
74
  <StaticRouter location={pathname}>
77
- <PageProvider
78
- initialConfig={config}
79
- initialTree={tree}
80
- initialPage={pageData}
81
- initialApiSpecs={apiSpecs}
82
- >
83
- <App />
84
- </PageProvider>
75
+ <ReactRouterProvider>
76
+ <PageProvider
77
+ initialConfig={config}
78
+ initialTree={tree}
79
+ initialPage={pageData}
80
+ initialApiSpecs={apiSpecs}
81
+ loadMdx={async () => ({ content: null, toc: [] })}
82
+ >
83
+ <App />
84
+ </PageProvider>
85
+ </ReactRouterProvider>
85
86
  </StaticRouter>
86
87
  </div>
87
88
  </body>
88
89
  </html>,
89
90
  );
90
91
 
92
+ const isApiRoute = pathname.startsWith('/apis');
93
+ const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
94
+
91
95
  return new Response(stream, {
96
+ status,
92
97
  headers: { 'Content-Type': 'text/html;charset=utf-8' },
93
98
  });
94
99
  },
@@ -16,8 +16,9 @@ export default defineHandler(async event => {
16
16
 
17
17
  const pages = await getPages();
18
18
  const docPages = pages.map(page => {
19
- const lastmod = page.frontmatter.lastModified
20
- ? `<lastmod>${new Date(page.frontmatter.lastModified).toISOString()}</lastmod>`
19
+ const data = page.data as Record<string, unknown>;
20
+ const lastmod = data.lastModified
21
+ ? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
21
22
  : '';
22
23
  return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
23
24
  });
@@ -1,8 +1,17 @@
1
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';
2
4
  import mdx from 'fumadocs-mdx/vite';
3
5
  import { nitro } from 'nitro/vite';
4
6
  import path from 'node:path';
7
+ import remarkDirective from 'remark-directive';
5
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
+ }
6
15
 
7
16
  export interface ViteConfigOptions {
8
17
  packageRoot: string;
@@ -23,25 +32,40 @@ export async function createViteConfig(
23
32
  nitro({
24
33
  serverDir: path.resolve(packageRoot, 'src/server'),
25
34
  ...(preset && { preset }),
26
- alias: {
27
- '@content': path.resolve(packageRoot, '.content'),
28
- },
29
35
  }),
30
- mdx({}, { index: false }),
31
- react(),
32
- {
33
- name: 'chronicle:content-alias',
34
- resolveId(id) {
35
- if (id.startsWith('@content/')) {
36
- return path.resolve(packageRoot, '.content', id.slice('@content/'.length));
37
- }
38
- },
39
- }
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()
40
65
  ],
41
66
  resolve: {
42
67
  alias: {
43
68
  '@': path.resolve(packageRoot, 'src'),
44
- '@content': path.resolve(packageRoot, '.content'),
45
69
  },
46
70
  conditions: ['module-sync', 'import', 'node'],
47
71
  dedupe: [
@@ -59,8 +83,7 @@ export async function createViteConfig(
59
83
  },
60
84
  define: {
61
85
  __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
62
- __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
63
- __CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
86
+ __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
64
87
  },
65
88
  css: {
66
89
  modules: {
@@ -78,6 +101,11 @@ export async function createViteConfig(
78
101
  }
79
102
  }
80
103
  }
81
- }
104
+ },
105
+ nitro: {
106
+ output: {
107
+ dir: resolveOutputDir(projectRoot, preset),
108
+ },
109
+ },
82
110
  };
83
111
  }
@@ -14,7 +14,8 @@ import { MethodBadge } from '@/components/api/method-badge';
14
14
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
15
  import { Footer } from '@/components/ui/footer';
16
16
  import { Search } from '@/components/ui/search';
17
- import type { PageTreeItem, ThemeLayoutProps } from '@/types';
17
+ import type { Node } from 'fumadocs-core/page-tree';
18
+ import type { ThemeLayoutProps } from '@/types';
18
19
  import styles from './Layout.module.css';
19
20
 
20
21
  const iconMap: Record<string, React.ReactNode> = {
@@ -96,9 +97,9 @@ export function Layout({
96
97
  className={cx(styles.sidebar, classNames?.sidebar)}
97
98
  >
98
99
  <Sidebar.Main ref={scrollRef}>
99
- {tree.children.map(item => (
100
+ {tree.children.map((item, i) => (
100
101
  <SidebarNode
101
- key={item.url ?? item.name}
102
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
102
103
  item={item}
103
104
  pathname={pathname}
104
105
  />
@@ -118,23 +119,24 @@ function SidebarNode({
118
119
  item,
119
120
  pathname
120
121
  }: {
121
- item: PageTreeItem;
122
+ item: Node;
122
123
  pathname: string;
123
124
  }) {
124
125
  if (item.type === 'separator') {
125
126
  return null;
126
127
  }
127
128
 
128
- if (item.type === 'folder' && item.children) {
129
+ if (item.type === 'folder') {
130
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
129
131
  return (
130
132
  <Sidebar.Group
131
- label={item.name}
132
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
133
+ label={item.name?.toString() ?? ''}
134
+ leadingIcon={icon ?? undefined}
133
135
  classNames={{ items: styles.groupItems }}
134
136
  >
135
- {item.children.map(child => (
137
+ {item.children.map((child, i) => (
136
138
  <SidebarNode
137
- key={child.url ?? child.name}
139
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
138
140
  item={child}
139
141
  pathname={pathname}
140
142
  />
@@ -145,13 +147,14 @@ function SidebarNode({
145
147
 
146
148
  const isActive = pathname === item.url;
147
149
  const href = item.url ?? '#';
150
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
148
151
  const link = useMemo(() => <RouterLink to={href} />, [href]);
149
152
 
150
153
  return (
151
154
  <Sidebar.Item
152
155
  href={href}
153
156
  active={isActive}
154
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
157
+ leadingIcon={icon ?? undefined}
155
158
  as={link}
156
159
  >
157
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
+ }