@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.
Files changed (80) hide show
  1. package/dist/cli/index.js +268 -9902
  2. package/package.json +20 -12
  3. package/src/cli/commands/build.ts +27 -25
  4. package/src/cli/commands/dev.ts +24 -25
  5. package/src/cli/commands/init.ts +38 -132
  6. package/src/cli/commands/serve.ts +36 -49
  7. package/src/cli/commands/start.ts +20 -25
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +25 -26
  10. package/src/cli/utils/index.ts +3 -3
  11. package/src/cli/utils/resolve.ts +9 -3
  12. package/src/cli/utils/scaffold.ts +11 -124
  13. package/src/components/mdx/code.tsx +10 -1
  14. package/src/components/mdx/details.module.css +1 -26
  15. package/src/components/mdx/details.tsx +2 -3
  16. package/src/components/mdx/image.tsx +5 -34
  17. package/src/components/mdx/index.tsx +15 -1
  18. package/src/components/mdx/link.tsx +18 -15
  19. package/src/components/ui/breadcrumbs.tsx +8 -42
  20. package/src/components/ui/search.tsx +63 -51
  21. package/src/lib/api-routes.ts +6 -8
  22. package/src/lib/config.ts +12 -35
  23. package/src/lib/head.tsx +49 -0
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +111 -0
  26. package/src/lib/source.ts +134 -63
  27. package/src/pages/ApiLayout.tsx +33 -0
  28. package/src/pages/ApiPage.tsx +73 -0
  29. package/src/pages/DocsLayout.tsx +18 -0
  30. package/src/pages/DocsPage.tsx +43 -0
  31. package/src/pages/NotFound.tsx +17 -0
  32. package/src/server/App.tsx +67 -0
  33. package/src/server/api/apis-proxy.ts +69 -0
  34. package/src/server/api/health.ts +5 -0
  35. package/src/server/api/page/[...slug].ts +17 -0
  36. package/src/server/api/search.ts +118 -0
  37. package/src/server/api/specs.ts +9 -0
  38. package/src/server/build-search-index.ts +117 -0
  39. package/src/server/entry-client.tsx +86 -0
  40. package/src/server/entry-server.tsx +100 -0
  41. package/src/server/routes/llms.txt.ts +21 -0
  42. package/src/server/routes/og.tsx +75 -0
  43. package/src/server/routes/robots.txt.ts +11 -0
  44. package/src/server/routes/sitemap.xml.ts +40 -0
  45. package/src/server/utils/safe-path.ts +17 -0
  46. package/src/server/vite-config.ts +126 -0
  47. package/src/themes/default/Layout.tsx +78 -48
  48. package/src/themes/default/Page.module.css +44 -0
  49. package/src/themes/default/Page.tsx +9 -11
  50. package/src/themes/default/Toc.tsx +25 -39
  51. package/src/themes/default/index.ts +7 -9
  52. package/src/themes/paper/ChapterNav.tsx +64 -45
  53. package/src/themes/paper/Layout.module.css +1 -1
  54. package/src/themes/paper/Layout.tsx +24 -12
  55. package/src/themes/paper/Page.module.css +16 -4
  56. package/src/themes/paper/Page.tsx +56 -63
  57. package/src/themes/paper/ReadingProgress.tsx +160 -139
  58. package/src/themes/paper/index.ts +5 -5
  59. package/src/themes/registry.ts +7 -7
  60. package/src/types/config.ts +11 -0
  61. package/src/types/content.ts +6 -21
  62. package/src/types/globals.d.ts +4 -0
  63. package/src/types/theme.ts +4 -3
  64. package/tsconfig.json +2 -3
  65. package/next.config.mjs +0 -10
  66. package/source.config.ts +0 -50
  67. package/src/app/[[...slug]]/layout.tsx +0 -15
  68. package/src/app/[[...slug]]/page.tsx +0 -57
  69. package/src/app/api/apis-proxy/route.ts +0 -59
  70. package/src/app/api/health/route.ts +0 -3
  71. package/src/app/api/search/route.ts +0 -90
  72. package/src/app/apis/[[...slug]]/layout.tsx +0 -26
  73. package/src/app/apis/[[...slug]]/page.tsx +0 -57
  74. package/src/app/layout.tsx +0 -26
  75. package/src/app/llms-full.txt/route.ts +0 -18
  76. package/src/app/llms.txt/route.ts +0 -15
  77. package/src/app/providers.tsx +0 -8
  78. package/src/cli/utils/process.ts +0 -7
  79. package/src/themes/default/font.ts +0 -6
  80. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -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
