@raystack/chronicle 0.1.0-canary.0efaef0

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 (115) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +276 -0
  3. package/package.json +71 -0
  4. package/src/cli/commands/build.ts +34 -0
  5. package/src/cli/commands/dev.ts +32 -0
  6. package/src/cli/commands/init.ts +69 -0
  7. package/src/cli/commands/serve.ts +40 -0
  8. package/src/cli/commands/start.ts +28 -0
  9. package/src/cli/index.ts +21 -0
  10. package/src/cli/utils/config.ts +42 -0
  11. package/src/cli/utils/index.ts +3 -0
  12. package/src/cli/utils/resolve.ts +10 -0
  13. package/src/cli/utils/scaffold.ts +18 -0
  14. package/src/components/api/code-snippets.module.css +7 -0
  15. package/src/components/api/code-snippets.tsx +76 -0
  16. package/src/components/api/endpoint-page.module.css +58 -0
  17. package/src/components/api/endpoint-page.tsx +283 -0
  18. package/src/components/api/field-row.module.css +126 -0
  19. package/src/components/api/field-row.tsx +204 -0
  20. package/src/components/api/field-section.module.css +24 -0
  21. package/src/components/api/field-section.tsx +100 -0
  22. package/src/components/api/index.ts +8 -0
  23. package/src/components/api/json-editor.module.css +9 -0
  24. package/src/components/api/json-editor.tsx +61 -0
  25. package/src/components/api/key-value-editor.module.css +13 -0
  26. package/src/components/api/key-value-editor.tsx +62 -0
  27. package/src/components/api/method-badge.module.css +4 -0
  28. package/src/components/api/method-badge.tsx +29 -0
  29. package/src/components/api/response-panel.module.css +8 -0
  30. package/src/components/api/response-panel.tsx +44 -0
  31. package/src/components/common/breadcrumb.tsx +3 -0
  32. package/src/components/common/button.tsx +3 -0
  33. package/src/components/common/callout.module.css +7 -0
  34. package/src/components/common/callout.tsx +27 -0
  35. package/src/components/common/code-block.tsx +3 -0
  36. package/src/components/common/dialog.tsx +3 -0
  37. package/src/components/common/index.ts +10 -0
  38. package/src/components/common/input-field.tsx +3 -0
  39. package/src/components/common/sidebar.tsx +3 -0
  40. package/src/components/common/switch.tsx +3 -0
  41. package/src/components/common/table.tsx +3 -0
  42. package/src/components/common/tabs.tsx +3 -0
  43. package/src/components/mdx/code.module.css +42 -0
  44. package/src/components/mdx/code.tsx +27 -0
  45. package/src/components/mdx/details.module.css +37 -0
  46. package/src/components/mdx/details.tsx +18 -0
  47. package/src/components/mdx/image.tsx +9 -0
  48. package/src/components/mdx/index.tsx +35 -0
  49. package/src/components/mdx/link.tsx +41 -0
  50. package/src/components/mdx/mermaid.module.css +9 -0
  51. package/src/components/mdx/mermaid.tsx +37 -0
  52. package/src/components/mdx/paragraph.module.css +8 -0
  53. package/src/components/mdx/paragraph.tsx +19 -0
  54. package/src/components/mdx/table.tsx +40 -0
  55. package/src/components/ui/breadcrumbs.tsx +72 -0
  56. package/src/components/ui/client-theme-switcher.tsx +18 -0
  57. package/src/components/ui/footer.module.css +27 -0
  58. package/src/components/ui/footer.tsx +30 -0
  59. package/src/components/ui/search.module.css +111 -0
  60. package/src/components/ui/search.tsx +218 -0
  61. package/src/lib/api-routes.ts +120 -0
  62. package/src/lib/config.ts +58 -0
  63. package/src/lib/get-llm-text.ts +10 -0
  64. package/src/lib/head.tsx +49 -0
  65. package/src/lib/index.ts +2 -0
  66. package/src/lib/openapi.ts +188 -0
  67. package/src/lib/page-context.tsx +117 -0
  68. package/src/lib/remark-unused-directives.ts +30 -0
  69. package/src/lib/schema.ts +99 -0
  70. package/src/lib/snippet-generators.ts +87 -0
  71. package/src/lib/source.ts +186 -0
  72. package/src/pages/ApiLayout.module.css +22 -0
  73. package/src/pages/ApiLayout.tsx +33 -0
  74. package/src/pages/ApiPage.tsx +73 -0
  75. package/src/pages/DocsLayout.tsx +18 -0
  76. package/src/pages/DocsPage.tsx +43 -0
  77. package/src/pages/NotFound.tsx +17 -0
  78. package/src/server/App.tsx +67 -0
  79. package/src/server/api/apis-proxy.ts +69 -0
  80. package/src/server/api/health.ts +5 -0
  81. package/src/server/api/page/[...slug].ts +18 -0
  82. package/src/server/api/search.ts +170 -0
  83. package/src/server/api/specs.ts +9 -0
  84. package/src/server/build-search-index.ts +117 -0
  85. package/src/server/entry-client.tsx +73 -0
  86. package/src/server/entry-server.tsx +95 -0
  87. package/src/server/routes/llms.txt.ts +61 -0
  88. package/src/server/routes/og.tsx +75 -0
  89. package/src/server/routes/robots.txt.ts +11 -0
  90. package/src/server/routes/sitemap.xml.ts +39 -0
  91. package/src/server/utils/safe-path.ts +17 -0
  92. package/src/server/vite-config.ts +71 -0
  93. package/src/themes/default/Layout.module.css +81 -0
  94. package/src/themes/default/Layout.tsx +160 -0
  95. package/src/themes/default/Page.module.css +46 -0
  96. package/src/themes/default/Page.tsx +19 -0
  97. package/src/themes/default/Toc.module.css +48 -0
  98. package/src/themes/default/Toc.tsx +68 -0
  99. package/src/themes/default/index.ts +11 -0
  100. package/src/themes/paper/ChapterNav.module.css +71 -0
  101. package/src/themes/paper/ChapterNav.tsx +115 -0
  102. package/src/themes/paper/Layout.module.css +33 -0
  103. package/src/themes/paper/Layout.tsx +37 -0
  104. package/src/themes/paper/Page.module.css +181 -0
  105. package/src/themes/paper/Page.tsx +126 -0
  106. package/src/themes/paper/ReadingProgress.module.css +132 -0
  107. package/src/themes/paper/ReadingProgress.tsx +315 -0
  108. package/src/themes/paper/index.ts +8 -0
  109. package/src/themes/registry.ts +14 -0
  110. package/src/types/config.ts +80 -0
  111. package/src/types/content.ts +36 -0
  112. package/src/types/globals.d.ts +4 -0
  113. package/src/types/index.ts +3 -0
  114. package/src/types/theme.ts +22 -0
  115. package/tsconfig.json +29 -0
