@raystack/chronicle 0.12.4 → 0.12.5

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 (36) hide show
  1. package/dist/cli/index.js +1763 -252
  2. package/package.json +8 -2
  3. package/src/cli/commands/build.ts +36 -7
  4. package/src/cli/commands/static-generate.ts +1071 -0
  5. package/src/cli/utils/mdx-error-report.ts +48 -0
  6. package/src/components/api/playground-dialog.tsx +65 -14
  7. package/src/components/mdx/code.tsx +8 -3
  8. package/src/components/mdx/image.tsx +15 -1
  9. package/src/components/mdx/index.tsx +10 -3
  10. package/src/components/ui/search.tsx +226 -64
  11. package/src/lib/data-urls.ts +23 -0
  12. package/src/lib/mdx-component-names.ts +15 -0
  13. package/src/lib/mdx-error.test.ts +78 -0
  14. package/src/lib/mdx-error.ts +91 -0
  15. package/src/lib/openapi.ts +1 -1
  16. package/src/lib/page-context.tsx +29 -14
  17. package/src/lib/preload.ts +4 -2
  18. package/src/lib/remark-validate-mdx.test.ts +95 -0
  19. package/src/lib/remark-validate-mdx.ts +85 -0
  20. package/src/lib/route-resolver.ts +7 -0
  21. package/src/lib/static-mode.ts +3 -0
  22. package/src/pages/DocsPage.tsx +3 -2
  23. package/src/pages/NotFound.module.css +10 -0
  24. package/src/pages/RenderError.tsx +18 -0
  25. package/src/server/App.tsx +16 -12
  26. package/src/server/api/apis-proxy.ts +32 -7
  27. package/src/server/dev-error-page.ts +133 -0
  28. package/src/server/entry-server.tsx +6 -0
  29. package/src/server/entry-static.tsx +151 -0
  30. package/src/server/error.ts +13 -1
  31. package/src/server/plugins/telemetry.ts +3 -2
  32. package/src/server/vite-config.ts +22 -9
  33. package/src/themes/default/Page.module.css +2 -0
  34. package/src/themes/paper/Layout.module.css +0 -4
  35. package/src/themes/paper/Layout.tsx +0 -2
  36. package/src/themes/paper/Page.module.css +2 -0
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { buildCodeFrame, parseMdxError } from '@/lib/mdx-error';
5
+
6
+ /**
7
+ * Prints a formatted report for MDX build failures (file, line:column,
8
+ * message, code frame). Returns false when the error is not MDX-related
9
+ * so the caller can rethrow it.
10
+ */
11
+ export async function printMdxBuildError(err: unknown, packageRoot: string): Promise<boolean> {
12
+ const info = parseMdxError(err);
13
+ if (!info) return false;
14
+
15
+ const contentMirror = path.resolve(packageRoot, '.content');
16
+ const displayPath = info.file?.startsWith(contentMirror)
17
+ ? path.relative(contentMirror, info.file)
18
+ : info.file && !path.relative(process.cwd(), info.file).startsWith('..')
19
+ ? path.relative(process.cwd(), info.file)
20
+ : info.file;
21
+
22
+ console.error();
23
+ console.error(chalk.bgRed.white(' MDX Error '), chalk.red(info.message));
24
+ if (displayPath) {
25
+ const position = info.line ? `:${info.line}${info.column ? `:${info.column}` : ''}` : '';
26
+ console.error(chalk.dim(` ${displayPath}${position}`));
27
+ }
28
+
29
+ if (info.file && info.line) {
30
+ try {
31
+ const source = await fs.readFile(info.file, 'utf-8');
32
+ const frame = buildCodeFrame(source, info.line, info.column);
33
+ const gutter = String(frame.lines[frame.lines.length - 1]?.number ?? 0).length;
34
+ console.error();
35
+ for (const { number, text, target } of frame.lines) {
36
+ const ln = `${target ? '>' : ' '} ${String(number).padStart(gutter)} | `;
37
+ console.error(target ? chalk.red(ln) + text : chalk.dim(ln + text));
38
+ if (target && frame.caretColumn) {
39
+ console.error(chalk.red(` ${' '.repeat(gutter)} | ${' '.repeat(frame.caretColumn - 1)}^`));
40
+ }
41
+ }
42
+ } catch {
43
+ // source not readable — message-only report
44
+ }
45
+ }
46
+ console.error();
47
+ return true;
48
+ }
@@ -8,9 +8,66 @@ import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons'
8
8
  import { MethodBadge } from '@/components/api/method-badge'
