@raystack/chronicle 0.1.0-canary.5a730d4 → 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/dist/cli/index.js +217 -412
- package/package.json +13 -10
- package/src/cli/commands/build.ts +30 -48
- package/src/cli/commands/dev.ts +24 -13
- package/src/cli/commands/init.ts +38 -123
- package/src/cli/commands/serve.ts +35 -50
- package/src/cli/commands/start.ts +20 -16
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +14 -16
- package/src/components/mdx/details.module.css +0 -2
- package/src/components/mdx/image.tsx +5 -20
- package/src/components/mdx/index.tsx +18 -4
- package/src/components/mdx/link.tsx +24 -20
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/footer.tsx +2 -3
- package/src/components/ui/search.tsx +116 -72
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +31 -29
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/head.tsx +26 -22
- package/src/lib/mdx-loader.ts +21 -0
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +74 -58
- package/src/lib/source.ts +136 -114
- package/src/pages/ApiLayout.tsx +22 -18
- package/src/pages/ApiPage.tsx +32 -27
- package/src/pages/DocsLayout.tsx +7 -7
- package/src/pages/DocsPage.tsx +11 -11
- package/src/pages/NotFound.tsx +11 -4
- package/src/server/App.tsx +35 -27
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +17 -0
- package/src/server/api/search.ts +170 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +47 -55
- package/src/server/entry-server.tsx +100 -35
- package/src/server/routes/llms.txt.ts +61 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +40 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +90 -44
- package/src/themes/default/Layout.tsx +78 -47
- package/src/themes/default/Page.module.css +0 -12
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +25 -39
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +63 -43
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +16 -4
- package/src/themes/paper/Page.tsx +56 -62
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +7 -7
- package/src/types/content.ts +5 -21
- package/src/types/globals.d.ts +3 -0
- package/src/types/theme.ts +4 -3
- package/src/cli/__tests__/config.test.ts +0 -25
- package/src/cli/__tests__/scaffold.test.ts +0 -10
- package/src/pages/__tests__/head.test.tsx +0 -57
- package/src/server/__tests__/entry-server.test.tsx +0 -35
- package/src/server/__tests__/handlers.test.ts +0 -77
- package/src/server/__tests__/og.test.ts +0 -23
- package/src/server/__tests__/router.test.ts +0 -72
- package/src/server/__tests__/vite-config.test.ts +0 -25
- package/src/server/dev.ts +0 -156
- package/src/server/entry-prod.ts +0 -127
- package/src/server/handlers/apis-proxy.ts +0 -52
- package/src/server/handlers/health.ts +0 -3
- package/src/server/handlers/llms.ts +0 -58
- package/src/server/handlers/og.ts +0 -87
- package/src/server/handlers/robots.ts +0 -11
- package/src/server/handlers/search.ts +0 -140
- package/src/server/handlers/sitemap.ts +0 -39
- package/src/server/handlers/specs.ts +0 -9
- package/src/server/index.html +0 -12
- package/src/server/prod.ts +0 -18
- package/src/server/router.ts +0 -42
- package/src/themes/default/font.ts +0 -4
|
@@ -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
|
+
}
|
|
@@ -1,65 +1,111 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import mdx from '
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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
|
+
}
|
|
10
15
|
|
|
11
16
|
export interface ViteConfigOptions {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
packageRoot: string;
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
contentDir: string;
|
|
20
|
+
preset?: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
export async function createViteConfig(
|
|
18
|
-
|
|
23
|
+
export async function createViteConfig(
|
|
24
|
+
options: ViteConfigOptions
|
|
25
|
+
): Promise<InlineConfig> {
|
|
26
|
+
const { packageRoot, projectRoot, contentDir, preset } = options;
|
|
19
27
|
|
|
20
28
|
return {
|
|
21
|
-
root,
|
|
29
|
+
root: packageRoot,
|
|
22
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
|
+
],
|
|
23
66
|
resolve: {
|
|
24
67
|
alias: {
|
|
25
|
-
'@': path.resolve(
|
|
26
|
-
'@content': contentDir,
|
|
68
|
+
'@': path.resolve(packageRoot, 'src'),
|
|
27
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
|
+
}
|
|
28
83
|
},
|
|
29
|
-
plugins: [
|
|
30
|
-
mdx({
|
|
31
|
-
remarkPlugins: [
|
|
32
|
-
remarkFrontmatter,
|
|
33
|
-
remarkMdxFrontmatter,
|
|
34
|
-
remarkGfm,
|
|
35
|
-
remarkDirective,
|
|
36
|
-
],
|
|
37
|
-
rehypePlugins: [
|
|
38
|
-
[rehypeShiki, { themes: { light: 'github-light', dark: 'github-dark' } }],
|
|
39
|
-
],
|
|
40
|
-
mdExtensions: ['.md'],
|
|
41
|
-
mdxExtensions: ['.mdx'],
|
|
42
|
-
}),
|
|
43
|
-
react(),
|
|
44
|
-
],
|
|
45
84
|
define: {
|
|
46
|
-
|
|
47
|
-
|
|
85
|
+
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
|
|
86
|
+
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
48
87
|
},
|
|
49
88
|
css: {
|
|
50
89
|
modules: {
|
|
51
|
-
localsConvention: 'camelCase'
|
|
52
|
-
}
|
|
90
|
+
localsConvention: 'camelCase'
|
|
91
|
+
}
|
|
53
92
|
},
|
|
54
93
|
ssr: {
|
|
55
|
-
noExternal: ['@raystack/apsara']
|
|
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
|
+
}
|
|
56
104
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
client: path.resolve(root, 'src/server/index.html'),
|
|
61
|
-
},
|
|
105
|
+
nitro: {
|
|
106
|
+
output: {
|
|
107
|
+
dir: resolveOutputDir(projectRoot, preset),
|
|
62
108
|
},
|
|
63
109
|
},
|
|
64
|
-
}
|
|
110
|
+
};
|
|
65
111
|
}
|
|
@@ -1,64 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import
|
|
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';
|
|
14
20
|
|
|
15
21
|
const iconMap: Record<string, React.ReactNode> = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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' />
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
let savedScrollTop = 0;
|
|
25
31
|
|
|
26
|
-
export function Layout({
|
|
32
|
+
export function Layout({
|
|
33
|
+
children,
|
|
34
|
+
config,
|
|
35
|
+
tree,
|
|
36
|
+
classNames
|
|
37
|
+
}: ThemeLayoutProps) {
|
|
27
38
|
const { pathname } = useLocation();
|
|
28
39
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
29
40
|
|
|
30
41
|
useEffect(() => {
|
|
31
42
|
const el = scrollRef.current;
|
|
32
43
|
if (!el) return;
|
|
33
|
-
const onScroll = () => {
|
|
44
|
+
const onScroll = () => {
|
|
45
|
+
savedScrollTop = el.scrollTop;
|
|
46
|
+
};
|
|
34
47
|
el.addEventListener('scroll', onScroll);
|
|
35
48
|
return () => el.removeEventListener('scroll', onScroll);
|
|
36
49
|
}, []);
|
|
37
50
|
|
|
38
51
|
useEffect(() => {
|
|
39
52
|
const el = scrollRef.current;
|
|
40
|
-
if (el)
|
|
53
|
+
if (el)
|
|
54
|
+
requestAnimationFrame(() => {
|
|
55
|
+
el.scrollTop = savedScrollTop;
|
|
56
|
+
});
|
|
41
57
|
}, [pathname]);
|
|
42
58
|
|
|
43
59
|
return (
|
|
44
|
-
<Flex direction=
|
|
60
|
+
<Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
|
|
45
61
|
<Navbar className={styles.header}>
|
|
46
62
|
<Navbar.Start>
|
|
47
|
-
<
|
|
48
|
-
|
|
63
|
+
<RouterLink
|
|
64
|
+
to='/'
|
|
65
|
+
style={{ textDecoration: 'none', color: 'inherit' }}
|
|
66
|
+
>
|
|
67
|
+
<Headline size='small' weight='medium' as='h1'>
|
|
49
68
|
{config.title}
|
|
50
69
|
</Headline>
|
|
51
|
-
</
|
|
70
|
+
</RouterLink>
|
|
52
71
|
</Navbar.Start>
|
|
53
72
|
<Navbar.End>
|
|
54
|
-
<Flex gap=
|
|
55
|
-
{config.api?.map(
|
|
56
|
-
<
|
|
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
|
+
>
|
|
57
80
|
{api.name} API
|
|
58
|
-
</
|
|
81
|
+
</RouterLink>
|
|
59
82
|
))}
|
|
60
|
-
{config.navigation?.links?.map(
|
|
61
|
-
<Link key={link.href}
|
|
83
|
+
{config.navigation?.links?.map(link => (
|
|
84
|
+
<Link key={link.href} href={link.href}>
|
|
62
85
|
{link.label}
|
|
63
86
|
</Link>
|
|
64
87
|
))}
|
|
@@ -68,18 +91,24 @@ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps)
|
|
|
68
91
|
</Navbar.End>
|
|
69
92
|
</Navbar>
|
|
70
93
|
<Flex className={cx(styles.body, classNames?.body)}>
|
|
71
|
-
<Sidebar
|
|
94
|
+
<Sidebar
|
|
95
|
+
defaultOpen
|
|
96
|
+
collapsible={false}
|
|
97
|
+
className={cx(styles.sidebar, classNames?.sidebar)}
|
|
98
|
+
>
|
|
72
99
|
<Sidebar.Main ref={scrollRef}>
|
|
73
|
-
{tree.children.map((item) => (
|
|
100
|
+
{tree.children.map((item, i) => (
|
|
74
101
|
<SidebarNode
|
|
75
|
-
key={item.url
|
|
102
|
+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
|
|
76
103
|
item={item}
|
|
77
104
|
pathname={pathname}
|
|
78
105
|
/>
|
|
79
106
|
))}
|
|
80
107
|
</Sidebar.Main>
|
|
81
108
|
</Sidebar>
|
|
82
|
-
<main className={cx(styles.content, classNames?.content)}>
|
|
109
|
+
<main className={cx(styles.content, classNames?.content)}>
|
|
110
|
+
{children}
|
|
111
|
+
</main>
|
|
83
112
|
</Flex>
|
|
84
113
|
<Footer config={config.footer} />
|
|
85
114
|
</Flex>
|
|
@@ -88,25 +117,26 @@ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps)
|
|
|
88
117
|
|
|
89
118
|
function SidebarNode({
|
|
90
119
|
item,
|
|
91
|
-
pathname
|
|
120
|
+
pathname
|
|
92
121
|
}: {
|
|
93
|
-
item:
|
|
122
|
+
item: Node;
|
|
94
123
|
pathname: string;
|
|
95
124
|
}) {
|
|
96
|
-
if (item.type ===
|
|
125
|
+
if (item.type === 'separator') {
|
|
97
126
|
return null;
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
if (item.type ===
|
|
129
|
+
if (item.type === 'folder') {
|
|
130
|
+
const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
|
|
101
131
|
return (
|
|
102
132
|
<Sidebar.Group
|
|
103
|
-
label={item.name}
|
|
104
|
-
leadingIcon={
|
|
133
|
+
label={item.name?.toString() ?? ''}
|
|
134
|
+
leadingIcon={icon ?? undefined}
|
|
105
135
|
classNames={{ items: styles.groupItems }}
|
|
106
136
|
>
|
|
107
|
-
{item.children.map((child) => (
|
|
137
|
+
{item.children.map((child, i) => (
|
|
108
138
|
<SidebarNode
|
|
109
|
-
key={child.url
|
|
139
|
+
key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
|
|
110
140
|
item={child}
|
|
111
141
|
pathname={pathname}
|
|
112
142
|
/>
|
|
@@ -116,14 +146,15 @@ function SidebarNode({
|
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
const isActive = pathname === item.url;
|
|
119
|
-
const href = item.url ??
|
|
120
|
-
const
|
|
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]);
|
|
121
152
|
|
|
122
153
|
return (
|
|
123
154
|
<Sidebar.Item
|
|
124
155
|
href={href}
|
|
125
156
|
active={isActive}
|
|
126
|
-
leadingIcon={
|
|
157
|
+
leadingIcon={icon ?? undefined}
|
|
127
158
|
as={link}
|
|
128
159
|
>
|
|
129
160
|
{item.name}
|
|
@@ -38,18 +38,6 @@
|
|
|
38
38
|
margin-bottom: var(--rs-space-3);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
.content :global(pre code span) {
|
|
42
|
-
color: var(--shiki-light);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
:global([data-theme="dark"]) .content :global(pre code span) {
|
|
46
|
-
color: var(--shiki-dark);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
:global([data-theme="dark"]) .content :global(pre) {
|
|
50
|
-
background-color: var(--shiki-dark-bg, #24292e);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
41
|
.content img {
|
|
54
42
|
max-width: 100%;
|
|
55
43
|
height: auto;
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
'use client'
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
import { Flex } from '@raystack/apsara'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
3
|
+
import { Flex } from '@raystack/apsara';
|
|
4
|
+
import { Breadcrumbs } from '@/components/ui/breadcrumbs';
|
|
5
|
+
import type { ThemePageProps } from '@/types';
|
|
6
|
+
import styles from './Page.module.css';
|
|
7
|
+
import { Toc } from './Toc';
|
|
8
8
|
|
|
9
9
|
export function Page({ page, tree }: ThemePageProps) {
|
|
10
10
|
return (
|
|
11
11
|
<Flex className={styles.page}>
|
|
12
|
-
<article className={styles.article}>
|
|
12
|
+
<article className={styles.article} data-article-content>
|
|
13
13
|
<Breadcrumbs slug={page.slug} tree={tree} />
|
|
14
|
-
<div className={styles.content}>
|
|
15
|
-
{page.content}
|
|
16
|
-
</div>
|
|
14
|
+
<div className={styles.content}>{page.content}</div>
|
|
17
15
|
</article>
|
|
18
16
|
<Toc items={page.toc} />
|
|
19
17
|
</Flex>
|
|
20
|
-
)
|
|
18
|
+
);
|
|
21
19
|
}
|