@raystack/chronicle 0.1.0-canary.5a2be79

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 (107) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +9980 -0
  3. package/next.config.mjs +10 -0
  4. package/package.json +63 -0
  5. package/source.config.ts +50 -0
  6. package/src/app/[[...slug]]/layout.tsx +15 -0
  7. package/src/app/[[...slug]]/page.tsx +57 -0
  8. package/src/app/api/apis-proxy/route.ts +59 -0
  9. package/src/app/api/health/route.ts +3 -0
  10. package/src/app/api/search/route.ts +90 -0
  11. package/src/app/apis/[[...slug]]/layout.module.css +22 -0
  12. package/src/app/apis/[[...slug]]/layout.tsx +26 -0
  13. package/src/app/apis/[[...slug]]/page.tsx +57 -0
  14. package/src/app/layout.tsx +26 -0
  15. package/src/app/llms-full.txt/route.ts +18 -0
  16. package/src/app/llms.txt/route.ts +15 -0
  17. package/src/app/providers.tsx +8 -0
  18. package/src/cli/commands/build.ts +32 -0
  19. package/src/cli/commands/dev.ts +33 -0
  20. package/src/cli/commands/init.ts +155 -0
  21. package/src/cli/commands/serve.ts +53 -0
  22. package/src/cli/commands/start.ts +33 -0
  23. package/src/cli/index.ts +21 -0
  24. package/src/cli/utils/config.ts +43 -0
  25. package/src/cli/utils/index.ts +3 -0
  26. package/src/cli/utils/process.ts +7 -0
  27. package/src/cli/utils/resolve.ts +4 -0
  28. package/src/cli/utils/scaffold.ts +131 -0
  29. package/src/components/api/code-snippets.module.css +7 -0
  30. package/src/components/api/code-snippets.tsx +76 -0
  31. package/src/components/api/endpoint-page.module.css +58 -0
  32. package/src/components/api/endpoint-page.tsx +283 -0
  33. package/src/components/api/field-row.module.css +126 -0
  34. package/src/components/api/field-row.tsx +204 -0
  35. package/src/components/api/field-section.module.css +24 -0
  36. package/src/components/api/field-section.tsx +100 -0
  37. package/src/components/api/index.ts +8 -0
  38. package/src/components/api/json-editor.module.css +9 -0
  39. package/src/components/api/json-editor.tsx +61 -0
  40. package/src/components/api/key-value-editor.module.css +13 -0
  41. package/src/components/api/key-value-editor.tsx +62 -0
  42. package/src/components/api/method-badge.module.css +4 -0
  43. package/src/components/api/method-badge.tsx +29 -0
  44. package/src/components/api/response-panel.module.css +8 -0
  45. package/src/components/api/response-panel.tsx +44 -0
  46. package/src/components/common/breadcrumb.tsx +3 -0
  47. package/src/components/common/button.tsx +3 -0
  48. package/src/components/common/callout.module.css +7 -0
  49. package/src/components/common/callout.tsx +27 -0
  50. package/src/components/common/code-block.tsx +3 -0
  51. package/src/components/common/dialog.tsx +3 -0
  52. package/src/components/common/index.ts +10 -0
  53. package/src/components/common/input-field.tsx +3 -0
  54. package/src/components/common/sidebar.tsx +3 -0
  55. package/src/components/common/switch.tsx +3 -0
  56. package/src/components/common/table.tsx +3 -0
  57. package/src/components/common/tabs.tsx +3 -0
  58. package/src/components/mdx/code.module.css +42 -0
  59. package/src/components/mdx/code.tsx +27 -0
  60. package/src/components/mdx/details.module.css +37 -0
  61. package/src/components/mdx/details.tsx +18 -0
  62. package/src/components/mdx/image.tsx +38 -0
  63. package/src/components/mdx/index.tsx +35 -0
  64. package/src/components/mdx/link.tsx +38 -0
  65. package/src/components/mdx/mermaid.module.css +9 -0
  66. package/src/components/mdx/mermaid.tsx +37 -0
  67. package/src/components/mdx/paragraph.module.css +8 -0
  68. package/src/components/mdx/paragraph.tsx +19 -0
  69. package/src/components/mdx/table.tsx +40 -0
  70. package/src/components/ui/breadcrumbs.tsx +72 -0
  71. package/src/components/ui/client-theme-switcher.tsx +18 -0
  72. package/src/components/ui/footer.module.css +27 -0
  73. package/src/components/ui/footer.tsx +30 -0
  74. package/src/components/ui/search.module.css +104 -0
  75. package/src/components/ui/search.tsx +202 -0
  76. package/src/lib/api-routes.ts +120 -0
  77. package/src/lib/config.ts +55 -0
  78. package/src/lib/get-llm-text.ts +10 -0
  79. package/src/lib/index.ts +2 -0
  80. package/src/lib/openapi.ts +188 -0
  81. package/src/lib/remark-unused-directives.ts +30 -0
  82. package/src/lib/schema.ts +99 -0
  83. package/src/lib/snippet-generators.ts +87 -0
  84. package/src/lib/source.ts +67 -0
  85. package/src/themes/default/Layout.module.css +81 -0
  86. package/src/themes/default/Layout.tsx +133 -0
  87. package/src/themes/default/Page.module.css +46 -0
  88. package/src/themes/default/Page.tsx +21 -0
  89. package/src/themes/default/Toc.module.css +48 -0
  90. package/src/themes/default/Toc.tsx +66 -0
  91. package/src/themes/default/font.ts +6 -0
  92. package/src/themes/default/index.ts +13 -0
  93. package/src/themes/paper/ChapterNav.module.css +71 -0
  94. package/src/themes/paper/ChapterNav.tsx +96 -0
  95. package/src/themes/paper/Layout.module.css +33 -0
  96. package/src/themes/paper/Layout.tsx +25 -0
  97. package/src/themes/paper/Page.module.css +174 -0
  98. package/src/themes/paper/Page.tsx +107 -0
  99. package/src/themes/paper/ReadingProgress.module.css +132 -0
  100. package/src/themes/paper/ReadingProgress.tsx +294 -0
  101. package/src/themes/paper/index.ts +8 -0
  102. package/src/themes/registry.ts +14 -0
  103. package/src/types/config.ts +69 -0
  104. package/src/types/content.ts +35 -0
  105. package/src/types/index.ts +3 -0
  106. package/src/types/theme.ts +22 -0
  107. package/tsconfig.json +30 -0
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
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'>
8
+
9
+ export function Link({ href, children, ...props }: LinkProps) {
10
+ if (!href) {
11
+ return <span {...props}>{children}</span>
12
+ }
13
+
14
+ const isExternal = href.startsWith('http://') || href.startsWith('https://')
15
+ const isAnchor = href.startsWith('#')
16
+
17
+ if (isAnchor) {
18
+ return (
19
+ <ApsaraLink href={href} {...props}>
20
+ {children}
21
+ </ApsaraLink>
22
+ )
23
+ }
24
+
25
+ if (isExternal) {
26
+ return (
27
+ <ApsaraLink href={href} target="_blank" rel="noopener noreferrer" {...props}>
28
+ {children}
29
+ </ApsaraLink>
30
+ )
31
+ }
32
+
33
+ return (
34
+ <NextLink href={href} className={props.className}>
35
+ {children}
36
+ </NextLink>
37
+ )
38
+ }
@@ -0,0 +1,9 @@
1
+ .mermaid {
2
+ margin: var(--rs-space-5) 0;
3
+ overflow-x: auto;
4
+ }
5
+
6
+ .mermaid svg {
7
+ max-width: 100%;
8
+ height: auto;
9
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useId, useState } from 'react'
4
+ import styles from './mermaid.module.css'
5
+
6
+ interface MermaidProps {
7
+ chart: string
8
+ }
9
+
10
+ export function Mermaid({ chart }: MermaidProps) {
11
+ const mermaidId = useId().replace(/:/g, '-')
12
+ const [svg, setSvg] = useState<string>('')
13
+
14
+ useEffect(() => {
15
+ let cancelled = false
16
+
17
+ async function render() {
18
+ const { default: mermaid } = await import('mermaid')
19
+ mermaid.initialize({ startOnLoad: false, theme: 'default' })
20
+ const { svg: rendered } = await mermaid.render(
21
+ mermaidId,
22
+ chart
23
+ )
24
+ if (!cancelled) setSvg(rendered)
25
+ }
26
+
27
+ render()
28
+ return () => { cancelled = true }
29
+ }, [chart])
30
+
31
+ return (
32
+ <div
33
+ className={styles.mermaid}
34
+ dangerouslySetInnerHTML={{ __html: svg }}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,8 @@
1
+ .paragraph {
2
+ font-size: var(--rs-font-size-regular);
3
+ margin-bottom: var(--rs-space-5);
4
+ }
5
+
6
+ .paragraph :global(a) {
7
+ font-size: inherit;
8
+ }
@@ -0,0 +1,19 @@
1
+ import { Children, isValidElement, type ComponentProps } from 'react'
2
+ import styles from './paragraph.module.css'
3
+
4
+ const BLOCK_ELEMENTS = new Set(['summary', 'details', 'div', 'table', 'ul', 'ol'])
5
+
6
+ function hasBlockChild(children: React.ReactNode): boolean {
7
+ return Children.toArray(children).some(
8
+ (child) => isValidElement(child) && typeof child.type === 'string' && BLOCK_ELEMENTS.has(child.type)
9
+ )
10
+ }
11
+
12
+ export function MdxParagraph({ children, className, ...props }: ComponentProps<'p'>) {
13
+ const Tag = hasBlockChild(children) ? 'div' : 'p'
14
+ return (
15
+ <Tag className={`${styles.paragraph} ${className ?? ''}`} {...props}>
16
+ {children}
17
+ </Tag>
18
+ )
19
+ }
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ import { Table } from '@raystack/apsara'
4
+ import type { ComponentProps, ReactNode } from 'react'
5
+
6
+ type TableProps = ComponentProps<'table'>
7
+
8
+ export function MdxTable({ children, ...props }: TableProps) {
9
+ return <Table {...props}>{children}</Table>
10
+ }
11
+
12
+ type TheadProps = ComponentProps<'thead'>
13
+
14
+ export function MdxThead({ children, ...props }: TheadProps) {
15
+ return <Table.Header {...props}>{children}</Table.Header>
16
+ }
17
+
18
+ type TbodyProps = ComponentProps<'tbody'>
19
+
20
+ export function MdxTbody({ children, ...props }: TbodyProps) {
21
+ return <Table.Body {...props}>{children}</Table.Body>
22
+ }
23
+
24
+ type TrProps = ComponentProps<'tr'>
25
+
26
+ export function MdxTr({ children, ...props }: TrProps) {
27
+ return <Table.Row {...props}>{children}</Table.Row>
28
+ }
29
+
30
+ type ThProps = ComponentProps<'th'>
31
+
32
+ export function MdxTh({ children, ...props }: ThProps) {
33
+ return <Table.Head {...props}>{children}</Table.Head>
34
+ }
35
+
36
+ type TdProps = ComponentProps<'td'>
37
+
38
+ export function MdxTd({ children, ...props }: TdProps) {
39
+ return <Table.Cell {...props}>{children}</Table.Cell>
40
+ }
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import { Breadcrumb } from '@raystack/apsara'
4
+ import type { PageTree, PageTreeItem } from '@/types'
5
+
6
+ interface BreadcrumbsProps {
7
+ 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
36
+ }
37
+
38
+ export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
39
+ const items: { label: string; href: string }[] = []
40
+
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
+ }
51
+
52
+ return (
53
+ <Breadcrumb size="small">
54
+ {items.flatMap((item, index) => {
55
+ const breadcrumbItem = (
56
+ <Breadcrumb.Item
57
+ key={`item-${index}`}
58
+ href={item.href}
59
+ current={index === items.length - 1}
60
+ >
61
+ {item.label}
62
+ </Breadcrumb.Item>
63
+ )
64
+ if (index === 0) return [breadcrumbItem]
65
+ return [
66
+ <Breadcrumb.Separator key={`sep-${index}`} style={{ display: 'flex' }} />,
67
+ breadcrumbItem,
68
+ ]
69
+ })}
70
+ </Breadcrumb>
71
+ )
72
+ }
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+
3
+ import { ThemeSwitcher } from '@raystack/apsara'
4
+ import { useState, useEffect } from 'react'
5
+
6
+ interface ClientThemeSwitcherProps {
7
+ size?: number
8
+ }
9
+
10
+ export function ClientThemeSwitcher({ size }: ClientThemeSwitcherProps) {
11
+ const [isClient, setIsClient] = useState(false)
12
+
13
+ useEffect(() => {
14
+ setIsClient(true)
15
+ }, [])
16
+
17
+ return isClient ? <ThemeSwitcher size={size} /> : null
18
+ }
@@ -0,0 +1,27 @@
1
+ .footer {
2
+ border-top: 1px solid var(--rs-color-border-base-primary);
3
+ padding: var(--rs-space-5) var(--rs-space-7);
4
+ }
5
+
6
+ .container {
7
+ max-width: 1200px;
8
+ margin: 0 auto;
9
+ width: 100%;
10
+ }
11
+
12
+ .copyright {
13
+ color: var(--rs-color-foreground-base-secondary);
14
+ }
15
+
16
+ .links {
17
+ flex-wrap: wrap;
18
+ }
19
+
20
+ .link {
21
+ color: var(--rs-color-foreground-base-secondary);
22
+ font-size: 14px;
23
+ }
24
+
25
+ .link:hover {
26
+ color: var(--rs-color-foreground-base-primary);
27
+ }
@@ -0,0 +1,30 @@
1
+ import { Flex, Link, Text } from "@raystack/apsara";
2
+ import type { FooterConfig } from "@/types";
3
+ import styles from "./footer.module.css";
4
+
5
+ interface FooterProps {
6
+ config?: FooterConfig;
7
+ }
8
+
9
+ export function Footer({ config }: FooterProps) {
10
+ return (
11
+ <footer className={styles.footer}>
12
+ <Flex align="center" justify="between" className={styles.container}>
13
+ {config?.copyright && (
14
+ <Text size={2} className={styles.copyright}>
15
+ {config.copyright}
16
+ </Text>
17
+ )}
18
+ {config?.links && config.links.length > 0 && (
19
+ <Flex gap="medium" className={styles.links}>
20
+ {config.links.map((link) => (
21
+ <Link key={link.href} href={link.href} className={styles.link}>
22
+ {link.label}
23
+ </Link>
24
+ ))}
25
+ </Flex>
26
+ )}
27
+ </Flex>
28
+ </footer>
29
+ );
30
+ }
@@ -0,0 +1,104 @@
1
+ .trigger {
2
+ gap: 8px;
3
+ color: var(--rs-color-foreground-base-secondary);
4
+ cursor: pointer;
5
+ }
6
+
7
+ .kbd {
8
+ padding: 2px 6px;
9
+ border-radius: 4px;
10
+ border: 1px solid var(--rs-color-border-base-primary);
11
+ font-size: 12px;
12
+ }
13
+
14
+ .dialogContent {
15
+ max-width: 600px;
16
+ padding: 0;
17
+ min-height: 0;
18
+ position: fixed;
19
+ top: 20%;
20
+ left: 50%;
21
+ transform: translateX(-50%);
22
+ border-radius: var(--rs-radius-4);
23
+ }
24
+
25
+ .input {
26
+ flex: 1;
27
+ border: none;
28
+ outline: none;
29
+ background: transparent;
30
+ font-size: 16px;
31
+ color: var(--rs-color-foreground-base-primary);
32
+ }
33
+
34
+ .list {
35
+ max-height: 300px;
36
+ overflow: auto;
37
+ padding: 0;
38
+ }
39
+
40
+ .visuallyHidden {
41
+ position: absolute;
42
+ width: 1px;
43
+ height: 1px;
44
+ padding: 0;
45
+ margin: -1px;
46
+ overflow: hidden;
47
+ clip: rect(0, 0, 0, 0);
48
+ white-space: nowrap;
49
+ border: 0;
50
+ }
51
+
52
+ .item {
53
+ padding: 12px 16px;
54
+ cursor: pointer;
55
+ border-radius: 6px;
56
+ }
57
+
58
+ .item[data-selected="true"] {
59
+ background: var(--rs-color-background-base-secondary);
60
+ color: var(--rs-color-foreground-accent-primary-hover);
61
+ }
62
+
63
+ .itemContent {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 12px;
67
+ }
68
+
69
+ .resultText {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 8px;
73
+ }
74
+
75
+ .headingText {
76
+ color: var(--rs-color-foreground-base-secondary);
77
+ }
78
+
79
+ .item[data-selected="true"] .headingText {
80
+ color: var(--rs-color-foreground-accent-primary-hover);
81
+ }
82
+
83
+ .separator {
84
+ color: var(--rs-color-foreground-base-secondary);
85
+ }
86
+
87
+ .pageText {
88
+ color: var(--rs-color-foreground-base-primary);
89
+ }
90
+
91
+ .item[data-selected="true"] .pageText {
92
+ color: var(--rs-color-foreground-accent-primary-hover);
93
+ }
94
+
95
+ .icon {
96
+ width: 18px;
97
+ height: 18px;
98
+ color: var(--rs-color-foreground-base-secondary);
99
+ flex-shrink: 0;
100
+ }
101
+
102
+ .item[data-selected="true"] .icon {
103
+ color: var(--rs-color-foreground-accent-primary-hover);
104
+ }
@@ -0,0 +1,202 @@
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";
13
+
14
+ function SearchShortcutKey({ className }: { className?: string }) {
15
+ const [key, setKey] = useState("⌘");
16
+
17
+ useEffect(() => {
18
+ setKey(isMacOs ? "⌘" : "Ctrl");
19
+ }, []);
20
+
21
+ return (
22
+ <kbd className={className} suppressHydrationWarning>
23
+ {key} K
24
+ </kbd>
25
+ );
26
+ }
27
+
28
+ interface SearchProps {
29
+ className?: string
30
+ }
31
+
32
+ export function Search({ className }: SearchProps) {
33
+ const [open, setOpen] = useState(false);
34
+ const router = useRouter();
35
+
36
+ const { search, setSearch, query } = useDocsSearch({
37
+ type: "fetch",
38
+ api: "/api/search",
39
+ delayMs: 100,
40
+ allowEmpty: true,
41
+ });
42
+
43
+ const onSelect = useCallback(
44
+ (url: string) => {
45
+ setOpen(false);
46
+ router.push(url);
47
+ },
48
+ [router],
49
+ );
50
+
51
+ useEffect(() => {
52
+ const down = (e: KeyboardEvent) => {
53
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
54
+ e.preventDefault();
55
+ setOpen((open) => !open);
56
+ }
57
+ };
58
+
59
+ document.addEventListener("keydown", down);
60
+ return () => document.removeEventListener("keydown", down);
61
+ }, []);
62
+
63
+ const results = deduplicateByUrl(
64
+ query.data === "empty" ? [] : (query.data ?? []),
65
+ );
66
+
67
+ return (
68
+ <>
69
+ <Button
70
+ variant="outline"
71
+ color="neutral"
72
+ size="small"
73
+ onClick={() => setOpen(true)}
74
+ className={cx(styles.trigger, className)}
75
+ trailingIcon={<SearchShortcutKey className={styles.kbd} />}
76
+ >
77
+ Search...
78
+ </Button>
79
+
80
+ <Dialog open={open} onOpenChange={setOpen}>
81
+ <Dialog.Content className={styles.dialogContent}>
82
+ <Dialog.Title className={styles.visuallyHidden}>
83
+ Search documentation
84
+ </Dialog.Title>
85
+ <Command loop>
86
+ <Command.Input
87
+ placeholder="Search"
88
+ value={search}
89
+ onValueChange={setSearch}
90
+ className={styles.input}
91
+ />
92
+
93
+ <Command.List className={styles.list}>
94
+ {query.isLoading && <Command.Empty>Loading...</Command.Empty>}
95
+ {!query.isLoading &&
96
+ search.length > 0 &&
97
+ results.length === 0 && (
98
+ <Command.Empty>No results found.</Command.Empty>
99
+ )}
100
+ {!query.isLoading &&
101
+ search.length === 0 &&
102
+ results.length > 0 && (
103
+ <Command.Group heading="Suggestions">
104
+ {results.slice(0, 8).map((result: SortedResult) => (
105
+ <Command.Item
106
+ key={result.id}
107
+ value={result.id}
108
+ onSelect={() => onSelect(result.url)}
109
+ className={styles.item}
110
+ >
111
+ <div className={styles.itemContent}>
112
+ {getResultIcon(result)}
113
+ <Text className={styles.pageText}>
114
+ {stripMethod(result.content)}
115
+ </Text>
116
+ </div>
117
+ </Command.Item>
118
+ ))}
119
+ </Command.Group>
120
+ )}
121
+ {search.length > 0 &&
122
+ results.map((result: SortedResult) => (
123
+ <Command.Item
124
+ key={result.id}
125
+ value={result.id}
126
+ onSelect={() => onSelect(result.url)}
127
+ className={styles.item}
128
+ >
129
+ <div className={styles.itemContent}>
130
+ {getResultIcon(result)}
131
+ <div className={styles.resultText}>
132
+ {result.type === "heading" ? (
133
+ <>
134
+ <Text className={styles.headingText}>
135
+ {stripMethod(result.content)}
136
+ </Text>
137
+ <Text className={styles.separator}>-</Text>
138
+ <Text className={styles.pageText}>
139
+ {getPageTitle(result.url)}
140
+ </Text>
141
+ </>
142
+ ) : (
143
+ <Text className={styles.pageText}>
144
+ {stripMethod(result.content)}
145
+ </Text>
146
+ )}
147
+ </div>
148
+ </div>
149
+ </Command.Item>
150
+ ))}
151
+ </Command.List>
152
+ </Command>
153
+ </Dialog.Content>
154
+ </Dialog>
155
+ </>
156
+ );
157
+ }
158
+
159
+ function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
160
+ const seen = new Set<string>();
161
+ return results.filter((r) => {
162
+ const base = r.url.split("#")[0];
163
+ if (seen.has(base)) return false;
164
+ seen.add(base);
165
+ return true;
166
+ });
167
+ }
168
+
169
+ const API_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
170
+
171
+ function extractMethod(content: string): string | null {
172
+ const first = content.split(" ")[0];
173
+ return API_METHODS.has(first) ? first : null;
174
+ }
175
+
176
+ function stripMethod(content: string): string {
177
+ const first = content.split(" ")[0];
178
+ return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
179
+ }
180
+
181
+ function getResultIcon(result: SortedResult): React.ReactNode {
182
+ if (!result.url.startsWith("/apis/")) {
183
+ return result.type === "page" ? (
184
+ <DocumentIcon className={styles.icon} />
185
+ ) : (
186
+ <HashtagIcon className={styles.icon} />
187
+ );
188
+ }
189
+ const method = extractMethod(result.content);
190
+ return method ? <MethodBadge method={method} size="micro" /> : null;
191
+ }
192
+
193
+ function getPageTitle(url: string): string {
194
+ const path = url.split("#")[0];
195
+ const segments = path.split("/").filter(Boolean);
196
+ const lastSegment = segments[segments.length - 1];
197
+ if (!lastSegment) return "Home";
198
+ return lastSegment
199
+ .split("-")
200
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
201
+ .join(" ");
202
+ }