@raystack/chronicle 0.1.0-canary.111b55a → 0.1.0-canary.1e5fdae
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 +212 -833
- package/package.json +13 -9
- package/src/cli/commands/build.ts +30 -70
- package/src/cli/commands/dev.ts +24 -13
- package/src/cli/commands/init.ts +38 -123
- package/src/cli/commands/serve.ts +35 -50
- package/src/cli/commands/start.ts +20 -16
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +14 -16
- package/src/components/mdx/details.module.css +0 -2
- package/src/components/mdx/image.tsx +5 -20
- package/src/components/mdx/index.tsx +18 -4
- package/src/components/mdx/link.tsx +24 -20
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/footer.tsx +2 -3
- package/src/components/ui/search.tsx +116 -71
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +31 -29
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/head.tsx +26 -22
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +74 -58
- package/src/lib/source.ts +136 -114
- package/src/pages/ApiLayout.tsx +22 -18
- package/src/pages/ApiPage.tsx +32 -27
- package/src/pages/DocsLayout.tsx +7 -7
- package/src/pages/DocsPage.tsx +11 -11
- package/src/pages/NotFound.tsx +11 -4
- package/src/server/App.tsx +35 -27
- 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 +170 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +78 -68
- package/src/server/entry-client.tsx +67 -55
- package/src/server/entry-server.tsx +100 -35
- package/src/server/routes/llms.txt.ts +61 -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 +87 -47
- package/src/themes/default/Layout.tsx +78 -47
- package/src/themes/default/Page.module.css +0 -16
- 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 +63 -43
- 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 -62
- 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/content.ts +5 -21
- package/src/types/globals.d.ts +3 -0
- package/src/types/theme.ts +4 -3
- package/src/cli/__tests__/config.test.ts +0 -25
- package/src/cli/__tests__/scaffold.test.ts +0 -10
- package/src/pages/__tests__/head.test.tsx +0 -57
- package/src/server/__tests__/entry-server.test.tsx +0 -35
- package/src/server/__tests__/handlers.test.ts +0 -77
- package/src/server/__tests__/og.test.ts +0 -23
- package/src/server/__tests__/router.test.ts +0 -72
- package/src/server/__tests__/vite-config.test.ts +0 -25
- package/src/server/adapters/vercel.ts +0 -133
- package/src/server/dev.ts +0 -156
- package/src/server/entry-prod.ts +0 -97
- package/src/server/entry-vercel.ts +0 -28
- package/src/server/handlers/apis-proxy.ts +0 -52
- package/src/server/handlers/health.ts +0 -3
- package/src/server/handlers/llms.ts +0 -58
- package/src/server/handlers/og.ts +0 -87
- package/src/server/handlers/robots.ts +0 -11
- package/src/server/handlers/search.ts +0 -172
- package/src/server/handlers/sitemap.ts +0 -39
- package/src/server/handlers/specs.ts +0 -9
- package/src/server/index.html +0 -12
- package/src/server/prod.ts +0 -18
- package/src/server/request-handler.ts +0 -63
- package/src/server/router.ts +0 -42
- package/src/themes/default/font.ts +0 -4
package/src/cli/utils/resolve.ts
CHANGED
|
@@ -1,6 +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
5
|
// After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
|
|
6
|
-
export const PACKAGE_ROOT = path.resolve(
|
|
6
|
+
export const PACKAGE_ROOT = path.resolve(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
'..',
|
|
9
|
+
'..'
|
|
10
|
+
);
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { PACKAGE_ROOT } from './resolve'
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT } from './resolve';
|
|
4
4
|
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export async function linkContent(contentDir: string): Promise<void> {
|
|
6
|
+
const linkPath = path.join(PACKAGE_ROOT, '.content');
|
|
7
|
+
const target = path.resolve(contentDir);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const existing = await fs.readlink(linkPath);
|
|
11
|
+
if (existing === target) return;
|
|
12
|
+
await fs.unlink(linkPath);
|
|
13
|
+
} catch {
|
|
14
|
+
// link doesn't exist
|
|
8
15
|
}
|
|
9
|
-
const cwd = process.cwd()
|
|
10
|
-
if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
|
|
11
|
-
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
12
|
-
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
|
13
|
-
return 'npm'
|
|
14
|
-
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
|
|
18
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
19
|
-
return pkg.version
|
|
17
|
+
await fs.symlink(target, linkPath);
|
|
20
18
|
}
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type ImageProps = ComponentProps<'img'>;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
src
|
|
7
|
-
width?: number | string
|
|
8
|
-
height?: number | string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function Image({ src, alt, width, height, ...props }: ImageProps) {
|
|
12
|
-
if (!src || typeof src !== 'string') return null
|
|
5
|
+
export function Image({ src, alt, ...props }: ImageProps) {
|
|
6
|
+
if (!src) return null;
|
|
13
7
|
|
|
14
|
-
return
|
|
15
|
-
<img
|
|
16
|
-
src={src}
|
|
17
|
-
alt={alt ?? ''}
|
|
18
|
-
width={width}
|
|
19
|
-
height={height}
|
|
20
|
-
loading="lazy"
|
|
21
|
-
{...props}
|
|
22
|
-
/>
|
|
23
|
-
)
|
|
8
|
+
return <img src={src} alt={alt ?? ''} loading='lazy' {...props} />;
|
|
24
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MDXComponents } from 'mdx/types'
|
|
2
2
|
import { Image } from './image'
|
|
3
|
-
import {
|
|
3
|
+
import { Link } from './link'
|
|
4
4
|
import { MdxTable, MdxThead, MdxTbody, MdxTr, MdxTh, MdxTd } from './table'
|
|
5
5
|
import { MdxPre, MdxCode } from './code'
|
|
6
6
|
import { MdxDetails, MdxSummary } from './details'
|
|
@@ -8,11 +8,25 @@ 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,
|
|
14
28
|
img: Image,
|
|
15
|
-
a:
|
|
29
|
+
a: Link,
|
|
16
30
|
table: MdxTable,
|
|
17
31
|
thead: MdxThead,
|
|
18
32
|
tbody: MdxTbody,
|
|
@@ -27,9 +41,9 @@ 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
|
|
|
34
48
|
export { Image } from './image'
|
|
35
|
-
export {
|
|
49
|
+
export { Link } from './link'
|
|
@@ -1,37 +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 type { ComponentProps } from 'react'
|
|
5
|
+
type LinkProps = ComponentProps<'a'>;
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function MdxLink({ href, children, ...props }: LinkProps) {
|
|
7
|
+
export function Link({ href, children, ...props }: LinkProps) {
|
|
9
8
|
if (!href) {
|
|
10
|
-
return <span {...props}>{children}</span
|
|
9
|
+
return <span {...props}>{children}</span>;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
const isExternal = href.startsWith('http://') || href.startsWith('https://')
|
|
14
|
-
const isAnchor = href.startsWith('#')
|
|
12
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
13
|
+
const isAnchor = href.startsWith('#');
|
|
15
14
|
|
|
16
|
-
if (
|
|
15
|
+
if (isAnchor) {
|
|
17
16
|
return (
|
|
18
|
-
<
|
|
17
|
+
<ApsaraLink href={href} {...props}>
|
|
19
18
|
{children}
|
|
20
|
-
</
|
|
21
|
-
)
|
|
19
|
+
</ApsaraLink>
|
|
20
|
+
);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
if (
|
|
23
|
+
if (isExternal) {
|
|
25
24
|
return (
|
|
26
|
-
<
|
|
25
|
+
<ApsaraLink
|
|
26
|
+
href={href}
|
|
27
|
+
target='_blank'
|
|
28
|
+
rel='noopener noreferrer'
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
27
31
|
{children}
|
|
28
|
-
</
|
|
29
|
-
)
|
|
32
|
+
</ApsaraLink>
|
|
33
|
+
);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return (
|
|
33
|
-
<
|
|
37
|
+
<RouterLink to={href} className={props.className}>
|
|
34
38
|
{children}
|
|
35
|
-
</
|
|
36
|
-
)
|
|
39
|
+
</RouterLink>
|
|
40
|
+
);
|
|
37
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,5 +1,4 @@
|
|
|
1
|
-
import { Link } from "
|
|
2
|
-
import { Flex, Text } from "@raystack/apsara";
|
|
1
|
+
import { Flex, Link, Text } from "@raystack/apsara";
|
|
3
2
|
import type { FooterConfig } from "@/types";
|
|
4
3
|
import styles from "./footer.module.css";
|
|
5
4
|
|
|
@@ -19,7 +18,7 @@ export function Footer({ config }: FooterProps) {
|
|
|
19
18
|
{config?.links && config.links.length > 0 && (
|
|
20
19
|
<Flex gap="medium" className={styles.links}>
|
|
21
20
|
{config.links.map((link) => (
|
|
22
|
-
<Link key={link.href}
|
|
21
|
+
<Link key={link.href} href={link.href} className={styles.link}>
|
|
23
22
|
{link.label}
|
|
24
23
|
</Link>
|
|
25
24
|
))}
|
|
@@ -1,50 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { MethodBadge } from
|
|
9
|
-
import styles from
|
|
10
|
-
|
|
11
|
-
interface SearchResult {
|
|
12
|
-
id: string;
|
|
13
|
-
url: string;
|
|
14
|
-
type: "page" | "api";
|
|
15
|
-
content: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function useSearch(query: string) {
|
|
19
|
-
const [results, setResults] = useState<SearchResult[]>([]);
|
|
20
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
-
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
clearTimeout(timerRef.current);
|
|
25
|
-
timerRef.current = setTimeout(async () => {
|
|
26
|
-
setIsLoading(true);
|
|
27
|
-
try {
|
|
28
|
-
const params = new URLSearchParams();
|
|
29
|
-
if (query) params.set("query", query);
|
|
30
|
-
const res = await fetch(`/api/search?${params}`);
|
|
31
|
-
setResults(await res.json());
|
|
32
|
-
} catch {
|
|
33
|
-
setResults([]);
|
|
34
|
-
}
|
|
35
|
-
setIsLoading(false);
|
|
36
|
-
}, 100);
|
|
37
|
-
return () => clearTimeout(timerRef.current);
|
|
38
|
-
}, [query]);
|
|
39
|
-
|
|
40
|
-
return { results, isLoading };
|
|
41
|
-
}
|
|
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';
|
|
42
10
|
|
|
43
11
|
function SearchShortcutKey({ className }: { className?: string }) {
|
|
44
|
-
const [key, setKey] = useState(
|
|
12
|
+
const [key, setKey] = useState('⌘');
|
|
45
13
|
|
|
46
14
|
useEffect(() => {
|
|
47
|
-
|
|
15
|
+
const isMac = navigator.platform?.toUpperCase().includes('MAC');
|
|
16
|
+
setKey(isMac ? '⌘' : 'Ctrl');
|
|
48
17
|
}, []);
|
|
49
18
|
|
|
50
19
|
return (
|
|
@@ -61,35 +30,44 @@ interface SearchProps {
|
|
|
61
30
|
export function Search({ className }: SearchProps) {
|
|
62
31
|
const [open, setOpen] = useState(false);
|
|
63
32
|
const navigate = useNavigate();
|
|
64
|
-
|
|
65
|
-
const {
|
|
33
|
+
|
|
34
|
+
const { search, setSearch, query } = useDocsSearch({
|
|
35
|
+
type: 'fetch',
|
|
36
|
+
api: '/api/search',
|
|
37
|
+
delayMs: 100,
|
|
38
|
+
allowEmpty: true
|
|
39
|
+
});
|
|
66
40
|
|
|
67
41
|
const onSelect = useCallback(
|
|
68
42
|
(url: string) => {
|
|
69
43
|
setOpen(false);
|
|
70
44
|
navigate(url);
|
|
71
45
|
},
|
|
72
|
-
[navigate]
|
|
46
|
+
[navigate]
|
|
73
47
|
);
|
|
74
48
|
|
|
75
49
|
useEffect(() => {
|
|
76
50
|
const down = (e: KeyboardEvent) => {
|
|
77
|
-
if (e.key ===
|
|
51
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
78
52
|
e.preventDefault();
|
|
79
|
-
setOpen(
|
|
53
|
+
setOpen(open => !open);
|
|
80
54
|
}
|
|
81
55
|
};
|
|
82
56
|
|
|
83
|
-
document.addEventListener(
|
|
84
|
-
return () => document.removeEventListener(
|
|
57
|
+
document.addEventListener('keydown', down);
|
|
58
|
+
return () => document.removeEventListener('keydown', down);
|
|
85
59
|
}, []);
|
|
86
60
|
|
|
61
|
+
const results = deduplicateByUrl(
|
|
62
|
+
query.data === 'empty' ? [] : (query.data ?? [])
|
|
63
|
+
);
|
|
64
|
+
|
|
87
65
|
return (
|
|
88
66
|
<>
|
|
89
67
|
<Button
|
|
90
|
-
variant=
|
|
91
|
-
color=
|
|
92
|
-
size=
|
|
68
|
+
variant='outline'
|
|
69
|
+
color='neutral'
|
|
70
|
+
size='small'
|
|
93
71
|
onClick={() => setOpen(true)}
|
|
94
72
|
className={cx(styles.trigger, className)}
|
|
95
73
|
trailingIcon={<SearchShortcutKey className={styles.kbd} />}
|
|
@@ -104,24 +82,24 @@ export function Search({ className }: SearchProps) {
|
|
|
104
82
|
</Dialog.Title>
|
|
105
83
|
<Command loop>
|
|
106
84
|
<Command.Input
|
|
107
|
-
placeholder=
|
|
85
|
+
placeholder='Search'
|
|
108
86
|
value={search}
|
|
109
87
|
onValueChange={setSearch}
|
|
110
88
|
className={styles.input}
|
|
111
89
|
/>
|
|
112
90
|
|
|
113
91
|
<Command.List className={styles.list}>
|
|
114
|
-
{isLoading && <Command.Empty>Loading...</Command.Empty>}
|
|
115
|
-
{!isLoading &&
|
|
92
|
+
{query.isLoading && <Command.Empty>Loading...</Command.Empty>}
|
|
93
|
+
{!query.isLoading &&
|
|
116
94
|
search.length > 0 &&
|
|
117
95
|
results.length === 0 && (
|
|
118
96
|
<Command.Empty>No results found.</Command.Empty>
|
|
119
97
|
)}
|
|
120
|
-
{!isLoading &&
|
|
98
|
+
{!query.isLoading &&
|
|
121
99
|
search.length === 0 &&
|
|
122
100
|
results.length > 0 && (
|
|
123
|
-
<Command.Group heading=
|
|
124
|
-
{results.slice(0, 8).map((result) => (
|
|
101
|
+
<Command.Group heading='Suggestions'>
|
|
102
|
+
{results.slice(0, 8).map((result: SortedResult) => (
|
|
125
103
|
<Command.Item
|
|
126
104
|
key={result.id}
|
|
127
105
|
value={result.id}
|
|
@@ -131,7 +109,9 @@ export function Search({ className }: SearchProps) {
|
|
|
131
109
|
<div className={styles.itemContent}>
|
|
132
110
|
{getResultIcon(result)}
|
|
133
111
|
<Text className={styles.pageText}>
|
|
134
|
-
|
|
112
|
+
<HighlightedText
|
|
113
|
+
html={stripMethod(result.content)}
|
|
114
|
+
/>
|
|
135
115
|
</Text>
|
|
136
116
|
</div>
|
|
137
117
|
</Command.Item>
|
|
@@ -139,7 +119,7 @@ export function Search({ className }: SearchProps) {
|
|
|
139
119
|
</Command.Group>
|
|
140
120
|
)}
|
|
141
121
|
{search.length > 0 &&
|
|
142
|
-
results.map((result) => (
|
|
122
|
+
results.map((result: SortedResult) => (
|
|
143
123
|
<Command.Item
|
|
144
124
|
key={result.id}
|
|
145
125
|
value={result.id}
|
|
@@ -148,9 +128,27 @@ export function Search({ className }: SearchProps) {
|
|
|
148
128
|
>
|
|
149
129
|
<div className={styles.itemContent}>
|
|
150
130
|
{getResultIcon(result)}
|
|
151
|
-
<
|
|
152
|
-
{result.
|
|
153
|
-
|
|
131
|
+
<div className={styles.resultText}>
|
|
132
|
+
{result.type === 'heading' ? (
|
|
133
|
+
<>
|
|
134
|
+
<Text className={styles.headingText}>
|
|
135
|
+
<HighlightedText
|
|
136
|
+
html={stripMethod(result.content)}
|
|
137
|
+
/>
|
|
138
|
+
</Text>
|
|
139
|
+
<Text className={styles.separator}>-</Text>
|
|
140
|
+
<Text className={styles.pageText}>
|
|
141
|
+
{getPageTitle(result.url)}
|
|
142
|
+
</Text>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<Text className={styles.pageText}>
|
|
146
|
+
<HighlightedText
|
|
147
|
+
html={stripMethod(result.content)}
|
|
148
|
+
/>
|
|
149
|
+
</Text>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
154
152
|
</div>
|
|
155
153
|
</Command.Item>
|
|
156
154
|
))}
|
|
@@ -162,12 +160,59 @@ export function Search({ className }: SearchProps) {
|
|
|
162
160
|
);
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
|
|
164
|
+
const seen = new Set<string>();
|
|
165
|
+
return results.filter(r => {
|
|
166
|
+
const base = r.url.split('#')[0];
|
|
167
|
+
if (seen.has(base)) return false;
|
|
168
|
+
seen.add(base);
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const API_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
174
|
+
|
|
175
|
+
function extractMethod(content: string): string | null {
|
|
176
|
+
const first = content.split(' ')[0];
|
|
177
|
+
return API_METHODS.has(first) ? first : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stripMethod(content: string): string {
|
|
181
|
+
const first = content.split(' ')[0];
|
|
182
|
+
return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
|
|
183
|
+
}
|
|
184
|
+
|
|
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
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getResultIcon(result: SortedResult): React.ReactNode {
|
|
198
|
+
if (!result.url.startsWith('/apis/')) {
|
|
199
|
+
return result.type === 'page' ? (
|
|
200
|
+
<DocumentIcon className={styles.icon} />
|
|
201
|
+
) : (
|
|
202
|
+
<HashtagIcon className={styles.icon} />
|
|
203
|
+
);
|
|
171
204
|
}
|
|
172
|
-
|
|
205
|
+
const method = extractMethod(result.content);
|
|
206
|
+
return method ? <MethodBadge method={method} size='micro' /> : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getPageTitle(url: string): string {
|
|
210
|
+
const path = url.split('#')[0];
|
|
211
|
+
const segments = path.split('/').filter(Boolean);
|
|
212
|
+
const lastSegment = segments[segments.length - 1];
|
|
213
|
+
if (!lastSegment) return 'Home';
|
|
214
|
+
return lastSegment
|
|
215
|
+
.split('-')
|
|
216
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
217
|
+
.join(' ');
|
|
173
218
|
}
|
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
|
}
|