@raystack/chronicle 0.1.0-canary.ac60f9f → 0.1.0-canary.bd7f9af
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 +61 -6
- package/package.json +1 -1
- package/src/components/mdx/code.tsx +10 -1
- package/src/components/mdx/details.module.css +1 -26
- package/src/components/mdx/details.tsx +2 -3
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/page-context.tsx +16 -6
- package/src/lib/source.ts +50 -97
- package/src/server/api/page/[...slug].ts +12 -4
- package/src/server/entry-client.tsx +25 -13
- package/src/server/entry-server.tsx +30 -21
- package/src/server/routes/sitemap.xml.ts +3 -2
- package/src/server/vite-config.ts +33 -4
- package/src/themes/default/Layout.tsx +13 -10
- package/src/themes/default/Page.module.css +44 -0
- package/src/themes/default/Toc.tsx +14 -30
- package/src/themes/paper/ChapterNav.tsx +14 -14
- package/src/themes/paper/Page.module.css +5 -0
- package/src/themes/paper/Page.tsx +15 -41
- package/src/themes/paper/ReadingProgress.tsx +2 -2
- package/src/types/content.ts +5 -21
- package/src/types/globals.d.ts +0 -1
- package/src/types/theme.ts +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -16,15 +16,43 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
18
|
|
|
19
|
+
// src/lib/remark-unused-directives.ts
|
|
20
|
+
import { visit } from "unist-util-visit";
|
|
21
|
+
var remarkUnusedDirectives = () => {
|
|
22
|
+
return (tree) => {
|
|
23
|
+
visit(tree, ["textDirective"], (node) => {
|
|
24
|
+
const directive = node;
|
|
25
|
+
if (!directive.data) {
|
|
26
|
+
const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0;
|
|
27
|
+
const hasChildren = directive.children && directive.children.length > 0;
|
|
28
|
+
if (!hasAttributes && !hasChildren) {
|
|
29
|
+
const name = directive.name;
|
|
30
|
+
if (!name)
|
|
31
|
+
return;
|
|
32
|
+
Object.keys(directive).forEach((key) => delete directive[key]);
|
|
33
|
+
directive.type = "text";
|
|
34
|
+
directive.value = `:${name}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}, remark_unused_directives_default;
|
|
40
|
+
var init_remark_unused_directives = __esm(() => {
|
|
41
|
+
remark_unused_directives_default = remarkUnusedDirectives;
|
|
42
|
+
});
|
|
43
|
+
|
|
19
44
|
// src/server/vite-config.ts
|
|
20
45
|
var exports_vite_config = {};
|
|
21
46
|
__export(exports_vite_config, {
|
|
22
47
|
createViteConfig: () => createViteConfig
|
|
23
48
|
});
|
|
24
49
|
import react from "@vitejs/plugin-react";
|
|
50
|
+
import { remarkDirectiveAdmonition, remarkMdxMermaid } from "fumadocs-core/mdx-plugins";
|
|
51
|
+
import { defineConfig as defineFumadocsConfig } from "fumadocs-mdx/config";
|
|
25
52
|
import mdx from "fumadocs-mdx/vite";
|
|
26
53
|
import { nitro } from "nitro/vite";
|
|
27
54
|
import path4 from "node:path";
|
|
55
|
+
import remarkDirective from "remark-directive";
|
|
28
56
|
async function createViteConfig(options) {
|
|
29
57
|
const { packageRoot, projectRoot, contentDir, preset } = options;
|
|
30
58
|
return {
|
|
@@ -35,13 +63,39 @@ async function createViteConfig(options) {
|
|
|
35
63
|
serverDir: path4.resolve(packageRoot, "src/server"),
|
|
36
64
|
...preset && { preset }
|
|
37
65
|
}),
|
|
38
|
-
mdx({
|
|
66
|
+
mdx({
|
|
67
|
+
default: defineFumadocsConfig({
|
|
68
|
+
mdxOptions: {
|
|
69
|
+
remarkPlugins: [
|
|
70
|
+
remarkDirective,
|
|
71
|
+
[remarkDirectiveAdmonition, {
|
|
72
|
+
tags: {
|
|
73
|
+
CalloutContainer: "Callout",
|
|
74
|
+
CalloutTitle: "CalloutTitle",
|
|
75
|
+
CalloutDescription: "CalloutDescription"
|
|
76
|
+
},
|
|
77
|
+
types: {
|
|
78
|
+
note: "accent",
|
|
79
|
+
tip: "accent",
|
|
80
|
+
info: "accent",
|
|
81
|
+
warn: "attention",
|
|
82
|
+
warning: "attention",
|
|
83
|
+
danger: "alert",
|
|
84
|
+
caution: "alert",
|
|
85
|
+
success: "success"
|
|
86
|
+
}
|
|
87
|
+
}],
|
|
88
|
+
remark_unused_directives_default,
|
|
89
|
+
remarkMdxMermaid
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}, { index: false }),
|
|
39
94
|
react()
|
|
40
95
|
],
|
|
41
96
|
resolve: {
|
|
42
97
|
alias: {
|
|
43
|
-
"@": path4.resolve(packageRoot, "src")
|
|
44
|
-
"@content": path4.resolve(packageRoot, ".content")
|
|
98
|
+
"@": path4.resolve(packageRoot, "src")
|
|
45
99
|
},
|
|
46
100
|
conditions: ["module-sync", "import", "node"],
|
|
47
101
|
dedupe: [
|
|
@@ -59,8 +113,7 @@ async function createViteConfig(options) {
|
|
|
59
113
|
},
|
|
60
114
|
define: {
|
|
61
115
|
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
|
|
62
|
-
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
63
|
-
__CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
|
|
116
|
+
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
64
117
|
},
|
|
65
118
|
css: {
|
|
66
119
|
modules: {
|
|
@@ -81,7 +134,9 @@ async function createViteConfig(options) {
|
|
|
81
134
|
}
|
|
82
135
|
};
|
|
83
136
|
}
|
|
84
|
-
var init_vite_config = () => {
|
|
137
|
+
var init_vite_config = __esm(() => {
|
|
138
|
+
init_remark_unused_directives();
|
|
139
|
+
});
|
|
85
140
|
|
|
86
141
|
// src/cli/index.ts
|
|
87
142
|
import { Command as Command6 } from "commander";
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { type ComponentProps, isValidElement, Children } from 'react'
|
|
4
|
+
import { Mermaid } from './mermaid'
|
|
4
5
|
import styles from './code.module.css'
|
|
5
6
|
|
|
6
7
|
type PreProps = ComponentProps<'pre'> & {
|
|
@@ -16,6 +17,14 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function MdxPre({ children, title, className, ...props }: PreProps) {
|
|
20
|
+
// Detect mermaid code blocks
|
|
21
|
+
if (isValidElement(children)) {
|
|
22
|
+
const childProps = children.props as { className?: string; children?: string }
|
|
23
|
+
if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
|
|
24
|
+
return <Mermaid chart={childProps.children} />
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
return (
|
|
20
29
|
<div className={styles.codeBlock}>
|
|
21
30
|
{title && <div className={styles.codeHeader}>{title}</div>}
|
|
@@ -1,35 +1,10 @@
|
|
|
1
1
|
.details {
|
|
2
|
-
border: 1px solid var(--rs-color-border-base-primary);
|
|
3
|
-
border-radius: var(--rs-radius-2);
|
|
4
2
|
margin: var(--rs-space-5) 0;
|
|
5
3
|
}
|
|
6
4
|
|
|
7
|
-
.
|
|
8
|
-
padding: var(--rs-space-4) var(--rs-space-5);
|
|
9
|
-
cursor: pointer;
|
|
5
|
+
.trigger {
|
|
10
6
|
font-weight: 500;
|
|
11
7
|
font-size: var(--rs-font-size-small);
|
|
12
|
-
color: var(--rs-color-text-base-primary);
|
|
13
|
-
background: var(--rs-color-background-base-secondary);
|
|
14
|
-
list-style: none;
|
|
15
|
-
display: flex;
|
|
16
|
-
align-items: center;
|
|
17
|
-
gap: var(--rs-space-3);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.summary::-webkit-details-marker {
|
|
21
|
-
display: none;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.summary::before {
|
|
25
|
-
content: '▶';
|
|
26
|
-
font-size: 10px;
|
|
27
|
-
transition: transform 0.2s ease;
|
|
28
|
-
color: var(--rs-color-text-base-secondary);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.details[open] > .summary::before {
|
|
32
|
-
transform: rotate(90deg);
|
|
33
8
|
}
|
|
34
9
|
|
|
35
10
|
.content {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react'
|
|
2
|
-
import styles from './details.module.css'
|
|
3
2
|
|
|
4
3
|
export function MdxDetails({ children, className, ...props }: ComponentProps<'details'>) {
|
|
5
4
|
return (
|
|
6
|
-
<details className={
|
|
5
|
+
<details className={className} {...props}>
|
|
7
6
|
{children}
|
|
8
7
|
</details>
|
|
9
8
|
)
|
|
@@ -11,7 +10,7 @@ export function MdxDetails({ children, className, ...props }: ComponentProps<'de
|
|
|
11
10
|
|
|
12
11
|
export function MdxSummary({ children, className, ...props }: ComponentProps<'summary'>) {
|
|
13
12
|
return (
|
|
14
|
-
<summary className={
|
|
13
|
+
<summary className={className} {...props}>
|
|
15
14
|
{children}
|
|
16
15
|
</summary>
|
|
17
16
|
)
|
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Breadcrumb } from '@raystack/apsara'
|
|
4
|
-
import
|
|
4
|
+
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb'
|
|
5
|
+
import type { Root } from 'fumadocs-core/page-tree'
|
|
5
6
|
|
|
6
7
|
interface BreadcrumbsProps {
|
|
7
8
|
slug: string[]
|
|
8
|
-
tree:
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function findInTree(items: PageTreeItem[], targetPath: string): PageTreeItem | undefined {
|
|
12
|
-
for (const item of items) {
|
|
13
|
-
const itemUrl = item.url || `/${item.name.toLowerCase().replace(/\s+/g, '-')}`
|
|
14
|
-
if (itemUrl === targetPath || itemUrl === `/${targetPath}`) {
|
|
15
|
-
return item
|
|
16
|
-
}
|
|
17
|
-
if (item.children) {
|
|
18
|
-
const found = findInTree(item.children, targetPath)
|
|
19
|
-
if (found) return found
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return undefined
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getFirstPageUrl(item: PageTreeItem): string | undefined {
|
|
26
|
-
if (item.type === 'page' && item.url) {
|
|
27
|
-
return item.url
|
|
28
|
-
}
|
|
29
|
-
if (item.children) {
|
|
30
|
-
for (const child of item.children) {
|
|
31
|
-
const url = getFirstPageUrl(child)
|
|
32
|
-
if (url) return url
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return undefined
|
|
9
|
+
tree: Root
|
|
36
10
|
}
|
|
37
11
|
|
|
38
12
|
export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
|
|
39
|
-
const
|
|
13
|
+
const url = slug.length === 0 ? '/' : `/${slug.join('/')}`
|
|
14
|
+
const items = getBreadcrumbItems(url, tree, { includePage: true })
|
|
40
15
|
|
|
41
|
-
|
|
42
|
-
const currentPath = `/${slug.slice(0, i + 1).join('/')}`
|
|
43
|
-
const node = findInTree(tree.children, currentPath)
|
|
44
|
-
const href = node?.url || (node && getFirstPageUrl(node)) || currentPath
|
|
45
|
-
const label = node?.name ?? slug[i]
|
|
46
|
-
items.push({
|
|
47
|
-
label: label.charAt(0).toUpperCase() + label.slice(1),
|
|
48
|
-
href,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
16
|
+
if (items.length === 0) return null
|
|
51
17
|
|
|
52
18
|
return (
|
|
53
19
|
<Breadcrumb size="small">
|
|
@@ -55,10 +21,10 @@ export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
|
|
|
55
21
|
const breadcrumbItem = (
|
|
56
22
|
<Breadcrumb.Item
|
|
57
23
|
key={`item-${index}`}
|
|
58
|
-
href={item.
|
|
24
|
+
href={item.url}
|
|
59
25
|
current={index === items.length - 1}
|
|
60
26
|
>
|
|
61
|
-
{item.
|
|
27
|
+
{item.name}
|
|
62
28
|
</Breadcrumb.Item>
|
|
63
29
|
)
|
|
64
30
|
if (index === 0) return [breadcrumbItem]
|
package/src/lib/api-routes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
2
|
import slugify from 'slugify'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Root, Node, Item, Folder } from 'fumadocs-core/page-tree'
|
|
4
4
|
import type { ApiSpec } from './openapi'
|
|
5
5
|
|
|
6
6
|
export function getSpecSlug(spec: ApiSpec): string {
|
|
@@ -56,16 +56,15 @@ export function findApiOperation(specs: ApiSpec[], slug: string[]): ApiRouteMatc
|
|
|
56
56
|
return null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function buildApiPageTree(specs: ApiSpec[]):
|
|
60
|
-
const children:
|
|
59
|
+
export function buildApiPageTree(specs: ApiSpec[]): Root {
|
|
60
|
+
const children: Node[] = []
|
|
61
61
|
|
|
62
62
|
for (const spec of specs) {
|
|
63
63
|
const specSlug = getSpecSlug(spec)
|
|
64
64
|
const paths = spec.document.paths ?? {}
|
|
65
65
|
const tags = spec.document.tags ?? []
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
const opsByTag = new Map<string, PageTreeItem[]>()
|
|
67
|
+
const opsByTag = new Map<string, Item[]>()
|
|
69
68
|
const tagDisplayName = new Map<string, string>()
|
|
70
69
|
|
|
71
70
|
for (const [, pathItem] of Object.entries(paths)) {
|
|
@@ -90,7 +89,6 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
// Use doc.tags display names where available
|
|
94
92
|
for (const t of tags) {
|
|
95
93
|
const key = t.name.toLowerCase()
|
|
96
94
|
if (opsByTag.has(key)) {
|
|
@@ -98,7 +96,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
const tagFolders:
|
|
99
|
+
const tagFolders: Folder[] = Array.from(opsByTag.entries()).map(([key, ops]) => ({
|
|
102
100
|
type: 'folder' as const,
|
|
103
101
|
name: tagDisplayName.get(key) ?? key,
|
|
104
102
|
icon: 'rectangle-stack',
|
|
@@ -110,7 +108,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
110
108
|
type: 'folder',
|
|
111
109
|
name: spec.name,
|
|
112
110
|
children: tagFolders,
|
|
113
|
-
})
|
|
111
|
+
} as Folder)
|
|
114
112
|
} else {
|
|
115
113
|
children.push(...tagFolders)
|
|
116
114
|
}
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import React, {
|
|
|
8
8
|
import { useLocation } from 'react-router';
|
|
9
9
|
import { mdxComponents } from '@/components/mdx';
|
|
10
10
|
import type { ApiSpec } from '@/lib/openapi';
|
|
11
|
-
import type { ChronicleConfig, Frontmatter,
|
|
11
|
+
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
|
|
12
12
|
|
|
13
13
|
interface PageData {
|
|
14
14
|
slug: string[];
|
|
@@ -18,7 +18,7 @@ interface PageData {
|
|
|
18
18
|
|
|
19
19
|
interface PageContextValue {
|
|
20
20
|
config: ChronicleConfig;
|
|
21
|
-
tree:
|
|
21
|
+
tree: Root;
|
|
22
22
|
page: PageData | null;
|
|
23
23
|
apiSpecs: ApiSpec[];
|
|
24
24
|
}
|
|
@@ -31,7 +31,7 @@ export function usePageContext(): PageContextValue {
|
|
|
31
31
|
console.error('usePageContext: no context found!');
|
|
32
32
|
return {
|
|
33
33
|
config: { title: 'Documentation' },
|
|
34
|
-
tree: { name: 'root', children: [] },
|
|
34
|
+
tree: { name: 'root', children: [] } as Root,
|
|
35
35
|
page: null,
|
|
36
36
|
apiSpecs: []
|
|
37
37
|
};
|
|
@@ -41,14 +41,24 @@ export function usePageContext(): PageContextValue {
|
|
|
41
41
|
|
|
42
42
|
interface PageProviderProps {
|
|
43
43
|
initialConfig: ChronicleConfig;
|
|
44
|
-
initialTree:
|
|
44
|
+
initialTree: Root;
|
|
45
45
|
initialPage: PageData | null;
|
|
46
46
|
initialApiSpecs: ApiSpec[];
|
|
47
47
|
children: ReactNode;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
|
|
51
|
+
'../../.content/**/*.{mdx,md}'
|
|
52
|
+
);
|
|
53
|
+
|
|
50
54
|
async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
|
|
51
|
-
const
|
|
55
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
56
|
+
const key = relativePath.endsWith('.md')
|
|
57
|
+
? `../../.content/${withoutExt}.md`
|
|
58
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
59
|
+
const loader = contentModules[key];
|
|
60
|
+
if (!loader) return null;
|
|
61
|
+
const mod = await loader();
|
|
52
62
|
return mod.default
|
|
53
63
|
? React.createElement(mod.default, { components: mdxComponents })
|
|
54
64
|
: null;
|
|
@@ -62,7 +72,7 @@ export function PageProvider({
|
|
|
62
72
|
children
|
|
63
73
|
}: PageProviderProps) {
|
|
64
74
|
const { pathname } = useLocation();
|
|
65
|
-
const [tree] = useState<
|
|
75
|
+
const [tree] = useState<Root>(initialTree);
|
|
66
76
|
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
67
77
|
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
68
78
|
const [currentPath, setCurrentPath] = useState(pathname);
|
package/src/lib/source.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loader } from 'fumadocs-core/source';
|
|
4
|
+
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
|
|
4
5
|
import matter from 'gray-matter';
|
|
5
6
|
import type { MDXContent } from 'mdx/types';
|
|
6
|
-
import type { Frontmatter, PageTree, PageTreeItem } from '@/types';
|
|
7
|
-
|
|
8
|
-
export interface SourcePage {
|
|
9
|
-
url: string;
|
|
10
|
-
slugs: string[];
|
|
11
|
-
filePath: string;
|
|
12
|
-
frontmatter: Frontmatter;
|
|
13
|
-
}
|
|
14
7
|
|
|
15
8
|
function getContentDir(): string {
|
|
16
9
|
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
|
|
@@ -43,7 +36,7 @@ async function scanFiles(contentDir: string) {
|
|
|
43
36
|
files.push({
|
|
44
37
|
type: 'page',
|
|
45
38
|
path: relativePath,
|
|
46
|
-
data: { ...data,
|
|
39
|
+
data: { ...data, _relativePath: relativePath }
|
|
47
40
|
});
|
|
48
41
|
} else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
|
|
49
42
|
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
@@ -63,7 +56,6 @@ async function scanFiles(contentDir: string) {
|
|
|
63
56
|
}
|
|
64
57
|
|
|
65
58
|
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
66
|
-
let cachedPages: SourcePage[] | null = null;
|
|
67
59
|
|
|
68
60
|
async function getSource() {
|
|
69
61
|
if (cachedSource) return cachedSource;
|
|
@@ -76,111 +68,72 @@ async function getSource() {
|
|
|
76
68
|
return cachedSource;
|
|
77
69
|
}
|
|
78
70
|
|
|
71
|
+
export { getSource as source };
|
|
72
|
+
|
|
79
73
|
export function invalidate() {
|
|
80
74
|
cachedSource = null;
|
|
81
|
-
cachedPages = null;
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
if (
|
|
77
|
+
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
|
|
78
|
+
if (node.type === 'page') return orderMap.get(node.url);
|
|
79
|
+
if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
86
82
|
|
|
83
|
+
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
|
|
84
|
+
return [...nodes]
|
|
85
|
+
.map(n =>
|
|
86
|
+
n.type === 'folder'
|
|
87
|
+
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
|
|
88
|
+
: n
|
|
89
|
+
)
|
|
90
|
+
.sort(
|
|
91
|
+
(a, b) =>
|
|
92
|
+
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
93
|
+
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
|
|
98
|
+
const orderMap = new Map<string, number>();
|
|
99
|
+
for (const page of pages) {
|
|
100
|
+
const d = page.data as Record<string, unknown>;
|
|
101
|
+
const order = d.order as number | undefined;
|
|
102
|
+
if (order !== undefined) orderMap.set(page.url, order);
|
|
103
|
+
if (page.url === '/') orderMap.set('/', order ?? 0);
|
|
104
|
+
}
|
|
105
|
+
return { ...tree, children: sortNodes(tree.children, orderMap) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function getPageTree(): Promise<Root> {
|
|
87
109
|
const s = await getSource();
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
});
|
|
110
|
+
return sortTreeByOrder(s.pageTree as Root, s.getPages());
|
|
111
|
+
}
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
export async function getPages() {
|
|
114
|
+
const s = await getSource();
|
|
115
|
+
return s.getPages();
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
export async function getPage(
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
return pages.find(p => p.url === targetUrl) ?? null;
|
|
118
|
+
export async function getPage(slugs?: string[]) {
|
|
119
|
+
const s = await getSource();
|
|
120
|
+
return s.getPage(slugs);
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
export async function loadPageComponent(
|
|
117
|
-
|
|
124
|
+
relativePath: string
|
|
118
125
|
): Promise<MDXContent | null> {
|
|
119
|
-
if (!
|
|
126
|
+
if (!relativePath) return null;
|
|
127
|
+
const contentDir = getContentDir();
|
|
128
|
+
const fullPath = path.join(contentDir, relativePath);
|
|
120
129
|
try {
|
|
121
|
-
await fs.access(
|
|
130
|
+
await fs.access(fullPath);
|
|
122
131
|
} catch {
|
|
123
132
|
return null;
|
|
124
133
|
}
|
|
125
|
-
const contentDir = getContentDir();
|
|
126
|
-
const relativePath = path.relative(contentDir, page.filePath);
|
|
127
134
|
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
128
135
|
const mod = relativePath.endsWith('.md')
|
|
129
|
-
? await import(
|
|
130
|
-
: await import(
|
|
136
|
+
? await import(`../../.content/${withoutExt}.md`)
|
|
137
|
+
: await import(`../../.content/${withoutExt}.mdx`);
|
|
131
138
|
return mod.default;
|
|
132
139
|
}
|
|
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
|
-
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { defineHandler, HTTPError } from 'nitro';
|
|
3
2
|
import { getPage } from '@/lib/source';
|
|
4
3
|
|
|
@@ -11,8 +10,17 @@ export default defineHandler(async event => {
|
|
|
11
10
|
throw new HTTPError({ status: 404, message: 'Page not found' });
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const
|
|
15
|
-
const relativePath =
|
|
13
|
+
const data = page.data as Record<string, unknown>;
|
|
14
|
+
const relativePath = (data._relativePath as string) ?? '';
|
|
16
15
|
|
|
17
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
frontmatter: {
|
|
18
|
+
title: (data.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
|
|
19
|
+
description: data.description as string | undefined,
|
|
20
|
+
order: data.order as number | undefined,
|
|
21
|
+
icon: data.icon as string | undefined,
|
|
22
|
+
lastModified: data.lastModified as string | undefined,
|
|
23
|
+
},
|
|
24
|
+
relativePath,
|
|
25
|
+
};
|
|
18
26
|
});
|
|
@@ -2,16 +2,17 @@ import '@vitejs/plugin-react/preamble';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { hydrateRoot } from 'react-dom/client';
|
|
4
4
|
import { BrowserRouter } from 'react-router';
|
|
5
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
5
6
|
import { mdxComponents } from '@/components/mdx';
|
|
6
7
|
import { PageProvider } from '@/lib/page-context';
|
|
7
|
-
import type { ChronicleConfig, Frontmatter,
|
|
8
|
+
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
|
|
8
9
|
import type { ApiSpec } from '@/lib/openapi';
|
|
9
10
|
import type { ReactNode } from 'react';
|
|
10
11
|
import { App } from './App';
|
|
11
12
|
|
|
12
13
|
interface EmbeddedData {
|
|
13
14
|
config: ChronicleConfig;
|
|
14
|
-
tree:
|
|
15
|
+
tree: Root;
|
|
15
16
|
slug: string[];
|
|
16
17
|
frontmatter: Frontmatter;
|
|
17
18
|
relativePath: string;
|
|
@@ -26,7 +27,7 @@ async function hydrate() {
|
|
|
26
27
|
const config: ChronicleConfig = embedded?.config ?? {
|
|
27
28
|
title: 'Documentation'
|
|
28
29
|
};
|
|
29
|
-
const tree:
|
|
30
|
+
const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
|
|
30
31
|
const isApiPage =
|
|
31
32
|
window.location.pathname.startsWith('/apis') && !!config.api?.length;
|
|
32
33
|
const apiSpecs: ApiSpec[] = isApiPage
|
|
@@ -42,14 +43,16 @@ async function hydrate() {
|
|
|
42
43
|
hydrateRoot(
|
|
43
44
|
document.getElementById('root') as HTMLElement,
|
|
44
45
|
<BrowserRouter>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
<ReactRouterProvider>
|
|
47
|
+
<PageProvider
|
|
48
|
+
initialConfig={config}
|
|
49
|
+
initialTree={tree}
|
|
50
|
+
initialPage={page}
|
|
51
|
+
initialApiSpecs={apiSpecs}
|
|
52
|
+
>
|
|
53
|
+
<App />
|
|
54
|
+
</PageProvider>
|
|
55
|
+
</ReactRouterProvider>
|
|
53
56
|
</BrowserRouter>
|
|
54
57
|
);
|
|
55
58
|
} catch (err) {
|
|
@@ -57,11 +60,20 @@ async function hydrate() {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
|
|
64
|
+
'../../.content/**/*.{mdx,md}'
|
|
65
|
+
);
|
|
66
|
+
|
|
60
67
|
async function loadPage(
|
|
61
68
|
embedded: EmbeddedData
|
|
62
69
|
): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode }> {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
70
|
+
const withoutExt = embedded.relativePath.replace(/\.(mdx|md)$/, '');
|
|
71
|
+
const key = embedded.relativePath.endsWith('.md')
|
|
72
|
+
? `../../.content/${withoutExt}.md`
|
|
73
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
74
|
+
const loader = contentModules[key];
|
|
75
|
+
const mod = loader ? await loader() : null;
|
|
76
|
+
const content = mod?.default
|
|
65
77
|
? React.createElement(mod.default, { components: mdxComponents })
|
|
66
78
|
: null;
|
|
67
79
|
return { slug: embedded.slug, frontmatter: embedded.frontmatter, content };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import '@raystack/apsara/normalize.css';
|
|
2
2
|
import '@raystack/apsara/style.css';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import { renderToReadableStream } from 'react-dom/server.edge';
|
|
6
5
|
import { StaticRouter } from 'react-router';
|
|
6
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
7
7
|
import { mdxComponents } from '@/components/mdx';
|
|
8
8
|
import { loadConfig } from '@/lib/config';
|
|
9
9
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
10
|
import { PageProvider } from '@/lib/page-context';
|
|
11
|
-
import {
|
|
11
|
+
import { getPageTree, getPage, loadPageComponent } from '@/lib/source';
|
|
12
12
|
import { App } from './App';
|
|
13
13
|
|
|
14
14
|
// @ts-expect-error virtual import from Nitro
|
|
@@ -27,25 +27,32 @@ export default {
|
|
|
27
27
|
? await loadApiSpecs(config.api).catch(() => [])
|
|
28
28
|
: [];
|
|
29
29
|
|
|
30
|
-
const [tree,
|
|
31
|
-
|
|
30
|
+
const [tree, page] = await Promise.all([
|
|
31
|
+
getPageTree(),
|
|
32
32
|
getPage(slug),
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const data = page?.data as Record<string, unknown> | undefined;
|
|
36
|
+
const relativePath = (data?._relativePath as string) ?? null;
|
|
37
|
+
|
|
38
|
+
const pageData = page
|
|
36
39
|
? {
|
|
37
40
|
slug,
|
|
38
|
-
frontmatter:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
frontmatter: {
|
|
42
|
+
title: (data?.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
|
|
43
|
+
description: data?.description as string | undefined,
|
|
44
|
+
order: data?.order as number | undefined,
|
|
45
|
+
icon: data?.icon as string | undefined,
|
|
46
|
+
lastModified: data?.lastModified as string | undefined,
|
|
47
|
+
},
|
|
48
|
+
content: relativePath
|
|
49
|
+
? await loadPageComponent(relativePath).then(component =>
|
|
50
|
+
component ? React.createElement(component, { components: mdxComponents }) : null
|
|
51
|
+
)
|
|
52
|
+
: null,
|
|
42
53
|
}
|
|
43
54
|
: null;
|
|
44
55
|
|
|
45
|
-
const relativePath = sourcePage
|
|
46
|
-
? path.relative(__CHRONICLE_CONTENT_DIR__, sourcePage.filePath)
|
|
47
|
-
: null;
|
|
48
|
-
|
|
49
56
|
const embeddedData = {
|
|
50
57
|
config,
|
|
51
58
|
tree,
|
|
@@ -74,14 +81,16 @@ export default {
|
|
|
74
81
|
<body>
|
|
75
82
|
<div id="root">
|
|
76
83
|
<StaticRouter location={pathname}>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
<ReactRouterProvider>
|
|
85
|
+
<PageProvider
|
|
86
|
+
initialConfig={config}
|
|
87
|
+
initialTree={tree}
|
|
88
|
+
initialPage={pageData}
|
|
89
|
+
initialApiSpecs={apiSpecs}
|
|
90
|
+
>
|
|
91
|
+
<App />
|
|
92
|
+
</PageProvider>
|
|
93
|
+
</ReactRouterProvider>
|
|
85
94
|
</StaticRouter>
|
|
86
95
|
</div>
|
|
87
96
|
</body>
|
|
@@ -16,8 +16,9 @@ export default defineHandler(async event => {
|
|
|
16
16
|
|
|
17
17
|
const pages = await getPages();
|
|
18
18
|
const docPages = pages.map(page => {
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const data = page.data as Record<string, unknown>;
|
|
20
|
+
const lastmod = data.lastModified
|
|
21
|
+
? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
|
|
21
22
|
: '';
|
|
22
23
|
return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
|
|
23
24
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
|
|
3
|
+
import { defineConfig as defineFumadocsConfig } from 'fumadocs-mdx/config';
|
|
2
4
|
import mdx from 'fumadocs-mdx/vite';
|
|
3
5
|
import { nitro } from 'nitro/vite';
|
|
4
6
|
import path from 'node:path';
|
|
7
|
+
import remarkDirective from 'remark-directive';
|
|
5
8
|
import { type InlineConfig } from 'vite';
|
|
9
|
+
import remarkUnusedDirectives from '../lib/remark-unused-directives';
|
|
6
10
|
|
|
7
11
|
export interface ViteConfigOptions {
|
|
8
12
|
packageRoot: string;
|
|
@@ -24,13 +28,39 @@ export async function createViteConfig(
|
|
|
24
28
|
serverDir: path.resolve(packageRoot, 'src/server'),
|
|
25
29
|
...(preset && { preset }),
|
|
26
30
|
}),
|
|
27
|
-
mdx({
|
|
31
|
+
mdx({
|
|
32
|
+
default: defineFumadocsConfig({
|
|
33
|
+
mdxOptions: {
|
|
34
|
+
remarkPlugins: [
|
|
35
|
+
remarkDirective,
|
|
36
|
+
[remarkDirectiveAdmonition, {
|
|
37
|
+
tags: {
|
|
38
|
+
CalloutContainer: 'Callout',
|
|
39
|
+
CalloutTitle: 'CalloutTitle',
|
|
40
|
+
CalloutDescription: 'CalloutDescription',
|
|
41
|
+
},
|
|
42
|
+
types: {
|
|
43
|
+
note: 'accent',
|
|
44
|
+
tip: 'accent',
|
|
45
|
+
info: 'accent',
|
|
46
|
+
warn: 'attention',
|
|
47
|
+
warning: 'attention',
|
|
48
|
+
danger: 'alert',
|
|
49
|
+
caution: 'alert',
|
|
50
|
+
success: 'success',
|
|
51
|
+
},
|
|
52
|
+
}],
|
|
53
|
+
remarkUnusedDirectives,
|
|
54
|
+
remarkMdxMermaid,
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
}, { index: false }),
|
|
28
59
|
react()
|
|
29
60
|
],
|
|
30
61
|
resolve: {
|
|
31
62
|
alias: {
|
|
32
63
|
'@': path.resolve(packageRoot, 'src'),
|
|
33
|
-
'@content': path.resolve(packageRoot, '.content'),
|
|
34
64
|
},
|
|
35
65
|
conditions: ['module-sync', 'import', 'node'],
|
|
36
66
|
dedupe: [
|
|
@@ -48,8 +78,7 @@ export async function createViteConfig(
|
|
|
48
78
|
},
|
|
49
79
|
define: {
|
|
50
80
|
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
|
|
51
|
-
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
52
|
-
__CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
|
|
81
|
+
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
53
82
|
},
|
|
54
83
|
css: {
|
|
55
84
|
modules: {
|
|
@@ -14,7 +14,8 @@ import { MethodBadge } from '@/components/api/method-badge';
|
|
|
14
14
|
import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
|
|
15
15
|
import { Footer } from '@/components/ui/footer';
|
|
16
16
|
import { Search } from '@/components/ui/search';
|
|
17
|
-
import type {
|
|
17
|
+
import type { Node } from 'fumadocs-core/page-tree';
|
|
18
|
+
import type { ThemeLayoutProps } from '@/types';
|
|
18
19
|
import styles from './Layout.module.css';
|
|
19
20
|
|
|
20
21
|
const iconMap: Record<string, React.ReactNode> = {
|
|
@@ -96,9 +97,9 @@ export function Layout({
|
|
|
96
97
|
className={cx(styles.sidebar, classNames?.sidebar)}
|
|
97
98
|
>
|
|
98
99
|
<Sidebar.Main ref={scrollRef}>
|
|
99
|
-
{tree.children.map(item => (
|
|
100
|
+
{tree.children.map((item, i) => (
|
|
100
101
|
<SidebarNode
|
|
101
|
-
key={item.url
|
|
102
|
+
key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
|
|
102
103
|
item={item}
|
|
103
104
|
pathname={pathname}
|
|
104
105
|
/>
|
|
@@ -118,23 +119,24 @@ function SidebarNode({
|
|
|
118
119
|
item,
|
|
119
120
|
pathname
|
|
120
121
|
}: {
|
|
121
|
-
item:
|
|
122
|
+
item: Node;
|
|
122
123
|
pathname: string;
|
|
123
124
|
}) {
|
|
124
125
|
if (item.type === 'separator') {
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
if (item.type === 'folder'
|
|
129
|
+
if (item.type === 'folder') {
|
|
130
|
+
const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
|
|
129
131
|
return (
|
|
130
132
|
<Sidebar.Group
|
|
131
|
-
label={item.name}
|
|
132
|
-
leadingIcon={
|
|
133
|
+
label={item.name?.toString() ?? ''}
|
|
134
|
+
leadingIcon={icon ?? undefined}
|
|
133
135
|
classNames={{ items: styles.groupItems }}
|
|
134
136
|
>
|
|
135
|
-
{item.children.map(child => (
|
|
137
|
+
{item.children.map((child, i) => (
|
|
136
138
|
<SidebarNode
|
|
137
|
-
key={child.url
|
|
139
|
+
key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
|
|
138
140
|
item={child}
|
|
139
141
|
pathname={pathname}
|
|
140
142
|
/>
|
|
@@ -145,13 +147,14 @@ function SidebarNode({
|
|
|
145
147
|
|
|
146
148
|
const isActive = pathname === item.url;
|
|
147
149
|
const href = item.url ?? '#';
|
|
150
|
+
const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
|
|
148
151
|
const link = useMemo(() => <RouterLink to={href} />, [href]);
|
|
149
152
|
|
|
150
153
|
return (
|
|
151
154
|
<Sidebar.Item
|
|
152
155
|
href={href}
|
|
153
156
|
active={isActive}
|
|
154
|
-
leadingIcon={
|
|
157
|
+
leadingIcon={icon ?? undefined}
|
|
155
158
|
as={link}
|
|
156
159
|
>
|
|
157
160
|
{item.name}
|
|
@@ -38,9 +38,53 @@
|
|
|
38
38
|
margin-bottom: var(--rs-space-3);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.content img {
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
height: auto;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
.content table {
|
|
42
47
|
display: block;
|
|
43
48
|
max-width: 100%;
|
|
44
49
|
overflow-x: auto;
|
|
45
50
|
margin-bottom: var(--rs-space-5);
|
|
46
51
|
}
|
|
52
|
+
|
|
53
|
+
.content details {
|
|
54
|
+
border: 1px solid var(--rs-color-border-base-primary);
|
|
55
|
+
border-radius: var(--rs-radius-2);
|
|
56
|
+
margin: var(--rs-space-5) 0;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.content details summary {
|
|
61
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
font-size: var(--rs-font-size-small);
|
|
65
|
+
color: var(--rs-color-text-base-primary);
|
|
66
|
+
background: var(--rs-color-background-base-secondary);
|
|
67
|
+
list-style: none;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: var(--rs-space-3);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.content details summary::-webkit-details-marker {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.content details summary::before {
|
|
78
|
+
content: '▶';
|
|
79
|
+
font-size: 10px;
|
|
80
|
+
transition: transform 0.2s ease;
|
|
81
|
+
color: var(--rs-color-text-base-secondary);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.content details[open] > summary::before {
|
|
85
|
+
transform: rotate(90deg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.content details > :not(summary) {
|
|
89
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
90
|
+
}
|
|
@@ -1,46 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Text } from '@raystack/apsara';
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
4
|
+
import { AnchorProvider, useActiveAnchor } from 'fumadocs-core/toc';
|
|
5
|
+
import type { TableOfContents, TOCItemType } from 'fumadocs-core/toc';
|
|
6
6
|
import styles from './Toc.module.css';
|
|
7
7
|
|
|
8
8
|
interface TocProps {
|
|
9
|
-
items:
|
|
9
|
+
items: TableOfContents;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function Toc({ items }: TocProps) {
|
|
13
|
-
const [activeId, setActiveId] = useState<string>('');
|
|
14
|
-
|
|
15
|
-
// Filter to only show h2 and h3 headings
|
|
16
13
|
const filteredItems = items.filter(
|
|
17
14
|
item => item.depth >= 2 && item.depth <= 3
|
|
18
15
|
);
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
const headingIds = filteredItems.map(item => item.url.replace('#', ''));
|
|
22
|
-
|
|
23
|
-
const observer = new IntersectionObserver(
|
|
24
|
-
entries => {
|
|
25
|
-
entries.forEach(entry => {
|
|
26
|
-
if (entry.isIntersecting) {
|
|
27
|
-
setActiveId(entry.target.id);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
// -80px top: offset for fixed header, -80% bottom: trigger when heading is in top 20% of viewport
|
|
32
|
-
{ rootMargin: '-80px 0px -80% 0px' }
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
headingIds.forEach(id => {
|
|
36
|
-
const element = document.getElementById(id);
|
|
37
|
-
if (element) observer.observe(element);
|
|
38
|
-
});
|
|
17
|
+
if (filteredItems.length === 0) return null;
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
return (
|
|
20
|
+
<AnchorProvider toc={filteredItems} single>
|
|
21
|
+
<TocContent items={filteredItems} />
|
|
22
|
+
</AnchorProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
42
25
|
|
|
43
|
-
|
|
26
|
+
function TocContent({ items }: { items: TOCItemType[] }) {
|
|
27
|
+
const activeAnchor = useActiveAnchor();
|
|
44
28
|
|
|
45
29
|
return (
|
|
46
30
|
<aside className={styles.toc}>
|
|
@@ -48,9 +32,9 @@ export function Toc({ items }: TocProps) {
|
|
|
48
32
|
On this page
|
|
49
33
|
</Text>
|
|
50
34
|
<nav className={styles.nav}>
|
|
51
|
-
{
|
|
35
|
+
{items.map(item => {
|
|
52
36
|
const id = item.url.replace('#', '');
|
|
53
|
-
const isActive =
|
|
37
|
+
const isActive = activeAnchor === id;
|
|
54
38
|
const isNested = item.depth > 2;
|
|
55
39
|
return (
|
|
56
40
|
<a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Link as RouterLink, useLocation } from 'react-router';
|
|
2
2
|
import { MethodBadge } from '@/components/api/method-badge';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Root, Node } from 'fumadocs-core/page-tree';
|
|
4
4
|
import styles from './ChapterNav.module.css';
|
|
5
5
|
|
|
6
6
|
const iconMap: Record<string, React.ReactNode> = {
|
|
@@ -12,16 +12,16 @@ const iconMap: Record<string, React.ReactNode> = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
interface ChapterNavProps {
|
|
15
|
-
tree:
|
|
15
|
+
tree: Root;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function buildChapterIndices(
|
|
19
|
-
children:
|
|
20
|
-
): Map<
|
|
21
|
-
const indices = new Map<
|
|
19
|
+
children: Node[]
|
|
20
|
+
): Map<Node, number> {
|
|
21
|
+
const indices = new Map<Node, number>();
|
|
22
22
|
let index = 0;
|
|
23
23
|
for (const item of children) {
|
|
24
|
-
if (item.type === 'folder'
|
|
24
|
+
if (item.type === 'folder') {
|
|
25
25
|
index++;
|
|
26
26
|
indices.set(item, index);
|
|
27
27
|
}
|
|
@@ -39,17 +39,17 @@ export function ChapterNav({ tree }: ChapterNavProps) {
|
|
|
39
39
|
{tree.children.map(item => {
|
|
40
40
|
if (item.type === 'separator') return null;
|
|
41
41
|
|
|
42
|
-
if (item.type === 'folder'
|
|
42
|
+
if (item.type === 'folder') {
|
|
43
43
|
const chapterIndex = chapterIndices.get(item) ?? 0;
|
|
44
44
|
return (
|
|
45
|
-
<li key={item.name} className={styles.chapter}>
|
|
45
|
+
<li key={item.name?.toString()} className={styles.chapter}>
|
|
46
46
|
<span className={styles.chapterLabel}>
|
|
47
47
|
{String(chapterIndex).padStart(2, '0')}. {item.name}
|
|
48
48
|
</span>
|
|
49
49
|
<ul className={styles.chapterItems}>
|
|
50
50
|
{item.children.map(child => (
|
|
51
51
|
<ChapterItem
|
|
52
|
-
key={child.url
|
|
52
|
+
key={child.type === 'page' ? child.url : (child.name?.toString() ?? '')}
|
|
53
53
|
item={child}
|
|
54
54
|
pathname={pathname}
|
|
55
55
|
/>
|
|
@@ -61,7 +61,7 @@ export function ChapterNav({ tree }: ChapterNavProps) {
|
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
63
|
<ChapterItem
|
|
64
|
-
key={item.url ?? item.name}
|
|
64
|
+
key={item.url ?? item.name?.toString() ?? ''}
|
|
65
65
|
item={item}
|
|
66
66
|
pathname={pathname}
|
|
67
67
|
/>
|
|
@@ -76,19 +76,19 @@ function ChapterItem({
|
|
|
76
76
|
item,
|
|
77
77
|
pathname
|
|
78
78
|
}: {
|
|
79
|
-
item:
|
|
79
|
+
item: Node;
|
|
80
80
|
pathname: string;
|
|
81
81
|
}) {
|
|
82
82
|
if (item.type === 'separator') return null;
|
|
83
83
|
|
|
84
|
-
if (item.type === 'folder'
|
|
84
|
+
if (item.type === 'folder') {
|
|
85
85
|
return (
|
|
86
86
|
<li>
|
|
87
87
|
<span className={styles.subLabel}>{item.name}</span>
|
|
88
88
|
<ul className={styles.chapterItems}>
|
|
89
89
|
{item.children.map(child => (
|
|
90
90
|
<ChapterItem
|
|
91
|
-
key={child.url
|
|
91
|
+
key={child.type === 'page' ? child.url : (child.name?.toString() ?? '')}
|
|
92
92
|
item={child}
|
|
93
93
|
pathname={pathname}
|
|
94
94
|
/>
|
|
@@ -99,7 +99,7 @@ function ChapterItem({
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const isActive = pathname === item.url;
|
|
102
|
-
const icon = item.icon ? iconMap[item.icon] :
|
|
102
|
+
const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
|
|
103
103
|
|
|
104
104
|
return (
|
|
105
105
|
<li>
|
|
@@ -2,59 +2,33 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
|
|
2
2
|
import { Flex } from '@raystack/apsara';
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import { Link as RouterLink, useLocation } from 'react-router';
|
|
5
|
+
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
|
|
6
|
+
import { flattenTree } from 'fumadocs-core/page-tree';
|
|
5
7
|
import { Search } from '@/components/ui/search';
|
|
6
|
-
import type {
|
|
8
|
+
import type { ThemePageProps } from '@/types';
|
|
7
9
|
import styles from './Page.module.css';
|
|
8
10
|
import { ReadingProgress } from './ReadingProgress';
|
|
9
11
|
|
|
10
|
-
function flattenTree(items: PageTreeItem[]): PageTreeItem[] {
|
|
11
|
-
const result: PageTreeItem[] = [];
|
|
12
|
-
for (const item of items) {
|
|
13
|
-
if (item.type === 'page' && item.url) result.push(item);
|
|
14
|
-
if (item.children) result.push(...flattenTree(item.children));
|
|
15
|
-
}
|
|
16
|
-
return result;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function findBreadcrumb(
|
|
20
|
-
items: PageTreeItem[],
|
|
21
|
-
slug: string[]
|
|
22
|
-
): { label: string; href: string }[] {
|
|
23
|
-
const result: { label: string; href: string }[] = [];
|
|
24
|
-
for (let i = 0; i < slug.length; i++) {
|
|
25
|
-
const path = '/' + slug.slice(0, i + 1).join('/');
|
|
26
|
-
const found = findInTree(items, path);
|
|
27
|
-
result.push({ label: found?.name ?? slug[i], href: path });
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function findInTree(
|
|
33
|
-
items: PageTreeItem[],
|
|
34
|
-
path: string
|
|
35
|
-
): PageTreeItem | undefined {
|
|
36
|
-
for (const item of items) {
|
|
37
|
-
if (item.url === path) return item;
|
|
38
|
-
if (item.children) {
|
|
39
|
-
const found = findInTree(item.children, path);
|
|
40
|
-
if (found) return found;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
12
|
export function Page({ page, config, tree }: ThemePageProps) {
|
|
47
13
|
const { pathname } = useLocation();
|
|
48
14
|
|
|
49
15
|
const { prev, next, crumbs } = useMemo(() => {
|
|
50
16
|
const pages = flattenTree(tree.children);
|
|
51
17
|
const currentIndex = pages.findIndex(p => p.url === pathname);
|
|
18
|
+
const breadcrumbItems = getBreadcrumbItems(
|
|
19
|
+
pathname,
|
|
20
|
+
tree,
|
|
21
|
+
{ includePage: true }
|
|
22
|
+
);
|
|
52
23
|
return {
|
|
53
24
|
prev: currentIndex > 0 ? pages[currentIndex - 1] : null,
|
|
54
25
|
next: currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null,
|
|
55
|
-
crumbs:
|
|
26
|
+
crumbs: breadcrumbItems.map(item => ({
|
|
27
|
+
label: item.name,
|
|
28
|
+
href: item.url ?? pathname,
|
|
29
|
+
})),
|
|
56
30
|
};
|
|
57
|
-
}, [tree, pathname
|
|
31
|
+
}, [tree, pathname]);
|
|
58
32
|
|
|
59
33
|
return (
|
|
60
34
|
<>
|
|
@@ -63,7 +37,7 @@ export function Page({ page, config, tree }: ThemePageProps) {
|
|
|
63
37
|
<Flex align='center' gap='small' className={styles.navLeft}>
|
|
64
38
|
{prev ? (
|
|
65
39
|
<RouterLink
|
|
66
|
-
to={prev.url
|
|
40
|
+
to={prev.url}
|
|
67
41
|
className={styles.arrow}
|
|
68
42
|
aria-label='Previous page'
|
|
69
43
|
>
|
|
@@ -80,7 +54,7 @@ export function Page({ page, config, tree }: ThemePageProps) {
|
|
|
80
54
|
)}
|
|
81
55
|
{next ? (
|
|
82
56
|
<RouterLink
|
|
83
|
-
to={next.url
|
|
57
|
+
to={next.url}
|
|
84
58
|
className={styles.arrow}
|
|
85
59
|
aria-label='Next page'
|
|
86
60
|
>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { cx } from 'class-variance-authority';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import type {
|
|
5
|
+
import type { TOCItemType } from 'fumadocs-core/toc';
|
|
6
6
|
import styles from './ReadingProgress.module.css';
|
|
7
7
|
|
|
8
8
|
interface Heading {
|
|
@@ -68,7 +68,7 @@ function resolveOverlaps(headings: Heading[], maxPosition: number): Heading[] {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
interface ReadingProgressProps {
|
|
71
|
-
items:
|
|
71
|
+
items: TOCItemType[];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function ReadingProgress({ items }: ReadingProgressProps) {
|
package/src/types/content.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
+
import type { TableOfContents } from 'fumadocs-core/toc'
|
|
3
|
+
|
|
4
|
+
export type { Root, Node, Item, Folder, Separator } from 'fumadocs-core/page-tree'
|
|
5
|
+
export type { TOCItemType, TableOfContents } from 'fumadocs-core/toc'
|
|
2
6
|
|
|
3
7
|
export interface Frontmatter {
|
|
4
8
|
title: string
|
|
@@ -12,25 +16,5 @@ export interface Page {
|
|
|
12
16
|
slug: string[]
|
|
13
17
|
frontmatter: Frontmatter
|
|
14
18
|
content: ReactNode
|
|
15
|
-
toc:
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface TocItem {
|
|
19
|
-
title: string
|
|
20
|
-
url: string
|
|
21
|
-
depth: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface PageTreeItem {
|
|
25
|
-
type: 'page' | 'folder' | 'separator'
|
|
26
|
-
name: string
|
|
27
|
-
url?: string
|
|
28
|
-
order?: number
|
|
29
|
-
icon?: string
|
|
30
|
-
children?: PageTreeItem[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface PageTree {
|
|
34
|
-
name: string
|
|
35
|
-
children: PageTreeItem[]
|
|
19
|
+
toc: TableOfContents
|
|
36
20
|
}
|
package/src/types/globals.d.ts
CHANGED
package/src/types/theme.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
+
import type { Root } from 'fumadocs-core/page-tree'
|
|
2
3
|
import type { ChronicleConfig } from './config'
|
|
3
|
-
import type { Page
|
|
4
|
+
import type { Page } from './content'
|
|
4
5
|
|
|
5
6
|
export interface ThemeLayoutProps {
|
|
6
7
|
children: ReactNode
|
|
7
8
|
config: ChronicleConfig
|
|
8
|
-
tree:
|
|
9
|
+
tree: Root
|
|
9
10
|
classNames?: { layout?: string; body?: string; sidebar?: string; content?: string }
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ThemePageProps {
|
|
13
14
|
page: Page
|
|
14
15
|
config: ChronicleConfig
|
|
15
|
-
tree:
|
|
16
|
+
tree: Root
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface Theme {
|