9
9
  import { flattenSchema, generateExampleJson, toKind, type SchemaField } from '@/lib/schema'
10
10
  import { generateCurl } from '@/lib/snippet-generators'
11
+ import { isStaticMode } from '@/lib/static-mode'
11
12
  import { JsonEditor } from '@/components/api/json-editor'
12
13
  import styles from './playground-dialog.module.css'
13
14
 
15
+ type ProxyResponse = {
16
+ status: number
17
+ statusText: string
18
+ body: unknown
19
+ headers?: Record<string, string>
20
+ }
21
+
22
+ async function sendViaProxy(
23
+ specName: string,
24
+ method: string,
25
+ path: string,
26
+ headers: Record<string, string>,
27
+ body: unknown,
28
+ ): Promise<ProxyResponse> {
29
+ const res = await fetch('/api/apis-proxy', {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ specName, method, path, headers, body }),
33
+ })
34
+ const data = await res.json()
35
+ if (data.status !== undefined) return data
36
+ return { status: res.status, statusText: res.statusText, body: data.error ?? data }
37
+ }
38
+
39
+ async function sendDirect(
40
+ serverUrl: string,
41
+ method: string,
42
+ path: string,
43
+ headers: Record<string, string>,
44
+ body: unknown,
45
+ ): Promise<ProxyResponse> {
46
+ const url = serverUrl + path
47
+ try {
48
+ const res = await fetch(url, {
49
+ method,
50
+ headers,
51
+ body: body ? JSON.stringify(body) : undefined,
52
+ })
53
+ const contentType = res.headers.get('content-type') ?? ''
54
+ const responseBody = contentType.includes('application/json')
55
+ ? await res.json()
56
+ : await res.text()
57
+ const responseHeaders: Record<string, string> = {}
58
+ res.headers.forEach((v, k) => { responseHeaders[k] = v })
59
+ return { status: res.status, statusText: res.statusText, body: responseBody, headers: responseHeaders }
60
+ } catch (err) {
61
+ if (err instanceof TypeError) {
62
+ throw new Error(
63
+ `CORS Error: The API server at ${serverUrl} does not allow requests from this origin.\n` +
64
+ `Ask the API server administrator to add this site's origin to their CORS allowed origins.`
65
+ )
66
+ }
67
+ throw err
68
+ }
69
+ }
70
+
14
71
  type AuthScheme = {
15
72
  name: string
16
73
  type: 'apiKey' | 'bearer' | 'basic' | 'none'
@@ -193,24 +250,18 @@ export function PlaygroundDialog({
193
250
  }
194
251
 
195
252
  try {
196
- const res = await fetch('/api/apis-proxy', {
197
- method: 'POST',
198
- headers: { 'Content-Type': 'application/json' },
199
- body: JSON.stringify({ specName, method, path: fullPath, headers: reqHeaders, body: reqBody }),
200
- })
201
- const data = await res.json()
253
+ const data = isStaticMode()
254
+ ? await sendDirect(serverUrl, method, fullPath, reqHeaders, reqBody)
255
+ : await sendViaProxy(specName, method, fullPath, reqHeaders, reqBody)
202
256
  const elapsed = Math.round(performance.now() - startTime)
203
- if (data.status !== undefined) {
204
- setResponseData({ ...data, time: elapsed })
205
- } else {
206
- setResponseData({ status: res.status, statusText: res.statusText, body: data.error ?? data, time: elapsed })
207
- }
208
- } catch {
209
- setResponseData({ status: 0, statusText: 'Error', body: 'Failed to send request', time: 0 })
257
+ setResponseData({ ...data, time: elapsed })
258
+ } catch (err) {
259
+ const message = err instanceof Error ? err.message : 'Failed to send request'
260
+ setResponseData({ status: 0, statusText: 'Error', body: message, time: 0 })
210
261
  } finally {
211
262
  setLoading(false)
212
263
  }
213
- }, [specName, method, path, pathValues, queryValues, getAuthHeaders, headerValues, bodyValues, body])
264
+ }, [specName, method, path, serverUrl, pathValues, queryValues, getAuthHeaders, headerValues, bodyValues, body])
214
265
 
