@raystack/chronicle 0.1.0-canary.e11f924
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 +9788 -0
- package/next.config.mjs +10 -0
- package/package.json +62 -0
- package/source.config.ts +50 -0
- package/src/app/[[...slug]]/layout.tsx +15 -0
- package/src/app/[[...slug]]/page.tsx +57 -0
- package/src/app/api/apis-proxy/route.ts +59 -0
- package/src/app/api/health/route.ts +3 -0
- package/src/app/api/search/route.ts +90 -0
- package/src/app/apis/[[...slug]]/layout.module.css +22 -0
- package/src/app/apis/[[...slug]]/layout.tsx +26 -0
- package/src/app/apis/[[...slug]]/page.tsx +57 -0
- package/src/app/layout.tsx +26 -0
- package/src/app/llms-full.txt/route.ts +18 -0
- package/src/app/llms.txt/route.ts +15 -0
- package/src/app/providers.tsx +8 -0
- package/src/cli/commands/build.ts +33 -0
- package/src/cli/commands/dev.ts +34 -0
- package/src/cli/commands/init.ts +58 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/start.ts +34 -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/process.ts +7 -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 +27 -0
- package/src/components/mdx/details.module.css +37 -0
- package/src/components/mdx/details.tsx +18 -0
- package/src/components/mdx/image.tsx +38 -0
- package/src/components/mdx/index.tsx +35 -0
- package/src/components/mdx/link.tsx +38 -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 +30 -0
- package/src/components/ui/search.module.css +104 -0
- package/src/components/ui/search.tsx +202 -0
- package/src/lib/api-routes.ts +120 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/openapi.ts +188 -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 +67 -0
- package/src/themes/default/Layout.module.css +81 -0
- package/src/themes/default/Layout.tsx +133 -0
- package/src/themes/default/Page.module.css +46 -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 +6 -0
- package/src/themes/default/index.ts +13 -0
- package/src/themes/paper/ChapterNav.module.css +71 -0
- package/src/themes/paper/ChapterNav.tsx +96 -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 +107 -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 +69 -0
- package/src/types/content.ts +35 -0
- package/src/types/index.ts +3 -0
- package/src/types/theme.ts +22 -0
|
@@ -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,67 @@
|
|
|
1
|
+
import { docs } from '../../.source/server'
|
|
2
|
+
import { loader } from 'fumadocs-core/source'
|
|
3
|
+
import type { PageTree, PageTreeItem, Frontmatter } from '@/types'
|
|
4
|
+
|
|
5
|
+
export const source = loader({
|
|
6
|
+
baseUrl: '/',
|
|
7
|
+
source: docs.toFumadocsSource(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export function sortByOrder<T extends { frontmatter?: Frontmatter }>(
|
|
11
|
+
items: T[]
|
|
12
|
+
): T[] {
|
|
13
|
+
return [...items].sort((a, b) => {
|
|
14
|
+
const orderA = a.frontmatter?.order ?? Number.MAX_SAFE_INTEGER
|
|
15
|
+
const orderB = b.frontmatter?.order ?? Number.MAX_SAFE_INTEGER
|
|
16
|
+
return orderA - orderB
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildPageTree(): PageTree {
|
|
21
|
+
const pages = source.getPages()
|
|
22
|
+
const folders = new Map<string, PageTreeItem[]>()
|
|
23
|
+
const rootPages: PageTreeItem[] = []
|
|
24
|
+
|
|
25
|
+
pages.forEach((page) => {
|
|
26
|
+
const data = page.data as { title?: string; order?: number }
|
|
27
|
+
const isIndex = page.url === '/'
|
|
28
|
+
const item: PageTreeItem = {
|
|
29
|
+
type: 'page',
|
|
30
|
+
name: data.title ?? page.slugs.join('/') ?? 'Untitled',
|
|
31
|
+
url: page.url,
|
|
32
|
+
order: data.order ?? (isIndex ? 0 : undefined),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (page.slugs.length > 1) {
|
|
36
|
+
const folder = page.slugs[0]
|
|
37
|
+
if (!folders.has(folder)) {
|
|
38
|
+
folders.set(folder, [])
|
|
39
|
+
}
|
|
40
|
+
folders.get(folder)?.push(item)
|
|
41
|
+
} else {
|
|
42
|
+
rootPages.push(item)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const sortByOrder = (items: PageTreeItem[]) =>
|
|
47
|
+
items.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER))
|
|
48
|
+
|
|
49
|
+
const children: PageTreeItem[] = sortByOrder(rootPages)
|
|
50
|
+
|
|
51
|
+
const folderItems: PageTreeItem[] = []
|
|
52
|
+
folders.forEach((items, folder) => {
|
|
53
|
+
const sorted = sortByOrder(items)
|
|
54
|
+
const indexPage = items.find(item => item.url === `/${folder}`)
|
|
55
|
+
const folderOrder = indexPage?.order ?? sorted[0]?.order
|
|
56
|
+
folderItems.push({
|
|
57
|
+
type: 'folder',
|
|
58
|
+
name: folder.charAt(0).toUpperCase() + folder.slice(1),
|
|
59
|
+
order: folderOrder,
|
|
60
|
+
children: sorted,
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
children.push(...sortByOrder(folderItems))
|
|
65
|
+
|
|
66
|
+
return { name: 'root', children }
|
|
67
|
+
}
|
|
@@ -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,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useEffect, useRef } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import NextLink from "next/link";
|
|
6
|
+
import { cx } from "class-variance-authority";
|
|
7
|
+
import { Flex, Navbar, Headline, Link, Sidebar, Button } from "@raystack/apsara";
|
|
8
|
+
import { RectangleStackIcon } from "@heroicons/react/24/outline";
|
|
9
|
+
import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
|
|
10
|
+
import { Search } from "@/components/ui/search";
|
|
11
|
+
import { Footer } from "@/components/ui/footer";
|
|
12
|
+
import { MethodBadge } from "@/components/api/method-badge";
|
|
13
|
+
import type { ThemeLayoutProps, PageTreeItem } from "@/types";
|
|
14
|
+
import styles from "./Layout.module.css";
|
|
15
|
+
|
|
16
|
+
const iconMap: Record<string, React.ReactNode> = {
|
|
17
|
+
"rectangle-stack": <RectangleStackIcon width={16} height={16} />,
|
|
18
|
+
"method-get": <MethodBadge method="GET" size="micro" />,
|
|
19
|
+
"method-post": <MethodBadge method="POST" size="micro" />,
|
|
20
|
+
"method-put": <MethodBadge method="PUT" size="micro" />,
|
|
21
|
+
"method-delete": <MethodBadge method="DELETE" size="micro" />,
|
|
22
|
+
"method-patch": <MethodBadge method="PATCH" size="micro" />,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let savedScrollTop = 0;
|
|
26
|
+
|
|
27
|
+
export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const el = scrollRef.current;
|
|
33
|
+
if (!el) return;
|
|
34
|
+
const onScroll = () => { savedScrollTop = el.scrollTop; };
|
|
35
|
+
el.addEventListener('scroll', onScroll);
|
|
36
|
+
return () => el.removeEventListener('scroll', onScroll);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const el = scrollRef.current;
|
|
41
|
+
if (el) requestAnimationFrame(() => { el.scrollTop = savedScrollTop; });
|
|
42
|
+
}, [pathname]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
|
|
46
|
+
<Navbar className={styles.header}>
|
|
47
|
+
<Navbar.Start>
|
|
48
|
+
<NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
49
|
+
<Headline size="small" weight="medium" as="h1">
|
|
50
|
+
{config.title}
|
|
51
|
+
</Headline>
|
|
52
|
+
</NextLink>
|
|
53
|
+
</Navbar.Start>
|
|
54
|
+
<Navbar.End>
|
|
55
|
+
<Flex gap="medium" align="center" className={styles.navActions}>
|
|
56
|
+
{config.api?.map((api) => (
|
|
57
|
+
<NextLink key={api.basePath} href={api.basePath} className={styles.navButton}>
|
|
58
|
+
{api.name} API
|
|
59
|
+
</NextLink>
|
|
60
|
+
))}
|
|
61
|
+
{config.navigation?.links?.map((link) => (
|
|
62
|
+
<Link key={link.href} href={link.href}>
|
|
63
|
+
{link.label}
|
|
64
|
+
</Link>
|
|
65
|
+
))}
|
|
66
|
+
{config.search?.enabled && <Search />}
|
|
67
|
+
</Flex>
|
|
68
|
+
<ClientThemeSwitcher size={16} />
|
|
69
|
+
</Navbar.End>
|
|
70
|
+
</Navbar>
|
|
71
|
+
<Flex className={cx(styles.body, classNames?.body)}>
|
|
72
|
+
<Sidebar defaultOpen collapsible={false} className={cx(styles.sidebar, classNames?.sidebar)}>
|
|
73
|
+
<Sidebar.Main ref={scrollRef}>
|
|
74
|
+
{tree.children.map((item) => (
|
|
75
|
+
<SidebarNode
|
|
76
|
+
key={item.url ?? item.name}
|
|
77
|
+
item={item}
|
|
78
|
+
pathname={pathname}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</Sidebar.Main>
|
|
82
|
+
</Sidebar>
|
|
83
|
+
<main className={cx(styles.content, classNames?.content)}>{children}</main>
|
|
84
|
+
</Flex>
|
|
85
|
+
<Footer config={config.footer} />
|
|
86
|
+
</Flex>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SidebarNode({
|
|
91
|
+
item,
|
|
92
|
+
pathname,
|
|
93
|
+
}: {
|
|
94
|
+
item: PageTreeItem;
|
|
95
|
+
pathname: string;
|
|
96
|
+
}) {
|
|
97
|
+
if (item.type === "separator") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (item.type === "folder" && item.children) {
|
|
102
|
+
return (
|
|
103
|
+
<Sidebar.Group
|
|
104
|
+
label={item.name}
|
|
105
|
+
leadingIcon={item.icon ? iconMap[item.icon] : undefined}
|
|
106
|
+
classNames={{ items: styles.groupItems }}
|
|
107
|
+
>
|
|
108
|
+
{item.children.map((child) => (
|
|
109
|
+
<SidebarNode
|
|
110
|
+
key={child.url ?? child.name}
|
|
111
|
+
item={child}
|
|
112
|
+
pathname={pathname}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</Sidebar.Group>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isActive = pathname === item.url;
|
|
120
|
+
const href = item.url ?? "#";
|
|
121
|
+
const link = useMemo(() => <NextLink href={href} scroll={false} />, [href]);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Sidebar.Item
|
|
125
|
+
href={href}
|
|
126
|
+
active={isActive}
|
|
127
|
+
leadingIcon={item.icon ? iconMap[item.icon] : undefined}
|
|
128
|
+
as={link}
|
|
129
|
+
>
|
|
130
|
+
{item.name}
|
|
131
|
+
</Sidebar.Item>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 table {
|
|
42
|
+
display: block;
|
|
43
|
+
max-width: 100%;
|
|
44
|
+
overflow-x: auto;
|
|
45
|
+
margin-bottom: var(--rs-space-5);
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.toc {
|
|
2
|
+
width: 200px;
|
|
3
|
+
flex-shrink: 0;
|
|
4
|
+
position: sticky;
|
|
5
|
+
top: var(--rs-space-9);
|
|
6
|
+
max-height: calc(100vh - var(--rs-space-17));
|
|
7
|
+
overflow-y: auto;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.title {
|
|
11
|
+
display: block;
|
|
12
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
13
|
+
text-transform: uppercase;
|
|
14
|
+
letter-spacing: 0.05em;
|
|
15
|
+
margin-bottom: var(--rs-space-3);
|
|
16
|
+
font-size: var(--rs-font-size-mini);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.nav {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 0;
|
|
23
|
+
border-left: 1px solid var(--rs-color-border-base-primary);
|
|
24
|
+
padding-left: var(--rs-space-3);
|
|
25
|
+
margin-bottom: var(--rs-space-6);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.link {
|
|
29
|
+
color: var(--rs-color-foreground-base-tertiary);
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
font-size: var(--rs-font-size-small);
|
|
32
|
+
line-height: 1.4;
|
|
33
|
+
padding: var(--rs-space-1) 0;
|
|
34
|
+
transition: color 0.15s ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.link:hover {
|
|
38
|
+
color: var(--rs-color-foreground-base-primary);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.active {
|
|
42
|
+
color: var(--rs-color-foreground-base-primary);
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.nested {
|
|
47
|
+
padding-left: var(--rs-space-3);
|
|
48
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Text } from '@raystack/apsara'
|
|
5
|
+
import type { TocItem } from '@/types'
|
|
6
|
+
import styles from './Toc.module.css'
|
|
7
|
+
|
|
8
|
+
interface TocProps {
|
|
9
|
+
items: TocItem[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Toc({ items }: TocProps) {
|
|
13
|
+
const [activeId, setActiveId] = useState<string>('')
|
|
14
|
+
|
|
15
|
+
// Filter to only show h2 and h3 headings
|
|
16
|
+
const filteredItems = items.filter((item) => item.depth >= 2 && item.depth <= 3)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const headingIds = filteredItems.map((item) => item.url.replace('#', ''))
|
|
20
|
+
|
|
21
|
+
const observer = new IntersectionObserver(
|
|
22
|
+
(entries) => {
|
|
23
|
+
entries.forEach((entry) => {
|
|
24
|
+
if (entry.isIntersecting) {
|
|
25
|
+
setActiveId(entry.target.id)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
// -80px top: offset for fixed header, -80% bottom: trigger when heading is in top 20% of viewport
|
|
30
|
+
{ rootMargin: '-80px 0px -80% 0px' }
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
headingIds.forEach((id) => {
|
|
34
|
+
const element = document.getElementById(id)
|
|
35
|
+
if (element) observer.observe(element)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return () => observer.disconnect()
|
|
39
|
+
}, [filteredItems])
|
|
40
|
+
|
|
41
|
+
if (filteredItems.length === 0) return null
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<aside className={styles.toc}>
|
|
45
|
+
<Text size={1} weight="medium" className={styles.title}>
|
|
46
|
+
On this page
|
|
47
|
+
</Text>
|
|
48
|
+
<nav className={styles.nav}>
|
|
49
|
+
{filteredItems.map((item) => {
|
|
50
|
+
const id = item.url.replace('#', '')
|
|
51
|
+
const isActive = activeId === id
|
|
52
|
+
const isNested = item.depth > 2
|
|
53
|
+
return (
|
|
54
|
+
<a
|
|
55
|
+
key={item.url}
|
|
56
|
+
href={item.url}
|
|
57
|
+
className={`${styles.link} ${isActive ? styles.active : ''} ${isNested ? styles.nested : ''}`}
|
|
58
|
+
>
|
|
59
|
+
{item.title}
|
|
60
|
+
</a>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
</nav>
|
|
64
|
+
</aside>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Layout } from './Layout'
|
|
2
|
+
import { Page } from './Page'
|
|
3
|
+
import { Toc } from './Toc'
|
|
4
|
+
import { inter } from './font'
|
|
5
|
+
import type { Theme } from '@/types'
|
|
6
|
+
|
|
7
|
+
export const defaultTheme: Theme = {
|
|
8
|
+
Layout,
|
|
9
|
+
Page,
|
|
10
|
+
className: inter.className,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Layout, Page, Toc }
|