@@ -0,0 +1,117 @@
1
+ import React, {
2
+ createContext,
3
+ type ReactNode,
4
+ useContext,
5
+ useEffect,
6
+ useState
7
+ } from 'react';
8
+ import { useLocation } from 'react-router';
9
+ import { mdxComponents } from '@/components/mdx';
10
+ import type { ApiSpec } from '@/lib/openapi';
11
+ import type { ChronicleConfig, Frontmatter, PageTree } from '@/types';
12
+
13
+ interface PageData {
14
+ slug: string[];
15
+ frontmatter: Frontmatter;
16
+ content: ReactNode;
17
+ }
18
+
19
+ interface PageContextValue {
20
+ config: ChronicleConfig;
21
+ tree: PageTree;
22
+ page: PageData | null;
23
+ apiSpecs: ApiSpec[];
24
+ }
25
+
26
+ const PageContext = createContext<PageContextValue | null>(null);
27
+
28
+ export function usePageContext(): PageContextValue {
29
+ const ctx = useContext(PageContext);
30
+ if (!ctx) {
31
+ console.error('usePageContext: no context found!');
32
+ return {
33
+ config: { title: 'Documentation' },
34
+ tree: { name: 'root', children: [] },
35
+ page: null,
36
+ apiSpecs: []
37
+ };
38
+ }
39
+ return ctx;
40
+ }
41
+
42
+ interface PageProviderProps {
43
+ initialConfig: ChronicleConfig;
44
+ initialTree: PageTree;
45
+ initialPage: PageData | null;
46
+ initialApiSpecs: ApiSpec[];
47
+ children: ReactNode;
48
+ }
49
+
50
+ async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
51
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
52
+ const mod = relativePath.endsWith('.md')
53
+ ? await import(`../../.content/${withoutExt}.md`)
54
+ : await import(`../../.content/${withoutExt}.mdx`);
55
+ return mod.default
56
+ ? React.createElement(mod.default, { components: mdxComponents })
57
+ : null;
58
+ }
59
+
60
+ export function PageProvider({
61
+ initialConfig,
62
+ initialTree,
63
+ initialPage,
64
+ initialApiSpecs,
65
+ children
66
+ }: PageProviderProps) {
67
+ const { pathname } = useLocation();
68
+ const [tree] = useState<PageTree>(initialTree);
69
+ const [page, setPage] = useState<PageData | null>(initialPage);
70
+ const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
71
+ const [currentPath, setCurrentPath] = useState(pathname);
72
+
73
+ useEffect(() => {
74
+ if (pathname === currentPath) return;
75
+ setCurrentPath(pathname);
76
+
77
+ const cancelled = { current: false };
78
+
79
+ if (pathname.startsWith('/apis')) {
80
+ if (apiSpecs.length === 0) {
81
+ fetch('/api/specs')
82
+ .then(res => res.json())
83
+ .then(specs => {
84
+ if (!cancelled.current) setApiSpecs(specs);
85
+ })
86
+ .catch(() => {});
87
+ }
88
+ return () => { cancelled.current = true; };
89
+ }
90
+
91
+ const slug = pathname === '/'
92
+ ? []
93
+ : pathname.slice(1).split('/').filter(Boolean);
94
+
95
+ const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
96
+
97
+ fetch(apiPath)
98
+ .then(res => res.json())
99
+ .then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
100
+ if (cancelled.current) return;
101
+ const content = await loadMdxComponent(data.relativePath);
102
+ if (cancelled.current) return;
103
+ setPage({ slug, frontmatter: data.frontmatter, content });
104
+ })
105
+ .catch(() => {});
106
+
107
+ return () => { cancelled.current = true; };
108
+ }, [pathname]);
109
+
110
+ return (
111
+ <PageContext.Provider
112
+ value={{ config: initialConfig, tree, page, apiSpecs }}
113
+ >
114
+ {children}
115
+ </PageContext.Provider>
116
+ );
117
+ }
@@ -0,0 +1,30 @@
1
+ import { visit } from 'unist-util-visit'
2
+ import type { Plugin } from 'unified'
3
+ import type { Node } from 'unist'
4
+
5
+ const remarkUnusedDirectives: Plugin = () => {
6
+ return (tree) => {
7
+ visit(tree, ['textDirective'], (node) => {
8
+ const directive = node as Node & {
9
+ name?: string
10
+ attributes?: Record<string, string>
11
+ children?: Node[]
12
+ value?: string
13
+ [key: string]: unknown
14
+ }
15
+ if (!directive.data) {
16
+ const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0
17
+ const hasChildren = directive.children && directive.children.length > 0
18
+ if (!hasAttributes && !hasChildren) {
19
+ const name = directive.name
20
+ if (!name) return
21
+ Object.keys(directive).forEach((key) => delete directive[key])
22
+ directive.type = 'text'
23
+ directive.value = `:${name}`
24
+ }
25
+ }
26
+ })
27
+ }
28
+ }
29
+
30
+ export default remarkUnusedDirectives
@@ -0,0 +1,99 @@
1
+ import type { OpenAPIV3 } from 'openapi-types'
2
+
3
+ export interface SchemaField {
4
+ name: string
5
+ type: string
6
+ required: boolean
7
+ description?: string
8
+ default?: unknown
9
+ enum?: unknown[]
10
+ children?: SchemaField[]
11
+ }
12
+
13
+ export function flattenSchema(
14
+ schema: OpenAPIV3.SchemaObject,
15
+ requiredFields: string[] = [],
16
+ ): SchemaField[] {
17
+ if (schema.type === 'array' && schema.items) {
18
+ const items = schema.items as OpenAPIV3.SchemaObject
19
+ const itemType = inferType(items)
20
+ const children =
21
+ itemType === 'object' || items.properties
22
+ ? flattenSchema(items, items.required ?? [])
23
+ : itemType.endsWith('[]') && (items as OpenAPIV3.ArraySchemaObject).items
24
+ ? flattenSchema((items as OpenAPIV3.ArraySchemaObject).items as OpenAPIV3.SchemaObject)
25
+ : undefined
26
+ return [{
27
+ name: 'items',
28
+ type: `${itemType}[]`,
29
+ required: true,
30
+ description: items.description,
31
+ children: children?.length ? children : undefined,
32
+ }]
33
+ }
34
+
35
+ if (schema.type === 'object' || schema.properties) {
36
+ const properties = (schema.properties ?? {}) as Record<string, OpenAPIV3.SchemaObject>
37
+ const required = schema.required ?? requiredFields
38
+
39
+ return Object.entries(properties).map(([name, prop]) => {
40
+ const fieldType = inferType(prop)
41
+ const children =
42
+ fieldType === 'object' || prop.properties
43
+ ? flattenSchema(prop, prop.required)
44
+ : fieldType.endsWith('[]') && (prop as OpenAPIV3.ArraySchemaObject).items
45
+ ? flattenSchema((prop as OpenAPIV3.ArraySchemaObject).items as OpenAPIV3.SchemaObject)
46
+ : undefined
47
+
48
+ return {
49
+ name,
50
+ type: fieldType,
51
+ required: required.includes(name),
52
+ description: prop.description,
53
+ default: prop.default,
54
+ enum: prop.enum,
55
+ children: children?.length ? children : undefined,
56
+ }
57
+ })
58
+ }
59
+
60
+ return []
61
+ }
62
+
63
+ export function generateExampleJson(schema: OpenAPIV3.SchemaObject): unknown {
64
+ if (schema.example !== undefined) return schema.example
65
+ if (schema.default !== undefined) return schema.default
66
+
67
+ if (schema.type === 'array') {
68
+ const items = schema.items as OpenAPIV3.SchemaObject | undefined
69
+ return items ? [generateExampleJson(items)] : []
70
+ }
71
+
72
+ if (schema.type === 'object' || schema.properties) {
73
+ const properties = (schema.properties ?? {}) as Record<string, OpenAPIV3.SchemaObject>
74
+ const result: Record<string, unknown> = {}
75
+ for (const [name, prop] of Object.entries(properties)) {
76
+ result[name] = generateExampleJson(prop)
77
+ }
78
+ return result
79
+ }
80
+
81
+ const defaults: Record<string, unknown> = {
82
+ string: 'string',
83
+ integer: 0,
84
+ number: 0,
85
+ boolean: true,
86
+ }
87
+ return defaults[schema.type as string] ?? null
88
+ }
89
+
90
+ function inferType(schema: OpenAPIV3.SchemaObject): string {
91
+ if (schema.type === 'array') {
92
+ const items = schema.items as OpenAPIV3.SchemaObject | undefined
93
+ const itemType = items ? inferType(items) : 'unknown'
94
+ return `${itemType}[]`
95
+ }
96
+
97
+ if (schema.format) return `${schema.type}(${schema.format})`
98
+ return (schema.type as string) ?? 'object'
99
+ }
@@ -0,0 +1,87 @@
1
+ interface SnippetOptions {
2
+ method: string
3
+ url: string
4
+ headers: Record<string, string>
5
+ body?: string
6
+ }
7
+
8
+ export function generateCurl({ method, url, headers, body }: SnippetOptions): string {
9
+ const parts = [`curl -X ${method} '${url}'`]
10
+ for (const [key, value] of Object.entries(headers)) {
11
+ parts.push(` -H '${key}: ${value}'`)
12
+ }
13
+ if (body) {
14
+ parts.push(` -d '${body}'`)
15
+ }
16
+ return parts.join(' \\\n')
17
+ }
18
+
19
+ export function generatePython({ method, url, headers, body }: SnippetOptions): string {
20
+ const lines: string[] = ['import requests', '']
21
+ const methodLower = method.toLowerCase()
22
+ const headerEntries = Object.entries(headers)
23
+
24
+ lines.push(`response = requests.${methodLower}(`)
25
+ lines.push(` "${url}",`)
26
+
27
+ if (headerEntries.length > 0) {
28
+ lines.push(' headers={')
29
+ for (const [key, value] of headerEntries) {
30
+ lines.push(` "${key}": "${value}",`)
31
+ }
32
+ lines.push(' },')
33
+ }
34
+
35
+ if (body) {
36
+ lines.push(` json=${body},`)
37
+ }
38
+
39
+ lines.push(')')
40
+ lines.push('print(response.json())')
41
+ return lines.join('\n')
42
+ }
43
+
44
+ export function generateGo({ method, url, headers, body }: SnippetOptions): string {
45
+ const lines: string[] = []
46
+
47
+ if (body) {
48
+ lines.push('payload := strings.NewReader(`' + body + '`)')
49
+ lines.push('')
50
+ lines.push(`req, _ := http.NewRequest("${method}", "${url}", payload)`)
51
+ } else {
52
+ lines.push(`req, _ := http.NewRequest("${method}", "${url}", nil)`)
53
+ }
54
+
55
+ for (const [key, value] of Object.entries(headers)) {
56
+ lines.push(`req.Header.Set("${key}", "${value}")`)
57
+ }
58
+
59
+ lines.push('')
60
+ lines.push('resp, _ := http.DefaultClient.Do(req)')
61
+ lines.push('defer resp.Body.Close()')
62
+ return lines.join('\n')
63
+ }
64
+
65
+ export function generateTypeScript({ method, url, headers, body }: SnippetOptions): string {
66
+ const lines: string[] = []
67
+ const headerEntries = Object.entries(headers)
68
+
69
+ lines.push(`const response = await fetch("${url}", {`)
70
+ lines.push(` method: "${method}",`)
71
+
72
+ if (headerEntries.length > 0) {
73
+ lines.push(' headers: {')
74
+ for (const [key, value] of headerEntries) {
75
+ lines.push(` "${key}": "${value}",`)
76
+ }
77
+ lines.push(' },')
78
+ }
79
+
80
+ if (body) {
81
+ lines.push(` body: JSON.stringify(${body}),`)
82
+ }
83
+
84
+ lines.push('});')
85
+ lines.push('const data = await response.json();')
86
+ return lines.join('\n')
87
+ }
@@ -0,0 +1,186 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loader } from 'fumadocs-core/source';
4
+ import matter from 'gray-matter';
5
+ 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
+ }
14
+
15
+ function getContentDir(): string {
16
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
17
+ }
18
+
19
+ async function scanFiles(contentDir: string) {
20
+ const files: {
21
+ type: 'page' | 'meta';
22
+ path: string;
23
+ data: Record<string, unknown>;
24
+ }[] = [];
25
+
26
+ async function scan(dir: string, prefix: string[] = []) {
27
+ try {
28
+ const entries = await fs.readdir(dir, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
31
+ continue;
32
+ const fullPath = path.join(dir, entry.name);
33
+ const relativePath = [...prefix, entry.name].join('/');
34
+
35
+ if (entry.isDirectory()) {
36
+ await scan(fullPath, [...prefix, entry.name]);
37
+ continue;
38
+ }
39
+
40
+ if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
41
+ const raw = await fs.readFile(fullPath, 'utf-8');
42
+ const { data } = matter(raw);
43
+ files.push({
44
+ type: 'page',
45
+ path: relativePath,
46
+ data: { ...data, _absolutePath: fullPath }
47
+ });
48
+ } 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 });
54
+ }
55
+ }
56
+ } catch {
57
+ /* directory not readable */
58
+ }
59
+ }
60
+
61
+ await scan(contentDir);
62
+ return files;
63
+ }
64
+
65
+ let cachedSource: ReturnType<typeof loader> | null = null;
66
+ let cachedPages: SourcePage[] | null = null;
67
+
68
+ async function getSource() {
69
+ if (cachedSource) return cachedSource;
70
+ const contentDir = getContentDir();
71
+ const files = await scanFiles(contentDir);
72
+ cachedSource = loader({
73
+ source: { files },
74
+ baseUrl: '/'
75
+ });
76
+ return cachedSource;
77
+ }
78
+
79
+ export function invalidate() {
80
+ cachedSource = null;
81
+ cachedPages = null;
82
+ }
83
+
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;
108
+ }
109
+
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;
114
+ }
115
+
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;
124
+ }
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;
132
+ }
133
+
134
+ export async function buildPageTree(): Promise<PageTree> {
135
+ const s = await getSource();
136
+ const pages = s.getPages();
137
+ const folders = new Map<string, PageTreeItem[]>();
138
+ const rootPages: PageTreeItem[] = [];
139
+
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
+ }
160
+
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
+ );
167
+
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
+ }
182
+
183
+ children.push(...sortByOrder(folderItems));
184
+
185
+ return { name: 'root', children };
186
+ }
@@ -0,0 +1,22 @@
1
+ .layout {
2
+ height: 100vh;
3
+ overflow: hidden;
4
+ }
5
+
6
+ .body {
7
+ overflow: hidden;
8
+ }
9
+
10
+ .sidebar {
11
+ height: 100%;
12
+ }
13
+
14
+ .content {
15
+ height: 100%;
16
+ overflow-y: auto;
17
+ padding-right: 0;
18
+ }
19
+
20
+ .hiddenSearch {
21
+ display: none;
22
+ }
@@ -0,0 +1,33 @@
1
+ import { cx } from 'class-variance-authority';
2
+ import type { ReactNode } from 'react';
3
+ import { Search } from '@/components/ui/search';
4
+ import { buildApiPageTree } from '@/lib/api-routes';
5
+ import { usePageContext } from '@/lib/page-context';
6
+ import { getTheme } from '@/themes/registry';
7
+ import styles from './ApiLayout.module.css';
8
+
9
+ interface ApiLayoutProps {
10
+ children: ReactNode;
11
+ }
12
+
13
+ export function ApiLayout({ children }: ApiLayoutProps) {
14
+ const { config, apiSpecs } = usePageContext();
15
+ const { Layout, className } = getTheme(config.theme?.name);
16
+ const tree = buildApiPageTree(apiSpecs);
17
+
18
+ return (
19
+ <Layout
20
+ config={config}
21
+ tree={tree}
22
+ classNames={{
23
+ layout: cx(styles.layout, className),
24
+ body: styles.body,
25
+ sidebar: styles.sidebar,
26
+ content: styles.content
27
+ }}
28
+ >
29
+ <Search className={styles.hiddenSearch} />
30
+ {children}
31
+ </Layout>
32
+ );
33
+ }
@@ -0,0 +1,73 @@
1
+ import { Flex, Headline, Text } from '@raystack/apsara';
2
+ import type { OpenAPIV3 } from 'openapi-types';
3
+ import { EndpointPage } from '@/components/api';
4
+ import { findApiOperation } from '@/lib/api-routes';
5
+ import { Head } from '@/lib/head';
6
+ import type { ApiSpec } from '@/lib/openapi';
7
+ import { usePageContext } from '@/lib/page-context';
8
+
9
+ interface ApiPageProps {
10
+ slug: string[];
11
+ }
12
+
13
+ export function ApiPage({ slug }: ApiPageProps) {
14
+ const { config, apiSpecs } = usePageContext();
15
+
16
+ if (slug.length === 0) {
17
+ return (
18
+ <>
19
+ <Head
20
+ title='API Reference'
21
+ description={`API documentation for ${config.title}`}
22
+ config={config}
23
+ />
24
+ <ApiLanding specs={apiSpecs} />
25
+ </>
26
+ );
27
+ }
28
+
29
+ const match = findApiOperation(apiSpecs, slug);
30
+ if (!match) return null;
31
+
32
+ const operation = match.operation as OpenAPIV3.OperationObject;
33
+ const title =
34
+ operation.summary ?? `${match.method.toUpperCase()} ${match.path}`;
35
+
36
+ return (
37
+ <>
38
+ <Head title={title} description={operation.description} config={config} />
39
+ <EndpointPage
40
+ method={match.method}
41
+ path={match.path}
42
+ operation={match.operation}
43
+ serverUrl={match.spec.server.url}
44
+ specName={match.spec.name}
45
+ auth={match.spec.auth}
46
+ />
47
+ </>
48
+ );
49
+ }
50
+
51
+ function ApiLanding({ specs }: { specs: ApiSpec[] }) {
52
+ return (
53
+ <Flex
54
+ direction='column'
55
+ gap='large'
56
+ style={{ padding: 'var(--rs-space-7)' }}
57
+ >
58
+ <Headline size='medium' as='h1'>
59
+ API Reference
60
+ </Headline>
61
+ {specs.map(spec => (
62
+ <Flex key={spec.name} direction='column' gap='small'>
63
+ <Headline size='small' as='h2'>
64
+ {spec.name}
65
+ </Headline>
66
+ {spec.document.info.description && (
67
+ <Text size={3}>{spec.document.info.description}</Text>
68
+ )}
69
+ </Flex>
70
+ ))}
71
+ </Flex>
72
+ );
73
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ import { usePageContext } from '@/lib/page-context';
3
+ import { getTheme } from '@/themes/registry';
4
+
5
+ interface DocsLayoutProps {
6
+ children: ReactNode;
7
+ }
8
+
9
+ export function DocsLayout({ children }: DocsLayoutProps) {
10
+ const { config, tree } = usePageContext();
11
+ const { Layout, className } = getTheme(config.theme?.name);
12
+
13
+ return (
14
+ <Layout config={config} tree={tree} classNames={{ layout: className }}>
15
+ {children}
16
+ </Layout>
17
+ );
18
+ }