215
266
  const responseJson = responseData
216
267
  ? (typeof responseData.body === 'string' ? responseData.body : JSON.stringify(responseData.body, null, 2))
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { type ComponentProps, isValidElement, Children } from 'react'
4
- import { Mermaid } from './mermaid'
3
+ import { type ComponentProps, isValidElement, lazy, Suspense } from 'react'
5
4
  import styles from './code.module.css'
6
5
 
6
+ const Mermaid = lazy(() => import('./mermaid').then(m => ({ default: m.Mermaid })))
7
+
7
8
  type PreProps = ComponentProps<'pre'> & {
8
9
  'data-language'?: string
9
10
  title?: string
@@ -21,7 +22,11 @@ export function MdxPre({ children, title, className, ...props }: PreProps) {
21
22
  if (isValidElement(children)) {
22
23
  const childProps = children.props as { className?: string; children?: string }
23
24
  if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
24
- return <Mermaid chart={childProps.children} />
25
+ return (
26
+ <Suspense fallback={<pre className={styles.pre}><code>{childProps.children}</code></pre>}>
27
+ <Mermaid chart={childProps.children} />
28
+ </Suspense>
29
+ )
25
30
  }
26
31
  }
27
32
 
@@ -1,13 +1,27 @@
1
1
  import type { ComponentProps } from 'react';
2
2
  import { isLocalImage, isSvg, buildOptimizedUrl, DEFAULT_WIDTH } from '@/lib/image-utils';
3
+ import { isStaticMode } from '@/lib/static-mode';
3
4
 
4
5
  type MDXImageProps = ComponentProps<'img'>;
5
6
 
7
+ function webpUrl(src: string): string {
8
+ return src.replace(/\.[^.]+$/, '.webp');
9
+ }
10
+
6
11
  export function MDXImage({ src, alt, ...props }: MDXImageProps) {
7
12
  if (!src) return null;
8
13
 
9
14
  const optimize = isLocalImage(src) && !isSvg(src);
10
- const imgSrc = optimize ? buildOptimizedUrl(src, DEFAULT_WIDTH) : src;
11
15
 
16
+ if (optimize && isStaticMode()) {
17
+ return (
18
+ <picture>
19
+ <source srcSet={webpUrl(src)} type="image/webp" />
20
+ <img src={src} alt={alt ?? ''} loading='lazy' decoding='async' {...props} />
21
+ </picture>
22
+ );
23
+ }
24
+
25
+ const imgSrc = optimize ? buildOptimizedUrl(src, DEFAULT_WIDTH) : src;
12
26
  return <img src={imgSrc} alt={alt ?? ''} loading='lazy' decoding='async' {...props} />;
13
27
  }
@@ -4,11 +4,12 @@ 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'
7
- import { Mermaid } from './mermaid'
8
7
  import { MdxParagraph } from './paragraph'
9
8
  import { CalloutContainer, CalloutTitle, CalloutDescription, MdxBlockquote } from '@/components/common/callout'
10
9
  import { Tabs } from '@raystack/apsara'
11
- import { type ComponentProps, useEffect, useState } from 'react'
10
+ import { type ComponentProps, lazy, useEffect, useState, Suspense } from 'react'
11
+
12
+ const LazyMermaid = lazy(() => import('./mermaid').then(m => ({ default: m.Mermaid })))
12
13
 
13
14
  function ClientOnly({ children }: { children: React.ReactNode }) {
14
15
  const [mounted, setMounted] = useState(false)
@@ -23,6 +24,8 @@ MdxTabs.List = Tabs.List
23
24
  MdxTabs.Tab = Tabs.Tab
24
25
  MdxTabs.Content = Tabs.Content
25
26
 
27
+ // Capitalized keys must stay in sync with MDX_COMPONENT_NAMES in
28
+ // src/lib/mdx-component-names.ts (used for compile-time validation).
26
29
  export const mdxComponents: MDXComponents = {
27
30
  p: MdxParagraph,
28
31
  img: MDXImage,
@@ -42,7 +45,11 @@ export const mdxComponents: MDXComponents = {
42
45
  CalloutTitle,
43
46
  CalloutDescription,
44
47
  Tabs: MdxTabs,
45
- Mermaid,
48
+ Mermaid: (props: { chart: string }) => (
49
+ <Suspense fallback={<pre><code>{props.chart}</code></pre>}>
50
+ <LazyMermaid {...props} />
51
+ </Suspense>
52
+ ),
46
53
  }
47
54
 
48
55
  export { MDXImage } from './image'
@@ -7,11 +7,24 @@ import {
7
7
  } from '@heroicons/react/24/outline';
8
8
  import { Command, IconButton, Text } from '@raystack/apsara';
9
9
  import { keepPreviousData, useQuery } from '@tanstack/react-query';
10
+ import GithubSlugger from 'github-slugger';
10
11
  import { debounce } from 'lodash-es';
11
- import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react';
12
+ import MiniSearch from 'minisearch';
13
+ import {
14
+ createContext,
15
+ useCallback,
16
+ useContext,
17
+ useEffect,
18
+ useMemo,
19
+ useState,
20
+ type ChangeEvent,
21
+ type ReactNode
22
+ } from 'react';
12
23
  import { useNavigate } from 'react-router';
13
24
  import { MethodBadge } from '@/components/api/method-badge';
14
25
  import { usePageContext } from '@/lib/page-context';
26
+ import { isStaticMode } from '@/lib/static-mode';
27
+ import { searchIndexUrl } from '@/lib/data-urls';
15
28
  import { SearchMatchType } from '@/types';
16
29
  import styles from './search.module.css';
17
30
 
@@ -29,6 +42,127 @@ interface SearchProps {
29
42
  classNames?: { trigger?: string };
30
43
  }
31
44
 
45
+ interface SearchDocument {
46
+ id: string;
47
+ url: string;
48
+ title: string;
49
+ headings: string;
50
+ body: string;
51
+ type: string;
52
+ section: string;
53
+ }
54
+
55
+ let miniSearchInstance: MiniSearch<SearchDocument> | null = null;
56
+ let miniSearchLoading: Promise<MiniSearch<SearchDocument>> | null = null;
57
+ let searchDocuments: SearchDocument[] = [];
58
+
59
+ function loadSearchIndex(): Promise<MiniSearch<SearchDocument>> {
60
+ if (miniSearchInstance) return Promise.resolve(miniSearchInstance);
61
+ if (miniSearchLoading) return miniSearchLoading;
62
+
63
+ miniSearchLoading = fetch(searchIndexUrl())
64
+ .then(res => {
65
+ if (!res.ok) throw new Error(`Failed to load search index: ${res.status}`);
66
+ return res.json() as Promise<SearchDocument[]>;
67
+ })
68
+ .then(docs => {
69
+ searchDocuments = docs;
70
+ const ms = new MiniSearch<SearchDocument>({
71
+ fields: ['title', 'headings', 'body'],
72
+ storeFields: ['url', 'title', 'headings', 'body', 'type', 'section'],
73
+ searchOptions: {
74
+ boost: { title: 10, headings: 5, body: 1 },
75
+ prefix: true,
76
+ fuzzy: 0.2,
77
+ },
78
+ });
79
+ ms.addAll(docs);
80
+ miniSearchInstance = ms;
81
+ return ms;
82
+ })
83
+ .catch(err => {
84
+ miniSearchLoading = null;
85
+ throw err;
86
+ });
87
+
88
+ return miniSearchLoading;
89
+ }
90
+
91
+ function findMatch(
92
+ query: string,
93
+ title: string,
94
+ headings: string,
95
+ body: string,
96
+ ): { match: SearchMatchType; snippet: string; slug?: string } {
97
+ if (title.toLowerCase().includes(query)) {
98
+ return { match: SearchMatchType.Title, snippet: title };
99
+ }
100
+
101
+ const slugger = new GithubSlugger();
102
+ const headingList = headings.split('\n').filter(Boolean);
103
+ for (const h of headingList) {
104
+ const slug = slugger.slug(h);
105
+ if (h.toLowerCase().includes(query)) {
106
+ return { match: SearchMatchType.Heading, snippet: h, slug };
107
+ }
108
+ }
109
+
110
+ const idx = body.toLowerCase().indexOf(query);
111
+ if (idx >= 0) {
112
+ const start = Math.max(0, idx - 40);
113
+ const end = Math.min(body.length, idx + query.length + 80);
114
+ const snippet = (start > 0 ? '...' : '') + body.slice(start, end).trim() + (end < body.length ? '...' : '');
115
+ return { match: SearchMatchType.Body, snippet };
116
+ }
117
+
118
+ return { match: SearchMatchType.Title, snippet: title };
119
+ }
120
+
121
+ async function searchStatic(query: string, tag?: string): Promise<SearchResult[]> {
122
+ const ms = await loadSearchIndex();
123
+
124
+ if (!query) {
125
+ let docs = searchDocuments.filter(d => d.type === 'page');
126
+ if (tag) docs = docs.filter(d => d.url === `/${tag}` || d.url.startsWith(`/${tag}/`));
127
+ return docs.slice(0, 8).map(d => ({
128
+ id: d.id,
129
+ url: d.url,
130
+ type: d.type,
131
+ content: d.title,
132
+ section: d.section || undefined,
133
+ }));
134
+ }
135
+
136
+ let results = ms.search(query);
137
+ if (tag) {
138
+ results = results.filter(r => {
139
+ const url = r.url as string;
140
+ return url === `/${tag}` || url.startsWith(`/${tag}/`);
141
+ });
142
+ }
143
+
144
+ const queryLower = query.toLowerCase();
145
+ return results.slice(0, 20).map(r => {
146
+ const { match, snippet, slug } = findMatch(
147
+ queryLower,
148
+ r.title as string,
149
+ r.headings as string,
150
+ r.body as string,
151
+ );
152
+ const id = match === SearchMatchType.Heading && slug ? `${r.id}#${slug}` : r.id as string;
153
+ const url = match === SearchMatchType.Heading && slug ? `${r.url}#${slug}` : r.url as string;
154
+ return {
155
+ id,
156
+ url,
157
+ type: r.type as string,
158
+ content: r.title as string,
159
+ match,
160
+ snippet,
161
+ section: (r.section as string) || undefined,
162
+ };
163
+ });
164
+ }
165
+
32
166
  function buildSearchUrl(query: string, tag?: string): string {
33
167
  const params = new URLSearchParams();
34
168
  if (query) params.set('query', query);
@@ -37,8 +171,43 @@ function buildSearchUrl(query: string, tag?: string): string {
37
171
  return qs ? `/api/search?${qs}` : '/api/search';
38
172
  }
39
173
 
40
- export function Search({ classNames }: SearchProps) {
174
+ interface SearchContextValue {
175
+ open: boolean;
176
+ setOpen: React.Dispatch<React.SetStateAction<boolean>>;
177
+ }
178
+
179
+ const SearchContext = createContext<SearchContextValue | null>(null);
180
+
181
+ export function SearchProvider({ children }: { children: ReactNode }) {
41
182
  const [open, setOpen] = useState(false);
183
+ const value = useMemo(() => ({ open, setOpen }), [open]);
184
+ return <SearchContext.Provider value={value}>{children}</SearchContext.Provider>;
185
+ }
186
+
187
+ function useSearch(): SearchContextValue {
188
+ const ctx = useContext(SearchContext);
189
+ if (!ctx) throw new Error('Search components must be used within <SearchProvider>');
190
+ return ctx;
191
+ }
192
+
193
+ export function Search({ classNames }: SearchProps) {
194
+ const { setOpen } = useSearch();
195
+
196
+ return (
197
+ <IconButton
198
+ size={3}
199
+ aria-label='Search'
200
+ title='Search (Ctrl/⌘K)'
201
+ onClick={() => setOpen(true)}
202
+ className={classNames?.trigger}
203
+ >
204
+ <MagnifyingGlassIcon width={16} height={16} />
205
+ </IconButton>
206
+ );
207
+ }
208
+
209
+ export function SearchDialog() {
210
+ const { open, setOpen } = useSearch();
42
211
  const [search, setSearch] = useState('');
43
212
  const [debouncedSearch, setDebouncedSearch] = useState('');
44
213
  const navigate = useNavigate();
@@ -64,9 +233,14 @@ export function Search({ classNames }: SearchProps) {
64
233
  }
65
234
  }, [open, updateDebouncedSearch]);
66
235
 
236
+ const staticMode = isStaticMode();
237
+
67
238
  const { data = [], isLoading } = useQuery<SearchResult[]>({
68
- queryKey: ['search', debouncedSearch, tag],
239
+ queryKey: ['search', debouncedSearch, tag, staticMode],
69
240
  queryFn: async ({ signal }) => {
241
+ if (staticMode) {
242
+ return searchStatic(debouncedSearch, tag);
243
+ }
70
244
  const res = await fetch(buildSearchUrl(debouncedSearch, tag), { signal });
71
245
  if (!res.ok) throw new Error(String(res.status));
72
246
  return res.json();
@@ -98,67 +272,55 @@ export function Search({ classNames }: SearchProps) {
98
272
  const displayResults = deduplicateByUrl(data);
99
273
 
100
274
  return (
101
- <>
102
- <IconButton
103
- size={3}
104
- aria-label='Search'
105
- title='Search (Ctrl/⌘K)'
106
- onClick={() => setOpen(true)}
107
- className={classNames?.trigger}
108
- >
109
- <MagnifyingGlassIcon width={16} height={16} />
110
- </IconButton>
111
-
112
- <Command.Dialog open={open} onOpenChange={setOpen}>
113
- <Command.DialogContent className={styles.dialogContent}>
114
- <Command items={displayResults}>
115
- <Command.Input
116
- placeholder='Search'
117
- leadingIcon={<MagnifyingGlassIcon width={16} height={16} />}
118
- value={search}
119
- onChange={onSearchChange}
120
- className={styles.input}
121
- />
122
-
123
- <Command.Content className={styles.list}>
124
- {isLoading && displayResults.length === 0 && <Command.Empty>Loading...</Command.Empty>}
125
- {!isLoading &&
126
- search.length > 0 &&
127
- displayResults.length === 0 && (
128
- <Command.Empty>No results found.</Command.Empty>
129
- )}
130
- {search.length === 0 &&
131
- displayResults.length > 0 && (
132
- <Command.Group>
133
- <Command.Label>Suggestions</Command.Label>
134
- {displayResults.slice(0, 8).map((result) => (
135
- <Command.Item
136
- key={result.id}
137
- value={result.id}
138
- onClick={() => onSelect(result.url)}
139
- className={styles.item}
140
- >
141
- <SearchResultItem result={result} query="" />
142
- </Command.Item>
143
- ))}
144
- </Command.Group>
145
- )}
146
- {search.length > 0 &&
147
- displayResults.map((result) => (
148
- <Command.Item
149
- key={result.id}
150
- value={result.id}
151
- onClick={() => onSelect(result.url)}
152
- className={styles.item}
153
- >
154
- <SearchResultItem result={result} query={search} />
155
- </Command.Item>
156
- ))}
157
- </Command.Content>
158
- </Command>
159
- </Command.DialogContent>
160
- </Command.Dialog>
161
- </>
275
+ <Command.Dialog open={open} onOpenChange={setOpen}>
276
+ <Command.DialogContent className={styles.dialogContent}>
277
+ <Command items={displayResults}>
278
+ <Command.Input
279
+ placeholder='Search'
280
+ leadingIcon={<MagnifyingGlassIcon width={16} height={16} />}
281
+ value={search}
282
+ onChange={onSearchChange}
283
+ className={styles.input}
284
+ />
285
+
286
+ <Command.Content className={styles.list}>
287
+ {isLoading && displayResults.length === 0 && <Command.Empty>Loading...</Command.Empty>}
288
+ {!isLoading &&
289
+ search.length > 0 &&
290
+ displayResults.length === 0 && (
291
+ <Command.Empty>No results found.</Command.Empty>
292
+ )}
293
+ {search.length === 0 &&
294
+ displayResults.length > 0 && (
295
+ <Command.Group>
296
+ <Command.Label>Suggestions</Command.Label>
297
+ {displayResults.slice(0, 8).map((result) => (
298
+ <Command.Item
299
+ key={result.id}
300
+ value={result.id}
301
+ onClick={() => onSelect(result.url)}
302
+ className={styles.item}
303
+ >
304
+ <SearchResultItem result={result} query="" />
305
+ </Command.Item>
306
+ ))}
307
+ </Command.Group>
308
+ )}
309
+ {search.length > 0 &&
310
+ displayResults.map((result) => (
311
+ <Command.Item
312
+ key={result.id}
313
+ value={result.id}
314
+ onClick={() => onSelect(result.url)}
315
+ className={styles.item}
316
+ >
317
+ <SearchResultItem result={result} query={search} />
318
+ </Command.Item>
319
+ ))}
320
+ </Command.Content>
321
+ </Command>
322
+ </Command.DialogContent>
323
+ </Command.Dialog>
162
324
  );
163
325
  }
164
326
 
@@ -0,0 +1,23 @@
1
+ import { isStaticMode } from './static-mode';
2
+
3
+ export function pageDataUrl(slug: string[]): string {
4
+ const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
5
+ if (isStaticMode()) {
6
+ return `/data/pages/${key || 'index'}.json`;
7
+ }
8
+ return key ? `/api/page?slug=${key}` : '/api/page';
9
+ }
10
+
11
+ export function specsUrl(versionDir: string | null): string {
12
+ if (isStaticMode()) {
13
+ const file = versionDir ? `${encodeURIComponent(versionDir)}.json` : 'latest.json';
14
+ return `/data/specs/${file}`;
15
+ }
16
+ return versionDir
17
+ ? `/api/specs?version=${encodeURIComponent(versionDir)}`
18
+ : '/api/specs';
19
+ }
20
+
21
+ export function searchIndexUrl(): string {
22
+ return '/data/search.json';
23
+ }
@@ -0,0 +1,15 @@
1
+ import { htmlTagNames } from 'html-tag-names'
2
+ import { svgTagNames } from 'svg-tag-names'
3
+
4
+ // Names of custom components available to MDX content.
5
+ // Keep in sync with the capitalized keys of `mdxComponents` in src/components/mdx/index.tsx.
6
+ // This lives in a React-free module so the CLI/vite-config can import it.
7
+ export const MDX_COMPONENT_NAMES = [
8
+ 'Callout',
9
+ 'CalloutTitle',
10
+ 'CalloutDescription',
11
+ 'Tabs',
12
+ 'Mermaid',
13
+ ] as const
14
+
15
+ export const KNOWN_TAGS = new Set<string>([...htmlTagNames, ...svgTagNames])