@raystack/chronicle 0.1.0-canary.1f5227c
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/chronicle.js +2 -0
- package/dist/cli/index.js +543 -0
- package/package.json +68 -0
- package/src/cli/__tests__/config.test.ts +25 -0
- package/src/cli/__tests__/scaffold.test.ts +10 -0
- package/src/cli/commands/build.ts +52 -0
- package/src/cli/commands/dev.ts +21 -0
- package/src/cli/commands/init.ts +154 -0
- package/src/cli/commands/serve.ts +55 -0
- package/src/cli/commands/start.ts +24 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/config.ts +43 -0
- package/src/cli/utils/index.ts +2 -0
- package/src/cli/utils/resolve.ts +6 -0
- package/src/cli/utils/scaffold.ts +20 -0
- package/src/components/api/code-snippets.module.css +7 -0
- package/src/components/api/code-snippets.tsx +76 -0
- package/src/components/api/endpoint-page.module.css +58 -0
- package/src/components/api/endpoint-page.tsx +283 -0
- package/src/components/api/field-row.module.css +126 -0
- package/src/components/api/field-row.tsx +204 -0
- package/src/components/api/field-section.module.css +24 -0
- package/src/components/api/field-section.tsx +100 -0
- package/src/components/api/index.ts +8 -0
- package/src/components/api/json-editor.module.css +9 -0
- package/src/components/api/json-editor.tsx +61 -0
- package/src/components/api/key-value-editor.module.css +13 -0
- package/src/components/api/key-value-editor.tsx +62 -0
- package/src/components/api/method-badge.module.css +4 -0
- package/src/components/api/method-badge.tsx +29 -0
- package/src/components/api/response-panel.module.css +8 -0
- package/src/components/api/response-panel.tsx +44 -0
- package/src/components/common/breadcrumb.tsx +3 -0
- package/src/components/common/button.tsx +3 -0
- package/src/components/common/callout.module.css +7 -0
- package/src/components/common/callout.tsx +27 -0
- package/src/components/common/code-block.tsx +3 -0
- package/src/components/common/dialog.tsx +3 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/input-field.tsx +3 -0
- package/src/components/common/sidebar.tsx +3 -0
- package/src/components/common/switch.tsx +3 -0
- package/src/components/common/table.tsx +3 -0
- package/src/components/common/tabs.tsx +3 -0
- package/src/components/mdx/code.module.css +42 -0
- package/src/components/mdx/code.tsx +36 -0
- package/src/components/mdx/details.module.css +14 -0
- package/src/components/mdx/details.tsx +17 -0
- package/src/components/mdx/image.tsx +24 -0
- package/src/components/mdx/index.tsx +35 -0
- package/src/components/mdx/link.tsx +37 -0
- package/src/components/mdx/mermaid.module.css +9 -0
- package/src/components/mdx/mermaid.tsx +37 -0
- package/src/components/mdx/paragraph.module.css +8 -0
- package/src/components/mdx/paragraph.tsx +19 -0
- package/src/components/mdx/table.tsx +40 -0
- package/src/components/ui/breadcrumbs.tsx +72 -0
- package/src/components/ui/client-theme-switcher.tsx +18 -0
- package/src/components/ui/footer.module.css +27 -0
- package/src/components/ui/footer.tsx +31 -0
- package/src/components/ui/search.module.css +111 -0
- package/src/components/ui/search.tsx +174 -0
- package/src/lib/api-routes.ts +120 -0
- package/src/lib/config.ts +56 -0
- package/src/lib/head.tsx +45 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/openapi.ts +188 -0
- package/src/lib/page-context.tsx +95 -0
- package/src/lib/remark-unused-directives.ts +30 -0
- package/src/lib/schema.ts +99 -0
- package/src/lib/snippet-generators.ts +87 -0
- package/src/lib/source.ts +138 -0
- package/src/pages/ApiLayout.module.css +22 -0
- package/src/pages/ApiLayout.tsx +29 -0
- package/src/pages/ApiPage.tsx +68 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +10 -0
- package/src/pages/__tests__/head.test.tsx +57 -0
- package/src/server/App.tsx +59 -0
- package/src/server/__tests__/entry-server.test.tsx +35 -0
- package/src/server/__tests__/handlers.test.ts +77 -0
- package/src/server/__tests__/og.test.ts +23 -0
- package/src/server/__tests__/router.test.ts +72 -0
- package/src/server/__tests__/vite-config.test.ts +25 -0
- package/src/server/dev.ts +156 -0
- package/src/server/entry-client.tsx +74 -0
- package/src/server/entry-prod.ts +127 -0
- package/src/server/entry-server.tsx +35 -0
- package/src/server/handlers/apis-proxy.ts +52 -0
- package/src/server/handlers/health.ts +3 -0
- package/src/server/handlers/llms.ts +58 -0
- package/src/server/handlers/og.ts +87 -0
- package/src/server/handlers/robots.ts +11 -0
- package/src/server/handlers/search.ts +140 -0
- package/src/server/handlers/sitemap.ts +39 -0
- package/src/server/handlers/specs.ts +9 -0
- package/src/server/index.html +12 -0
- package/src/server/prod.ts +18 -0
- package/src/server/router.ts +42 -0
- package/src/server/vite-config.ts +71 -0
- package/src/themes/default/Layout.module.css +81 -0
- package/src/themes/default/Layout.tsx +132 -0
- package/src/themes/default/Page.module.css +102 -0
- package/src/themes/default/Page.tsx +21 -0
- package/src/themes/default/Toc.module.css +48 -0
- package/src/themes/default/Toc.tsx +66 -0
- package/src/themes/default/font.ts +4 -0
- package/src/themes/default/index.ts +13 -0
- package/src/themes/paper/ChapterNav.module.css +71 -0
- package/src/themes/paper/ChapterNav.tsx +95 -0
- package/src/themes/paper/Layout.module.css +33 -0
- package/src/themes/paper/Layout.tsx +25 -0
- package/src/themes/paper/Page.module.css +174 -0
- package/src/themes/paper/Page.tsx +106 -0
- package/src/themes/paper/ReadingProgress.module.css +132 -0
- package/src/themes/paper/ReadingProgress.tsx +294 -0
- package/src/themes/paper/index.ts +8 -0
- package/src/themes/registry.ts +14 -0
- package/src/types/config.ts +80 -0
- package/src/types/content.ts +36 -0
- package/src/types/index.ts +3 -0
- package/src/types/theme.ts +22 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import matter from 'gray-matter'
|
|
4
|
+
import MiniSearch from 'minisearch'
|
|
5
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
6
|
+
import { loadConfig } from '@/lib/config'
|
|
7
|
+
import { loadApiSpecs, type ApiSpec } from '@/lib/openapi'
|
|
8
|
+
import { getSpecSlug } from '@/lib/api-routes'
|
|
9
|
+
|
|
10
|
+
interface SearchDocument {
|
|
11
|
+
id: string
|
|
12
|
+
url: string
|
|
13
|
+
title: string
|
|
14
|
+
content: string
|
|
15
|
+
type: 'page' | 'api'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let searchIndex: MiniSearch<SearchDocument> | null = null
|
|
19
|
+
|
|
20
|
+
function getContentDir(): string {
|
|
21
|
+
return process.env.CHRONICLE_CONTENT_DIR || path.join(process.cwd(), 'content')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function scanContent(): Promise<SearchDocument[]> {
|
|
25
|
+
const contentDir = getContentDir()
|
|
26
|
+
const docs: SearchDocument[] = []
|
|
27
|
+
|
|
28
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
29
|
+
let entries
|
|
30
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) }
|
|
31
|
+
catch { return }
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
35
|
+
const fullPath = path.join(dir, entry.name)
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
await scan(fullPath, [...prefix, entry.name])
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md')) continue
|
|
43
|
+
|
|
44
|
+
const raw = await fs.readFile(fullPath, 'utf-8')
|
|
45
|
+
const { data: fm, content } = matter(raw)
|
|
46
|
+
const baseName = entry.name.replace(/\.(mdx|md)$/, '')
|
|
47
|
+
const slugs = baseName === 'index' ? prefix : [...prefix, baseName]
|
|
48
|
+
const url = slugs.length === 0 ? '/' : '/' + slugs.join('/')
|
|
49
|
+
|
|
50
|
+
docs.push({
|
|
51
|
+
id: url,
|
|
52
|
+
url,
|
|
53
|
+
title: fm.title ?? baseName,
|
|
54
|
+
content: content.slice(0, 5000),
|
|
55
|
+
type: 'page',
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await scan(contentDir)
|
|
61
|
+
return docs
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildApiDocs(): SearchDocument[] {
|
|
65
|
+
const config = loadConfig()
|
|
66
|
+
if (!config.api?.length) return []
|
|
67
|
+
|
|
68
|
+
const docs: SearchDocument[] = []
|
|
69
|
+
const specs = loadApiSpecs(config.api)
|
|
70
|
+
|
|
71
|
+
for (const spec of specs) {
|
|
72
|
+
const specSlug = getSpecSlug(spec)
|
|
73
|
+
const paths = spec.document.paths ?? {}
|
|
74
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
75
|
+
if (!pathItem) continue
|
|
76
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
77
|
+
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined
|
|
78
|
+
if (!op?.operationId) continue
|
|
79
|
+
const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`
|
|
80
|
+
docs.push({
|
|
81
|
+
id: url,
|
|
82
|
+
url,
|
|
83
|
+
title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
|
|
84
|
+
content: op.description ?? '',
|
|
85
|
+
type: 'api',
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return docs
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getIndex(): Promise<MiniSearch<SearchDocument>> {
|
|
95
|
+
if (searchIndex) return searchIndex
|
|
96
|
+
|
|
97
|
+
const [contentDocs, apiDocs] = await Promise.all([
|
|
98
|
+
scanContent(),
|
|
99
|
+
Promise.resolve(buildApiDocs()),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
searchIndex = new MiniSearch<SearchDocument>({
|
|
103
|
+
fields: ['title', 'content'],
|
|
104
|
+
storeFields: ['url', 'title', 'type'],
|
|
105
|
+
searchOptions: {
|
|
106
|
+
boost: { title: 2 },
|
|
107
|
+
fuzzy: 0.2,
|
|
108
|
+
prefix: true,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
searchIndex.addAll([...contentDocs, ...apiDocs])
|
|
113
|
+
return searchIndex
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function handleSearch(req: Request): Promise<Response> {
|
|
117
|
+
const url = new URL(req.url)
|
|
118
|
+
const query = url.searchParams.get('query') ?? ''
|
|
119
|
+
const index = await getIndex()
|
|
120
|
+
|
|
121
|
+
if (!query) {
|
|
122
|
+
const contentDocs = await scanContent()
|
|
123
|
+
const suggestions = contentDocs.slice(0, 8).map((d) => ({
|
|
124
|
+
id: d.id,
|
|
125
|
+
url: d.url,
|
|
126
|
+
type: d.type,
|
|
127
|
+
content: d.title,
|
|
128
|
+
}))
|
|
129
|
+
return Response.json(suggestions)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const results = index.search(query).map((r) => ({
|
|
133
|
+
id: r.id,
|
|
134
|
+
url: r.url,
|
|
135
|
+
type: r.type,
|
|
136
|
+
content: r.title,
|
|
137
|
+
}))
|
|
138
|
+
|
|
139
|
+
return Response.json(results)
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
import { getPages } from '@/lib/source'
|
|
3
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
4
|
+
import { buildApiRoutes } from '@/lib/api-routes'
|
|
5
|
+
|
|
6
|
+
export async function handleSitemap(): Promise<Response> {
|
|
7
|
+
const config = loadConfig()
|
|
8
|
+
if (!config.url) {
|
|
9
|
+
return new Response('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>', {
|
|
10
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const baseUrl = config.url.replace(/\/$/, '')
|
|
15
|
+
|
|
16
|
+
const pages = await getPages()
|
|
17
|
+
const docPages = pages.map((page) => {
|
|
18
|
+
const lastmod = page.frontmatter.lastModified
|
|
19
|
+
? `<lastmod>${new Date(page.frontmatter.lastModified).toISOString()}</lastmod>`
|
|
20
|
+
: ''
|
|
21
|
+
return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const apiPages = config.api?.length
|
|
25
|
+
? buildApiRoutes(loadApiSpecs(config.api)).map((route) =>
|
|
26
|
+
`<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
|
|
27
|
+
)
|
|
28
|
+
: []
|
|
29
|
+
|
|
30
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
32
|
+
<url><loc>${baseUrl}</loc></url>
|
|
33
|
+
${[...docPages, ...apiPages].join('\n')}
|
|
34
|
+
</urlset>`
|
|
35
|
+
|
|
36
|
+
return new Response(xml, {
|
|
37
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
3
|
+
|
|
4
|
+
export function handleSpecs(): Response {
|
|
5
|
+
const config = loadConfig()
|
|
6
|
+
const specs = config.api?.length ? loadApiSpecs(config.api) : []
|
|
7
|
+
|
|
8
|
+
return Response.json(specs)
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<!--head-outlet-->
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"><!--ssr-outlet--></div>
|
|
10
|
+
<script type="module" src="/src/server/entry-client.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
export interface ProdServerOptions {
|
|
5
|
+
port: number
|
|
6
|
+
root: string
|
|
7
|
+
distDir: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function startProdServer(options: ProdServerOptions) {
|
|
11
|
+
const { port, distDir } = options
|
|
12
|
+
|
|
13
|
+
const serverEntry = path.resolve(distDir, 'server/entry-prod.js')
|
|
14
|
+
const { startServer } = await import(serverEntry)
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan('Starting production server...'))
|
|
17
|
+
return startServer({ port, distDir })
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { handleHealth } from './handlers/health'
|
|
2
|
+
import { handleSearch } from './handlers/search'
|
|
3
|
+
import { handleApisProxy } from './handlers/apis-proxy'
|
|
4
|
+
import { handleOg } from './handlers/og'
|
|
5
|
+
import { handleLlms } from './handlers/llms'
|
|
6
|
+
import { handleSitemap } from './handlers/sitemap'
|
|
7
|
+
import { handleRobots } from './handlers/robots'
|
|
8
|
+
import { handleSpecs } from './handlers/specs'
|
|
9
|
+
|
|
10
|
+
export type RouteHandler = (req: Request) => Response | Promise<Response>
|
|
11
|
+
|
|
12
|
+
interface Route {
|
|
13
|
+
pattern: URLPattern
|
|
14
|
+
handler: RouteHandler
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const routes: Route[] = []
|
|
18
|
+
|
|
19
|
+
function addRoute(path: string, handler: RouteHandler) {
|
|
20
|
+
routes.push({
|
|
21
|
+
pattern: new URLPattern({ pathname: path }),
|
|
22
|
+
handler,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addRoute('/api/health', handleHealth)
|
|
27
|
+
addRoute('/api/search', handleSearch)
|
|
28
|
+
addRoute('/api/apis-proxy', handleApisProxy)
|
|
29
|
+
addRoute('/api/specs', handleSpecs)
|
|
30
|
+
addRoute('/og', handleOg)
|
|
31
|
+
addRoute('/llms.txt', handleLlms)
|
|
32
|
+
addRoute('/sitemap.xml', handleSitemap)
|
|
33
|
+
addRoute('/robots.txt', handleRobots)
|
|
34
|
+
|
|
35
|
+
export function matchRoute(url: string): RouteHandler | null {
|
|
36
|
+
for (const route of routes) {
|
|
37
|
+
if (route.pattern.test(url)) {
|
|
38
|
+
return route.handler
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { type InlineConfig } from 'vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import mdx from '@mdx-js/rollup'
|
|
5
|
+
import remarkDirective from 'remark-directive'
|
|
6
|
+
import remarkGfm from 'remark-gfm'
|
|
7
|
+
import remarkFrontmatter from 'remark-frontmatter'
|
|
8
|
+
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
|
|
9
|
+
import rehypeShiki from '@shikijs/rehype'
|
|
10
|
+
|
|
11
|
+
export interface ViteConfigOptions {
|
|
12
|
+
root: string
|
|
13
|
+
contentDir: string
|
|
14
|
+
isDev?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createViteConfig(options: ViteConfigOptions): Promise<InlineConfig> {
|
|
18
|
+
const { root, contentDir, isDev = false } = options
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
root,
|
|
22
|
+
configFile: false,
|
|
23
|
+
resolve: {
|
|
24
|
+
alias: {
|
|
25
|
+
'@': path.resolve(root, 'src'),
|
|
26
|
+
'@content': contentDir,
|
|
27
|
+
},
|
|
28
|
+
dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
29
|
+
},
|
|
30
|
+
server: {
|
|
31
|
+
fs: {
|
|
32
|
+
allow: [root, contentDir],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
plugins: [
|
|
36
|
+
mdx({
|
|
37
|
+
remarkPlugins: [
|
|
38
|
+
remarkFrontmatter,
|
|
39
|
+
remarkMdxFrontmatter,
|
|
40
|
+
remarkGfm,
|
|
41
|
+
remarkDirective,
|
|
42
|
+
],
|
|
43
|
+
rehypePlugins: [
|
|
44
|
+
[rehypeShiki, { themes: { light: 'github-light', dark: 'github-dark' } }],
|
|
45
|
+
],
|
|
46
|
+
mdExtensions: ['.md'],
|
|
47
|
+
mdxExtensions: ['.mdx'],
|
|
48
|
+
}),
|
|
49
|
+
react(),
|
|
50
|
+
],
|
|
51
|
+
define: {
|
|
52
|
+
'process.env.CHRONICLE_CONTENT_DIR': JSON.stringify(contentDir),
|
|
53
|
+
'process.env.CHRONICLE_PROJECT_ROOT': JSON.stringify(root),
|
|
54
|
+
},
|
|
55
|
+
css: {
|
|
56
|
+
modules: {
|
|
57
|
+
localsConvention: 'camelCase',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
ssr: {
|
|
61
|
+
noExternal: ['@raystack/apsara'],
|
|
62
|
+
},
|
|
63
|
+
build: {
|
|
64
|
+
rolldownOptions: {
|
|
65
|
+
input: isDev ? undefined : {
|
|
66
|
+
client: path.resolve(root, 'src/server/index.html'),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
.layout {
|
|
2
|
+
min-height: 100vh;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
border-bottom: 1px solid var(--rs-color-border-base-primary);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.search {
|
|
10
|
+
margin-left: var(--rs-space-5);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.body {
|
|
14
|
+
flex: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.sidebar {
|
|
18
|
+
width: 260px;
|
|
19
|
+
position: sticky;
|
|
20
|
+
top: 0;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.content {
|
|
25
|
+
flex: 1;
|
|
26
|
+
padding: var(--rs-space-9);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sidebarList {
|
|
30
|
+
list-style: none;
|
|
31
|
+
padding: 0;
|
|
32
|
+
margin: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.separator {
|
|
36
|
+
height: 1px;
|
|
37
|
+
background: var(--rs-color-border-base-primary);
|
|
38
|
+
margin: var(--rs-space-3) 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.folder {
|
|
42
|
+
margin-bottom: var(--rs-space-3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.folderLabel {
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
font-size: 0.875rem;
|
|
48
|
+
color: var(--rs-color-text-base-secondary);
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
letter-spacing: 0.05em;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.folder > .sidebarList {
|
|
54
|
+
margin-top: var(--rs-space-2);
|
|
55
|
+
padding-left: var(--rs-space-4);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.navButton {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
height: 32px;
|
|
62
|
+
padding: 0 var(--rs-space-4);
|
|
63
|
+
border: 1px solid var(--rs-color-border-base-primary);
|
|
64
|
+
border-radius: var(--rs-radius-2);
|
|
65
|
+
font-size: var(--rs-font-size-small);
|
|
66
|
+
font-weight: var(--rs-font-weight-medium);
|
|
67
|
+
color: var(--rs-color-foreground-base-primary);
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.navButton:hover {
|
|
72
|
+
background: var(--rs-color-background-base-primary-hover);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.groupItems {
|
|
76
|
+
padding-left: var(--rs-space-4);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.page {
|
|
80
|
+
padding: var(--rs-space-2) 0;
|
|
81
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useEffect, useRef } from "react";
|
|
4
|
+
import { useLocation, Link } from "react-router-dom";
|
|
5
|
+
import { cx } from "class-variance-authority";
|
|
6
|
+
import { Flex, Navbar, Headline, Sidebar, Button } from "@raystack/apsara";
|
|
7
|
+
import { RectangleStackIcon } from "@heroicons/react/24/outline";
|
|
8
|
+
import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
|
|
9
|
+
import { Search } from "@/components/ui/search";
|
|
10
|
+
import { Footer } from "@/components/ui/footer";
|
|
11
|
+
import { MethodBadge } from "@/components/api/method-badge";
|
|
12
|
+
import type { ThemeLayoutProps, PageTreeItem } from "@/types";
|
|
13
|
+
import styles from "./Layout.module.css";
|
|
14
|
+
|
|
15
|
+
const iconMap: Record<string, React.ReactNode> = {
|
|
16
|
+
"rectangle-stack": <RectangleStackIcon width={16} height={16} />,
|
|
17
|
+
"method-get": <MethodBadge method="GET" size="micro" />,
|
|
18
|
+
"method-post": <MethodBadge method="POST" size="micro" />,
|
|
19
|
+
"method-put": <MethodBadge method="PUT" size="micro" />,
|
|
20
|
+
"method-delete": <MethodBadge method="DELETE" size="micro" />,
|
|
21
|
+
"method-patch": <MethodBadge method="PATCH" size="micro" />,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let savedScrollTop = 0;
|
|
25
|
+
|
|
26
|
+
export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
|
|
27
|
+
const { pathname } = useLocation();
|
|
28
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const el = scrollRef.current;
|
|
32
|
+
if (!el) return;
|
|
33
|
+
const onScroll = () => { savedScrollTop = el.scrollTop; };
|
|
34
|
+
el.addEventListener('scroll', onScroll);
|
|
35
|
+
return () => el.removeEventListener('scroll', onScroll);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const el = scrollRef.current;
|
|
40
|
+
if (el) requestAnimationFrame(() => { el.scrollTop = savedScrollTop; });
|
|
41
|
+
}, [pathname]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
|
|
45
|
+
<Navbar className={styles.header}>
|
|
46
|
+
<Navbar.Start>
|
|
47
|
+
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
48
|
+
<Headline size="small" weight="medium" as="h1">
|
|
49
|
+
{config.title}
|
|
50
|
+
</Headline>
|
|
51
|
+
</Link>
|
|
52
|
+
</Navbar.Start>
|
|
53
|
+
<Navbar.End>
|
|
54
|
+
<Flex gap="medium" align="center" className={styles.navActions}>
|
|
55
|
+
{config.api?.map((api) => (
|
|
56
|
+
<Link key={api.name} to={api.basePath} className={styles.navButton}>
|
|
57
|
+
{api.name} API
|
|
58
|
+
</Link>
|
|
59
|
+
))}
|
|
60
|
+
{config.navigation?.links?.map((link) => (
|
|
61
|
+
<Link key={link.href} to={link.href}>
|
|
62
|
+
{link.label}
|
|
63
|
+
</Link>
|
|
64
|
+
))}
|
|
65
|
+
{config.search?.enabled && <Search />}
|
|
66
|
+
</Flex>
|
|
67
|
+
<ClientThemeSwitcher size={16} />
|
|
68
|
+
</Navbar.End>
|
|
69
|
+
</Navbar>
|
|
70
|
+
<Flex className={cx(styles.body, classNames?.body)}>
|
|
71
|
+
<Sidebar defaultOpen collapsible={false} className={cx(styles.sidebar, classNames?.sidebar)}>
|
|
72
|
+
<Sidebar.Main ref={scrollRef}>
|
|
73
|
+
{tree.children.map((item) => (
|
|
74
|
+
<SidebarNode
|
|
75
|
+
key={item.url ?? item.name}
|
|
76
|
+
item={item}
|
|
77
|
+
pathname={pathname}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</Sidebar.Main>
|
|
81
|
+
</Sidebar>
|
|
82
|
+
<main className={cx(styles.content, classNames?.content)}>{children}</main>
|
|
83
|
+
</Flex>
|
|
84
|
+
<Footer config={config.footer} />
|
|
85
|
+
</Flex>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function SidebarNode({
|
|
90
|
+
item,
|
|
91
|
+
pathname,
|
|
92
|
+
}: {
|
|
93
|
+
item: PageTreeItem;
|
|
94
|
+
pathname: string;
|
|
95
|
+
}) {
|
|
96
|
+
if (item.type === "separator") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (item.type === "folder" && item.children) {
|
|
101
|
+
return (
|
|
102
|
+
<Sidebar.Group
|
|
103
|
+
label={item.name}
|
|
104
|
+
leadingIcon={item.icon ? iconMap[item.icon] : undefined}
|
|
105
|
+
classNames={{ items: styles.groupItems }}
|
|
106
|
+
>
|
|
107
|
+
{item.children.map((child) => (
|
|
108
|
+
<SidebarNode
|
|
109
|
+
key={child.url ?? child.name}
|
|
110
|
+
item={child}
|
|
111
|
+
pathname={pathname}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</Sidebar.Group>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isActive = pathname === item.url;
|
|
119
|
+
const href = item.url ?? "#";
|
|
120
|
+
const link = useMemo(() => <Link to={href} />, [href]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Sidebar.Item
|
|
124
|
+
href={href}
|
|
125
|
+
active={isActive}
|
|
126
|
+
leadingIcon={item.icon ? iconMap[item.icon] : undefined}
|
|
127
|
+
as={link}
|
|
128
|
+
>
|
|
129
|
+
{item.name}
|
|
130
|
+
</Sidebar.Item>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
gap: var(--rs-space-9);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.article {
|
|
6
|
+
flex: 1;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
max-width: 768px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.content {
|
|
12
|
+
line-height: 1.7;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.content h1,
|
|
16
|
+
.content h2,
|
|
17
|
+
.content h3,
|
|
18
|
+
.content h4,
|
|
19
|
+
.content h5,
|
|
20
|
+
.content h6 {
|
|
21
|
+
margin-top: var(--rs-space-8);
|
|
22
|
+
margin-bottom: var(--rs-space-5);
|
|
23
|
+
line-height: 1.4;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.content ul,
|
|
27
|
+
.content ol {
|
|
28
|
+
padding-left: var(--rs-space-5);
|
|
29
|
+
margin-bottom: var(--rs-space-5);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.content li {
|
|
33
|
+
font-size: var(--rs-font-size-regular);
|
|
34
|
+
margin: var(--rs-space-2) 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.content [role="tablist"] {
|
|
38
|
+
margin-bottom: var(--rs-space-3);
|
|
39
|
+
}
|
|
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
|
+
.content img {
|
|
54
|
+
max-width: 100%;
|
|
55
|
+
height: auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.content table {
|
|
59
|
+
display: block;
|
|
60
|
+
max-width: 100%;
|
|
61
|
+
overflow-x: auto;
|
|
62
|
+
margin-bottom: var(--rs-space-5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.content details {
|
|
66
|
+
border: 1px solid var(--rs-color-border-base-primary);
|
|
67
|
+
border-radius: var(--rs-radius-2);
|
|
68
|
+
margin: var(--rs-space-5) 0;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.content details summary {
|
|
73
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
font-weight: 500;
|
|
76
|
+
font-size: var(--rs-font-size-small);
|
|
77
|
+
color: var(--rs-color-text-base-primary);
|
|
78
|
+
background: var(--rs-color-background-base-secondary);
|
|
79
|
+
list-style: none;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: var(--rs-space-3);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.content details summary::-webkit-details-marker {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.content details summary::before {
|
|
90
|
+
content: '▶';
|
|
91
|
+
font-size: 10px;
|
|
92
|
+
transition: transform 0.2s ease;
|
|
93
|
+
color: var(--rs-color-text-base-secondary);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.content details[open] > summary::before {
|
|
97
|
+
transform: rotate(90deg);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.content details > :not(summary) {
|
|
101
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
102
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Flex } from '@raystack/apsara'
|
|
4
|
+
import type { ThemePageProps } from '@/types'
|
|
5
|
+
import { Breadcrumbs } from '@/components/ui/breadcrumbs'
|
|
6
|
+
import { Toc } from './Toc'
|
|
7
|
+
import styles from './Page.module.css'
|
|
8
|
+
|
|
9
|
+
export function Page({ page, tree }: ThemePageProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Flex className={styles.page}>
|
|
12
|
+
<article className={styles.article}>
|
|
13
|
+
<Breadcrumbs slug={page.slug} tree={tree} />
|
|
14
|
+
<div className={styles.content}>
|
|
15
|
+
{page.content}
|
|
16
|
+
</div>
|
|
17
|
+
</article>
|
|
18
|
+
<Toc items={page.toc} />
|
|
19
|
+
</Flex>
|
|
20
|
+
)
|
|
21
|
+
}
|