@raystack/chronicle 0.1.0-canary.323385a → 0.1.0-canary.3e58cd9
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 +268 -9902
- package/package.json +20 -12
- package/src/cli/commands/build.ts +27 -25
- package/src/cli/commands/dev.ts +24 -25
- package/src/cli/commands/init.ts +38 -132
- package/src/cli/commands/serve.ts +36 -49
- package/src/cli/commands/start.ts +20 -25
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/resolve.ts +9 -3
- package/src/cli/utils/scaffold.ts +11 -124
- 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/mdx/image.tsx +5 -34
- package/src/components/mdx/index.tsx +15 -1
- package/src/components/mdx/link.tsx +18 -15
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/search.tsx +63 -51
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +12 -35
- package/src/lib/head.tsx +49 -0
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +111 -0
- package/src/lib/source.ts +134 -63
- package/src/pages/ApiLayout.tsx +33 -0
- package/src/pages/ApiPage.tsx +73 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +17 -0
- package/src/server/App.tsx +67 -0
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +17 -0
- package/src/server/api/search.ts +118 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +86 -0
- package/src/server/entry-server.tsx +100 -0
- package/src/server/routes/llms.txt.ts +21 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +40 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +126 -0
- package/src/themes/default/Layout.tsx +78 -48
- package/src/themes/default/Page.module.css +44 -0
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +25 -39
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +64 -45
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +16 -4
- package/src/themes/paper/Page.tsx +56 -63
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +7 -7
- package/src/types/config.ts +11 -0
- package/src/types/content.ts +6 -21
- package/src/types/globals.d.ts +4 -0
- package/src/types/theme.ts +4 -3
- package/tsconfig.json +2 -3
- package/next.config.mjs +0 -10
- package/source.config.ts +0 -50
- package/src/app/[[...slug]]/layout.tsx +0 -15
- package/src/app/[[...slug]]/page.tsx +0 -57
- package/src/app/api/apis-proxy/route.ts +0 -59
- package/src/app/api/health/route.ts +0 -3
- package/src/app/api/search/route.ts +0 -90
- package/src/app/apis/[[...slug]]/layout.tsx +0 -26
- package/src/app/apis/[[...slug]]/page.tsx +0 -57
- package/src/app/layout.tsx +0 -26
- package/src/app/llms-full.txt/route.ts +0 -18
- package/src/app/llms.txt/route.ts +0 -15
- package/src/app/providers.tsx +0 -8
- package/src/cli/utils/process.ts +0 -7
- package/src/themes/default/font.ts +0 -6
- /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
package/src/cli/utils/resolve.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import { fileURLToPath } from 'url'
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// After bundling: dist/cli/index.js → ../.. = package root
|
|
5
|
+
// After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
|
|
6
|
+
export const PACKAGE_ROOT = path.resolve(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
'..',
|
|
9
|
+
'..'
|
|
10
|
+
);
|
|
@@ -1,131 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import chalk from 'chalk'
|
|
6
|
-
import { PACKAGE_ROOT } from './resolve'
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT } from './resolve';
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
export async function linkContent(contentDir: string): Promise<void> {
|
|
6
|
+
const linkPath = path.join(PACKAGE_ROOT, '.content');
|
|
7
|
+
const target = path.resolve(contentDir);
|
|
9
8
|
|
|
10
|
-
function copyRecursive(src: string, dest: string) {
|
|
11
|
-
const stat = fs.statSync(src)
|
|
12
|
-
if (stat.isDirectory()) {
|
|
13
|
-
fs.mkdirSync(dest, { recursive: true })
|
|
14
|
-
for (const entry of fs.readdirSync(src)) {
|
|
15
|
-
copyRecursive(path.join(src, entry), path.join(dest, entry))
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
fs.copyFileSync(src, dest)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ensureRemoved(targetPath: string) {
|
|
23
9
|
try {
|
|
24
|
-
fs.
|
|
25
|
-
|
|
10
|
+
const existing = await fs.readlink(linkPath);
|
|
11
|
+
if (existing === target) return;
|
|
12
|
+
await fs.unlink(linkPath);
|
|
26
13
|
} catch {
|
|
27
|
-
//
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function detectPackageManager(): string {
|
|
32
|
-
if (process.env.npm_config_user_agent) {
|
|
33
|
-
return process.env.npm_config_user_agent.split('/')[0]
|
|
34
|
-
}
|
|
35
|
-
const cwd = process.cwd()
|
|
36
|
-
if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
|
|
37
|
-
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
38
|
-
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
|
39
|
-
return 'npm'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function generateNextConfig(scaffoldPath: string) {
|
|
43
|
-
const config = `import { createMDX } from 'fumadocs-mdx/next'
|
|
44
|
-
|
|
45
|
-
const withMDX = createMDX()
|
|
46
|
-
|
|
47
|
-
/** @type {import('next').NextConfig} */
|
|
48
|
-
const nextConfig = {
|
|
49
|
-
reactStrictMode: true,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default withMDX(nextConfig)
|
|
53
|
-
`
|
|
54
|
-
fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function createPackageJson(): Record<string, unknown> {
|
|
58
|
-
return {
|
|
59
|
-
name: 'chronicle-docs',
|
|
60
|
-
private: true,
|
|
61
|
-
dependencies: {
|
|
62
|
-
'@raystack/chronicle': 'latest',
|
|
63
|
-
},
|
|
64
|
-
devDependencies: {
|
|
65
|
-
'@raystack/tools-config': '0.56.0',
|
|
66
|
-
'openapi-types': '^12.1.3',
|
|
67
|
-
typescript: '5.9.3',
|
|
68
|
-
'@types/react': '^19.2.10',
|
|
69
|
-
'@types/node': '^25.1.0',
|
|
70
|
-
},
|
|
14
|
+
// link doesn't exist
|
|
71
15
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function ensureDeps() {
|
|
75
|
-
const cwd = process.cwd()
|
|
76
|
-
const cwdPkgJson = path.join(cwd, 'package.json')
|
|
77
|
-
const cwdNodeModules = path.join(cwd, 'node_modules')
|
|
78
|
-
|
|
79
|
-
if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) {
|
|
80
|
-
// Case 1: existing project with deps installed
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Case 2: no package.json — create in cwd and install
|
|
85
|
-
if (!fs.existsSync(cwdPkgJson)) {
|
|
86
|
-
fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!fs.existsSync(cwdNodeModules)) {
|
|
90
|
-
const pm = detectPackageManager()
|
|
91
|
-
console.log(chalk.cyan(`Installing dependencies with ${pm}...`))
|
|
92
|
-
execSync(`${pm} install`, { cwd, stdio: 'inherit' })
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function resolveNextCli(): string {
|
|
97
|
-
const cwdRequire = createRequire(path.join(process.cwd(), 'package.json'))
|
|
98
|
-
return cwdRequire.resolve('next/dist/bin/next')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function scaffoldDir(contentDir: string): string {
|
|
102
|
-
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
103
|
-
|
|
104
|
-
// Create .chronicle/ if not exists
|
|
105
|
-
if (!fs.existsSync(scaffoldPath)) {
|
|
106
|
-
fs.mkdirSync(scaffoldPath, { recursive: true })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Copy package files
|
|
110
|
-
for (const name of COPY_FILES) {
|
|
111
|
-
const src = path.join(PACKAGE_ROOT, name)
|
|
112
|
-
const dest = path.join(scaffoldPath, name)
|
|
113
|
-
ensureRemoved(dest)
|
|
114
|
-
copyRecursive(src, dest)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Generate next.config.mjs
|
|
118
|
-
generateNextConfig(scaffoldPath)
|
|
119
|
-
|
|
120
|
-
// Symlink content dir
|
|
121
|
-
const contentLink = path.join(scaffoldPath, 'content')
|
|
122
|
-
ensureRemoved(contentLink)
|
|
123
|
-
fs.symlinkSync(path.resolve(contentDir), contentLink)
|
|
124
|
-
|
|
125
|
-
// Ensure dependencies are available
|
|
126
|
-
ensureDeps()
|
|
127
|
-
|
|
128
|
-
console.log(chalk.gray(`Scaffold: ${scaffoldPath}`))
|
|
129
16
|
|
|
130
|
-
|
|
17
|
+
await fs.symlink(target, linkPath);
|
|
131
18
|
}
|
|
@@ -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,38 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import type { ComponentProps } from 'react'
|
|
3
|
+
type ImageProps = ComponentProps<'img'>;
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
src
|
|
8
|
-
width?: number | string
|
|
9
|
-
height?: number | string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function Image({ src, alt, width, height, ...props }: ImageProps) {
|
|
13
|
-
if (!src || typeof src !== 'string') return null
|
|
14
|
-
|
|
15
|
-
const isExternal = src.startsWith('http://') || src.startsWith('https://')
|
|
16
|
-
|
|
17
|
-
if (isExternal) {
|
|
18
|
-
return (
|
|
19
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
20
|
-
<img
|
|
21
|
-
src={src}
|
|
22
|
-
alt={alt ?? ''}
|
|
23
|
-
width={width}
|
|
24
|
-
height={height}
|
|
25
|
-
{...props}
|
|
26
|
-
/>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
5
|
+
export function Image({ src, alt, ...props }: ImageProps) {
|
|
6
|
+
if (!src) return null;
|
|
29
7
|
|
|
30
|
-
return
|
|
31
|
-
<NextImage
|
|
32
|
-
src={src}
|
|
33
|
-
alt={alt ?? ''}
|
|
34
|
-
width={typeof width === 'string' ? parseInt(width, 10) : (width ?? 800)}
|
|
35
|
-
height={typeof height === 'string' ? parseInt(height, 10) : (height ?? 400)}
|
|
36
|
-
/>
|
|
37
|
-
)
|
|
8
|
+
return <img src={src} alt={alt ?? ''} loading='lazy' {...props} />;
|
|
38
9
|
}
|
|
@@ -8,6 +8,20 @@ import { Mermaid } from './mermaid'
|
|
|
8
8
|
import { MdxParagraph } from './paragraph'
|
|
9
9
|
import { CalloutContainer, CalloutTitle, CalloutDescription, MdxBlockquote } from '@/components/common/callout'
|
|
10
10
|
import { Tabs } from '@raystack/apsara'
|
|
11
|
+
import { type ComponentProps, useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
function ClientOnly({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const [mounted, setMounted] = useState(false)
|
|
15
|
+
useEffect(() => setMounted(true), [])
|
|
16
|
+
return mounted ? <>{children}</> : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function MdxTabs(props: ComponentProps<typeof Tabs>) {
|
|
20
|
+
return <ClientOnly><Tabs {...props} /></ClientOnly>
|
|
21
|
+
}
|
|
22
|
+
MdxTabs.List = Tabs.List
|
|
23
|
+
MdxTabs.Trigger = Tabs.Trigger
|
|
24
|
+
MdxTabs.Content = Tabs.Content
|
|
11
25
|
|
|
12
26
|
export const mdxComponents: MDXComponents = {
|
|
13
27
|
p: MdxParagraph,
|
|
@@ -27,7 +41,7 @@ export const mdxComponents: MDXComponents = {
|
|
|
27
41
|
Callout: CalloutContainer,
|
|
28
42
|
CalloutTitle,
|
|
29
43
|
CalloutDescription,
|
|
30
|
-
Tabs,
|
|
44
|
+
Tabs: MdxTabs,
|
|
31
45
|
Mermaid,
|
|
32
46
|
}
|
|
33
47
|
|
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
import { Link as ApsaraLink } from '@raystack/apsara';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { Link as RouterLink } from 'react-router';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
import { Link as ApsaraLink } from '@raystack/apsara'
|
|
5
|
-
import type { ComponentProps } from 'react'
|
|
6
|
-
|
|
7
|
-
type LinkProps = ComponentProps<'a'>
|
|
5
|
+
type LinkProps = ComponentProps<'a'>;
|
|
8
6
|
|
|
9
7
|
export function Link({ href, children, ...props }: LinkProps) {
|
|
10
8
|
if (!href) {
|
|
11
|
-
return <span {...props}>{children}</span
|
|
9
|
+
return <span {...props}>{children}</span>;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
const isExternal = href.startsWith('http://') || href.startsWith('https://')
|
|
15
|
-
const isAnchor = href.startsWith('#')
|
|
12
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
13
|
+
const isAnchor = href.startsWith('#');
|
|
16
14
|
|
|
17
15
|
if (isAnchor) {
|
|
18
16
|
return (
|
|
19
17
|
<ApsaraLink href={href} {...props}>
|
|
20
18
|
{children}
|
|
21
19
|
</ApsaraLink>
|
|
22
|
-
)
|
|
20
|
+
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
if (isExternal) {
|
|
26
24
|
return (
|
|
27
|
-
<ApsaraLink
|
|
25
|
+
<ApsaraLink
|
|
26
|
+
href={href}
|
|
27
|
+
target='_blank'
|
|
28
|
+
rel='noopener noreferrer'
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
28
31
|
{children}
|
|
29
32
|
</ApsaraLink>
|
|
30
|
-
)
|
|
33
|
+
);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
return (
|
|
34
|
-
<
|
|
37
|
+
<RouterLink to={href} className={props.className}>
|
|
35
38
|
{children}
|
|
36
|
-
</
|
|
37
|
-
)
|
|
39
|
+
</RouterLink>
|
|
40
|
+
);
|
|
38
41
|
}
|
|
@@ -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]
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { isMacOs } from "react-device-detect";
|
|
11
|
-
import { MethodBadge } from "@/components/api/method-badge";
|
|
12
|
-
import styles from "./search.module.css";
|
|
1
|
+
import { DocumentIcon, HashtagIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { Button, Command, Dialog, Text } from '@raystack/apsara';
|
|
3
|
+
import { cx } from 'class-variance-authority';
|
|
4
|
+
import type { SortedResult } from 'fumadocs-core/search';
|
|
5
|
+
import { useDocsSearch } from 'fumadocs-core/search/client';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { useNavigate } from 'react-router';
|
|
8
|
+
import { MethodBadge } from '@/components/api/method-badge';
|
|
9
|
+
import styles from './search.module.css';
|
|
13
10
|
|
|
14
11
|
function SearchShortcutKey({ className }: { className?: string }) {
|
|
15
|
-
const [key, setKey] = useState(
|
|
12
|
+
const [key, setKey] = useState('⌘');
|
|
16
13
|
|
|
17
14
|
useEffect(() => {
|
|
18
|
-
|
|
15
|
+
const isMac = navigator.platform?.toUpperCase().includes('MAC');
|
|
16
|
+
setKey(isMac ? '⌘' : 'Ctrl');
|
|
19
17
|
}, []);
|
|
20
18
|
|
|
21
19
|
return (
|
|
@@ -26,50 +24,50 @@ function SearchShortcutKey({ className }: { className?: string }) {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
interface SearchProps {
|
|
29
|
-
className?: string
|
|
27
|
+
className?: string;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export function Search({ className }: SearchProps) {
|
|
33
31
|
const [open, setOpen] = useState(false);
|
|
34
|
-
const
|
|
32
|
+
const navigate = useNavigate();
|
|
35
33
|
|
|
36
34
|
const { search, setSearch, query } = useDocsSearch({
|
|
37
|
-
type:
|
|
38
|
-
api:
|
|
35
|
+
type: 'fetch',
|
|
36
|
+
api: '/api/search',
|
|
39
37
|
delayMs: 100,
|
|
40
|
-
allowEmpty: true
|
|
38
|
+
allowEmpty: true
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
const onSelect = useCallback(
|
|
44
42
|
(url: string) => {
|
|
45
43
|
setOpen(false);
|
|
46
|
-
|
|
44
|
+
navigate(url);
|
|
47
45
|
},
|
|
48
|
-
[
|
|
46
|
+
[navigate]
|
|
49
47
|
);
|
|
50
48
|
|
|
51
49
|
useEffect(() => {
|
|
52
50
|
const down = (e: KeyboardEvent) => {
|
|
53
|
-
if (e.key ===
|
|
51
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
54
52
|
e.preventDefault();
|
|
55
|
-
setOpen(
|
|
53
|
+
setOpen(open => !open);
|
|
56
54
|
}
|
|
57
55
|
};
|
|
58
56
|
|
|
59
|
-
document.addEventListener(
|
|
60
|
-
return () => document.removeEventListener(
|
|
57
|
+
document.addEventListener('keydown', down);
|
|
58
|
+
return () => document.removeEventListener('keydown', down);
|
|
61
59
|
}, []);
|
|
62
60
|
|
|
63
61
|
const results = deduplicateByUrl(
|
|
64
|
-
query.data ===
|
|
62
|
+
query.data === 'empty' ? [] : (query.data ?? [])
|
|
65
63
|
);
|
|
66
64
|
|
|
67
65
|
return (
|
|
68
66
|
<>
|
|
69
67
|
<Button
|
|
70
|
-
variant=
|
|
71
|
-
color=
|
|
72
|
-
size=
|
|
68
|
+
variant='outline'
|
|
69
|
+
color='neutral'
|
|
70
|
+
size='small'
|
|
73
71
|
onClick={() => setOpen(true)}
|
|
74
72
|
className={cx(styles.trigger, className)}
|
|
75
73
|
trailingIcon={<SearchShortcutKey className={styles.kbd} />}
|
|
@@ -84,7 +82,7 @@ export function Search({ className }: SearchProps) {
|
|
|
84
82
|
</Dialog.Title>
|
|
85
83
|
<Command loop>
|
|
86
84
|
<Command.Input
|
|
87
|
-
placeholder=
|
|
85
|
+
placeholder='Search'
|
|
88
86
|
value={search}
|
|
89
87
|
onValueChange={setSearch}
|
|
90
88
|
className={styles.input}
|
|
@@ -100,7 +98,7 @@ export function Search({ className }: SearchProps) {
|
|
|
100
98
|
{!query.isLoading &&
|
|
101
99
|
search.length === 0 &&
|
|
102
100
|
results.length > 0 && (
|
|
103
|
-
<Command.Group heading=
|
|
101
|
+
<Command.Group heading='Suggestions'>
|
|
104
102
|
{results.slice(0, 8).map((result: SortedResult) => (
|
|
105
103
|
<Command.Item
|
|
106
104
|
key={result.id}
|
|
@@ -111,7 +109,9 @@ export function Search({ className }: SearchProps) {
|
|
|
111
109
|
<div className={styles.itemContent}>
|
|
112
110
|
{getResultIcon(result)}
|
|
113
111
|
<Text className={styles.pageText}>
|
|
114
|
-
<HighlightedText
|
|
112
|
+
<HighlightedText
|
|
113
|
+
html={stripMethod(result.content)}
|
|
114
|
+
/>
|
|
115
115
|
</Text>
|
|
116
116
|
</div>
|
|
117
117
|
</Command.Item>
|
|
@@ -129,10 +129,12 @@ export function Search({ className }: SearchProps) {
|
|
|
129
129
|
<div className={styles.itemContent}>
|
|
130
130
|
{getResultIcon(result)}
|
|
131
131
|
<div className={styles.resultText}>
|
|
132
|
-
{result.type ===
|
|
132
|
+
{result.type === 'heading' ? (
|
|
133
133
|
<>
|
|
134
134
|
<Text className={styles.headingText}>
|
|
135
|
-
<HighlightedText
|
|
135
|
+
<HighlightedText
|
|
136
|
+
html={stripMethod(result.content)}
|
|
137
|
+
/>
|
|
136
138
|
</Text>
|
|
137
139
|
<Text className={styles.separator}>-</Text>
|
|
138
140
|
<Text className={styles.pageText}>
|
|
@@ -141,7 +143,9 @@ export function Search({ className }: SearchProps) {
|
|
|
141
143
|
</>
|
|
142
144
|
) : (
|
|
143
145
|
<Text className={styles.pageText}>
|
|
144
|
-
<HighlightedText
|
|
146
|
+
<HighlightedText
|
|
147
|
+
html={stripMethod(result.content)}
|
|
148
|
+
/>
|
|
145
149
|
</Text>
|
|
146
150
|
)}
|
|
147
151
|
</div>
|
|
@@ -158,49 +162,57 @@ export function Search({ className }: SearchProps) {
|
|
|
158
162
|
|
|
159
163
|
function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
|
|
160
164
|
const seen = new Set<string>();
|
|
161
|
-
return results.filter(
|
|
162
|
-
const base = r.url.split(
|
|
165
|
+
return results.filter(r => {
|
|
166
|
+
const base = r.url.split('#')[0];
|
|
163
167
|
if (seen.has(base)) return false;
|
|
164
168
|
seen.add(base);
|
|
165
169
|
return true;
|
|
166
170
|
});
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
const API_METHODS = new Set([
|
|
173
|
+
const API_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
170
174
|
|
|
171
175
|
function extractMethod(content: string): string | null {
|
|
172
|
-
const first = content.split(
|
|
176
|
+
const first = content.split(' ')[0];
|
|
173
177
|
return API_METHODS.has(first) ? first : null;
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function stripMethod(content: string): string {
|
|
177
|
-
const first = content.split(
|
|
181
|
+
const first = content.split(' ')[0];
|
|
178
182
|
return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
function HighlightedText({
|
|
182
|
-
|
|
185
|
+
function HighlightedText({
|
|
186
|
+
html,
|
|
187
|
+
className
|
|
188
|
+
}: {
|
|
189
|
+
html: string;
|
|
190
|
+
className?: string;
|
|
191
|
+
}) {
|
|
192
|
+
return (
|
|
193
|
+
<span className={className} dangerouslySetInnerHTML={{ __html: html }} />
|
|
194
|
+
);
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
function getResultIcon(result: SortedResult): React.ReactNode {
|
|
186
|
-
if (!result.url.startsWith(
|
|
187
|
-
return result.type ===
|
|
198
|
+
if (!result.url.startsWith('/apis/')) {
|
|
199
|
+
return result.type === 'page' ? (
|
|
188
200
|
<DocumentIcon className={styles.icon} />
|
|
189
201
|
) : (
|
|
190
202
|
<HashtagIcon className={styles.icon} />
|
|
191
203
|
);
|
|
192
204
|
}
|
|
193
205
|
const method = extractMethod(result.content);
|
|
194
|
-
return method ? <MethodBadge method={method} size=
|
|
206
|
+
return method ? <MethodBadge method={method} size='micro' /> : null;
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
function getPageTitle(url: string): string {
|
|
198
|
-
const path = url.split(
|
|
199
|
-
const segments = path.split(
|
|
210
|
+
const path = url.split('#')[0];
|
|
211
|
+
const segments = path.split('/').filter(Boolean);
|
|
200
212
|
const lastSegment = segments[segments.length - 1];
|
|
201
|
-
if (!lastSegment) return
|
|
213
|
+
if (!lastSegment) return 'Home';
|
|
202
214
|
return lastSegment
|
|
203
|
-
.split(
|
|
204
|
-
.map(
|
|
205
|
-
.join(
|
|
215
|
+
.split('-')
|
|
216
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
217
|
+
.join(' ');
|
|
206
218
|
}
|