- export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
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 { execSync } from 'child_process'
2
- import { createRequire } from 'module'
3
- import fs from 'fs'
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
- const COPY_FILES = ['src', 'source.config.ts', 'tsconfig.json']
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.lstatSync(targetPath)
25
- fs.rmSync(targetPath, { recursive: true, force: true })
10
+ const existing = await fs.readlink(linkPath);
11
+ if (existing === target) return;
12
+ await fs.unlink(linkPath);
26
13
  } catch {
27
- // nothing exists, proceed
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
- return scaffoldPath
17
+ await fs.symlink(target, linkPath);
131
18
  }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import type { ComponentProps } from 'react'
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
- .summary {
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={`${styles.details} ${className ?? ''}`} {...props}>
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={`${styles.summary} ${className ?? ''}`} {...props}>
13
+ <summary className={className} {...props}>
15
14
  {children}
16
15
  </summary>
17
16
  )
@@ -1,38 +1,9 @@
1
- 'use client'
1
+ import type { ComponentProps } from 'react';
2
2
 
3
- import NextImage from 'next/image'
4
- import type { ComponentProps } from 'react'
3
+ type ImageProps = ComponentProps<'img'>;
5
4
 
6
- type ImageProps = Omit<ComponentProps<'img'>, 'src'> & {
7
- src?: string
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
- 'use client'
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
- import NextLink from 'next/link'
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 href={href} target="_blank" rel="noopener noreferrer" {...props}>
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
- <NextLink href={href} className={props.className}>
37
+ <RouterLink to={href} className={props.className}>
35
38
  {children}
36
- </NextLink>
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 type { PageTree, PageTreeItem } from '@/types'
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: PageTree
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 items: { label: string; href: string }[] = []
13
+ const url = slug.length === 0 ? '/' : `/${slug.join('/')}`
14
+ const items = getBreadcrumbItems(url, tree, { includePage: true })
40
15
 
41
- for (let i = 0; i < slug.length; i++) {
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.href}
24
+ href={item.url}
59
25
  current={index === items.length - 1}
60
26
  >
61
- {item.label}
27
+ {item.name}
62
28
  </Breadcrumb.Item>
63
29
  )
64
30
  if (index === 0) return [breadcrumbItem]
@@ -1,21 +1,19 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback } from "react";
4
- import { useRouter } from "next/navigation";
5
- import { Button, Command, Dialog, Text } from "@raystack/apsara";
6
- import { cx } from "class-variance-authority";
7
- import { useDocsSearch } from "fumadocs-core/search/client";
8
- import type { SortedResult } from "fumadocs-core/search";
9
- import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline";
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
- setKey(isMacOs ? "⌘" : "Ctrl");
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 router = useRouter();
32
+ const navigate = useNavigate();
35
33
 
36
34
  const { search, setSearch, query } = useDocsSearch({
37
- type: "fetch",
38
- api: "/api/search",
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
- router.push(url);
44
+ navigate(url);
47
45
  },
48
- [router],
46
+ [navigate]
49
47
  );
50
48
 
51
49
  useEffect(() => {
52
50
  const down = (e: KeyboardEvent) => {
53
- if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
51
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
54
52
  e.preventDefault();
55
- setOpen((open) => !open);
53
+ setOpen(open => !open);
56
54
  }
57
55
  };
58
56
 
59
- document.addEventListener("keydown", down);
60
- return () => document.removeEventListener("keydown", down);
57
+ document.addEventListener('keydown', down);
58
+ return () => document.removeEventListener('keydown', down);
61
59
  }, []);
62
60
 
63
61
  const results = deduplicateByUrl(
64
- query.data === "empty" ? [] : (query.data ?? []),
62
+ query.data === 'empty' ? [] : (query.data ?? [])
65
63
  );
66
64
 
67
65
  return (
68
66
  <>
69
67
  <Button
70
- variant="outline"
71
- color="neutral"
72
- size="small"
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="Search"
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="Suggestions">
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 html={stripMethod(result.content)} />
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 === "heading" ? (
132
+ {result.type === 'heading' ? (
133
133
  <>
134
134
  <Text className={styles.headingText}>
135
- <HighlightedText html={stripMethod(result.content)} />
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 html={stripMethod(result.content)} />
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((r) => {
162
- const base = r.url.split("#")[0];
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(["GET", "POST", "PUT", "DELETE", "PATCH"]);
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(" ")[0];
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(" ")[0];
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({ html, className }: { html: string; className?: string }) {
182
- return <span className={className} dangerouslySetInnerHTML={{ __html: html }} />;
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("/apis/")) {
187
- return result.type === "page" ? (
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="micro" /> : null;
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("#")[0];
199
- const segments = path.split("/").filter(Boolean);
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 "Home";
213
+ if (!lastSegment) return 'Home';
202
214
  return lastSegment
203
- .split("-")
204
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
205
- .join(" ");
215
+ .split('-')
216
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
217
+ .join(' ');
206
218
  }