@clidey/dory 1.0.8 → 1.9.0

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 (198) hide show
  1. package/README.md +12 -1
  2. package/bin/chat-server.ts +270 -0
  3. package/bin/dist/analytics-server.d.ts +11 -0
  4. package/bin/dist/analytics-server.d.ts.map +1 -0
  5. package/bin/dist/analytics-server.js +121 -0
  6. package/bin/dist/analytics-server.js.map +1 -0
  7. package/bin/dist/automation.d.ts +35 -0
  8. package/bin/dist/automation.d.ts.map +1 -0
  9. package/bin/dist/automation.js +259 -0
  10. package/bin/dist/automation.js.map +1 -0
  11. package/bin/dist/chat-server.d.ts +8 -0
  12. package/bin/dist/chat-server.d.ts.map +1 -0
  13. package/bin/dist/chat-server.js +322 -0
  14. package/bin/dist/chat-server.js.map +1 -0
  15. package/bin/dist/check.d.ts +16 -0
  16. package/bin/dist/check.d.ts.map +1 -0
  17. package/bin/dist/check.js +247 -0
  18. package/bin/dist/check.js.map +1 -0
  19. package/bin/dist/dory.js +252 -402
  20. package/bin/dist/dory.js.map +1 -1
  21. package/bin/dist/i18n-init.d.ts +6 -0
  22. package/bin/dist/i18n-init.d.ts.map +1 -0
  23. package/bin/dist/i18n-init.js +56 -0
  24. package/bin/dist/i18n-init.js.map +1 -0
  25. package/bin/dist/mcp-http.d.ts +8 -0
  26. package/bin/dist/mcp-http.d.ts.map +1 -0
  27. package/bin/dist/mcp-http.js +72 -0
  28. package/bin/dist/mcp-http.js.map +1 -0
  29. package/bin/dist/mcp.d.ts +2 -0
  30. package/bin/dist/mcp.d.ts.map +1 -0
  31. package/bin/dist/mcp.js +81 -0
  32. package/bin/dist/mcp.js.map +1 -0
  33. package/bin/dist/preview-entry.d.ts +2 -0
  34. package/bin/dist/preview-entry.d.ts.map +1 -0
  35. package/bin/dist/preview-entry.js +6 -0
  36. package/bin/dist/preview-entry.js.map +1 -0
  37. package/bin/dist/preview-server.d.ts +5 -0
  38. package/bin/dist/preview-server.d.ts.map +1 -0
  39. package/bin/dist/preview-server.js +159 -0
  40. package/bin/dist/preview-server.js.map +1 -0
  41. package/bin/dist/project-config.d.ts +22 -0
  42. package/bin/dist/project-config.d.ts.map +1 -0
  43. package/bin/dist/project-config.js +44 -0
  44. package/bin/dist/project-config.js.map +1 -0
  45. package/dist/index.html +57 -0
  46. package/dist/runtime/dory-runtime.css +2 -0
  47. package/dist/runtime/dory-runtime.html +14 -0
  48. package/dist/runtime/dory-runtime.js +5490 -0
  49. package/docs/api-playground-demo.mdx +119 -0
  50. package/docs/api-reference/asyncapi.json +324 -0
  51. package/docs/api-reference/openapi.json +366 -0
  52. package/docs/api-reference/operations/create.mdx +5 -0
  53. package/docs/api-reference/operations/delete.mdx +5 -0
  54. package/docs/api-reference/operations/get.mdx +5 -0
  55. package/docs/api-reference/operations/update.mdx +5 -0
  56. package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
  57. package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
  58. package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
  59. package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
  60. package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
  61. package/docs/assets/dory.jpeg +0 -0
  62. package/docs/components/accordion.mdx +145 -0
  63. package/docs/components/callouts.mdx +134 -0
  64. package/docs/components/cards.mdx +153 -0
  65. package/docs/components/code-blocks.mdx +281 -0
  66. package/docs/components/expandable.mdx +189 -0
  67. package/docs/components/icons.mdx +71 -0
  68. package/docs/components/images.mdx +82 -0
  69. package/docs/components/latex.mdx +148 -0
  70. package/docs/components/layout.mdx +175 -0
  71. package/docs/components/lists.mdx +134 -0
  72. package/docs/components/mermaid.mdx +149 -0
  73. package/docs/components/overview.mdx +81 -0
  74. package/docs/components/source.mdx +130 -0
  75. package/docs/components/steps.mdx +122 -0
  76. package/docs/components/tables.mdx +114 -0
  77. package/docs/components/tags.mdx +179 -0
  78. package/docs/components/typography.mdx +185 -0
  79. package/docs/configuration/analytics.mdx +45 -0
  80. package/docs/configuration/chat.mdx +59 -0
  81. package/docs/configuration/dory-json.mdx +278 -0
  82. package/docs/configuration/embed.mdx +73 -0
  83. package/docs/configuration/fonts.mdx +86 -0
  84. package/docs/configuration/internationalization.mdx +61 -0
  85. package/docs/configuration/navigation.mdx +217 -0
  86. package/docs/configuration/theming.mdx +166 -0
  87. package/docs/dory.json +129 -0
  88. package/docs/embed-example.html +176 -0
  89. package/docs/favicon.svg +18 -0
  90. package/docs/features.mdx +440 -0
  91. package/docs/getting-started/automation.mdx +76 -0
  92. package/docs/getting-started/configuration.mdx +95 -0
  93. package/docs/getting-started/installation.mdx +88 -0
  94. package/docs/getting-started/quick-start.mdx +53 -0
  95. package/docs/getting-started/whodb-ee-apps.mdx +122 -0
  96. package/docs/introduction.mdx +50 -0
  97. package/docs/logo/logo.svg +18 -0
  98. package/docs/playgrounds/api-playground.mdx +122 -0
  99. package/docs/playgrounds/websocket-playground.mdx +98 -0
  100. package/docs/social-card.png +0 -0
  101. package/docs/social-card.svg +17 -0
  102. package/docs/websocket-playground-demo.mdx +135 -0
  103. package/package.json +23 -17
  104. package/scripts/verify-runtime-parity.mjs +146 -0
  105. package/src/app.css +19 -1
  106. package/src/assets/fonts/HankenGrotesk.ttf +0 -0
  107. package/src/assets/fonts/OFL.txt +94 -0
  108. package/src/components/chat-assistant.tsx +229 -0
  109. package/src/components/dropdown.tsx +3 -3
  110. package/src/components/error-boundary.tsx +2 -2
  111. package/src/components/hooks.tsx +20 -1
  112. package/src/components/navigation.tsx +82 -51
  113. package/src/components/notification.tsx +2 -3
  114. package/src/components/routes.tsx +17 -19
  115. package/src/components/search-index.test.ts +15 -0
  116. package/src/components/search-index.ts +49 -48
  117. package/src/components/search.tsx +208 -104
  118. package/src/components/store.ts +27 -9
  119. package/src/config/mdx-common.ts +11 -0
  120. package/src/config/mdx.ts +10 -11
  121. package/src/embed/loader.ts +0 -3
  122. package/src/embed/types.ts +0 -2
  123. package/src/embed/widget-iframe.ts +19 -21
  124. package/src/embedded-runtime/config.ts +138 -0
  125. package/src/embedded-runtime/index.tsx +680 -0
  126. package/src/embedded-runtime/runtime.html +14 -0
  127. package/src/entry-server.tsx +46 -124
  128. package/src/main.tsx +16 -4
  129. package/src/mdx/accordion.tsx +3 -3
  130. package/src/mdx/api-playground.tsx +18 -16
  131. package/src/mdx/async-api.tsx +2 -2
  132. package/src/mdx/card.tsx +3 -2
  133. package/src/mdx/code.tsx +11 -9
  134. package/src/mdx/fence.tsx +6 -5
  135. package/src/mdx/icon-grid.tsx +4 -1
  136. package/src/mdx/icon-names.test.ts +15 -0
  137. package/src/mdx/icon-names.ts +60 -0
  138. package/src/mdx/icon-source-variants.ts +102 -0
  139. package/src/mdx/icon.tsx +12 -73
  140. package/src/mdx/latex.tsx +3 -2
  141. package/src/mdx/mdx.tsx +1 -1
  142. package/src/mdx/mermaid.tsx +12 -6
  143. package/src/mdx/open-api.tsx +130 -74
  144. package/src/mdx/preprocess.test.ts +16 -0
  145. package/src/mdx/preprocess.ts +61 -0
  146. package/src/mdx/processor.ts +4 -96
  147. package/src/mdx/source.tsx +5 -4
  148. package/src/mdx/specimen.tsx +38 -6
  149. package/src/mdx/tabs.tsx +29 -21
  150. package/src/mdx/text.tsx +9 -9
  151. package/src/mdx/try-it-panel.tsx +284 -0
  152. package/src/mdx/type-scale.tsx +4 -4
  153. package/src/mdx/websocket-playground.tsx +21 -14
  154. package/src/plugins/analytics-injector.ts +8 -0
  155. package/src/plugins/chat-dev-server.ts +32 -0
  156. package/src/plugins/docs-assets.test.ts +25 -0
  157. package/src/plugins/docs-assets.ts +6 -2
  158. package/src/plugins/frontmatter-dev-server.ts +2 -2
  159. package/src/plugins/frontmatter-generator.ts +1 -1
  160. package/src/plugins/html-files-middleware.test.ts +21 -1
  161. package/src/plugins/html-files-middleware.ts +26 -6
  162. package/src/plugins/html-metadata-injector.ts +24 -4
  163. package/src/plugins/llm-txt-dev-server.ts +2 -2
  164. package/src/plugins/llm-txt-generator.ts +21 -4
  165. package/src/plugins/prerender.ts +47 -20
  166. package/src/plugins/robots-generator.ts +1 -1
  167. package/src/plugins/sanitize.ts +1 -1
  168. package/src/plugins/shared.ts +6 -18
  169. package/src/plugins/sitemap-generator.ts +1 -1
  170. package/src/plugins/social-preview.test.ts +71 -0
  171. package/src/plugins/social-preview.ts +87 -0
  172. package/src/plugins/unified-mdx-plugin.ts +1 -1
  173. package/src/theme.scss +630 -207
  174. package/src/types/config.ts +53 -1
  175. package/src/ui/breadcrumb.tsx +17 -0
  176. package/src/ui/docs-header-view.tsx +32 -0
  177. package/src/ui/docs-navigation-view.tsx +69 -0
  178. package/src/ui/docs-page-frame.tsx +50 -0
  179. package/src/ui/dory-mark.tsx +9 -0
  180. package/src/ui/header.tsx +33 -90
  181. package/src/ui/layout.tsx +99 -153
  182. package/src/ui/mobile-navigation.tsx +10 -8
  183. package/src/ui/page-actions.tsx +67 -0
  184. package/src/ui/page-feedback.tsx +50 -0
  185. package/src/ui/page-footer.tsx +23 -0
  186. package/src/ui/prev-next-link.tsx +15 -69
  187. package/src/ui/prev-next-view.tsx +27 -0
  188. package/src/ui/table-of-content-view.tsx +167 -0
  189. package/src/ui/table-of-content.tsx +25 -183
  190. package/src/utils/analytics.test.ts +36 -0
  191. package/src/utils/analytics.ts +35 -0
  192. package/src/utils/colors.ts +7 -0
  193. package/src/utils/content-scope.test.ts +26 -0
  194. package/src/utils/content-scope.ts +39 -0
  195. package/src/utils/search-content.ts +12 -0
  196. package/src/utils/theme.ts +1 -7
  197. package/vite.config.ssr.ts +3 -3
  198. package/vite.config.ts +21 -15
@@ -2,6 +2,9 @@ import { useEffect, useState } from 'preact/hooks';
2
2
  import { useLocation } from 'wouter-preact';
3
3
  import { withBase } from '../utils/base-path';
4
4
 
5
+ const initialEmbedParams = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('embedded') === 'true'
6
+ ? new URLSearchParams(window.location.search) : null;
7
+
5
8
  /**
6
9
  * Strips trailing slash from a pathname (except root "/").
7
10
  */
@@ -53,5 +56,21 @@ export const useSearchParams = () => {
53
56
  */
54
57
  export const useIsEmbedded = () => {
55
58
  const searchParams = useSearchParams();
56
- return searchParams.get('embedded') === 'true';
59
+ const [pathname] = useLocation();
60
+
61
+ useEffect(() => {
62
+ if (!initialEmbedParams) return;
63
+ const params = new URLSearchParams(window.location.search);
64
+ let changed = false;
65
+ for (const key of ['embedded', 'parentOrigin', 'theme']) {
66
+ const value = initialEmbedParams.get(key);
67
+ if (value !== null && params.get(key) !== value) {
68
+ params.set(key, value);
69
+ changed = true;
70
+ }
71
+ }
72
+ if (changed) window.history.replaceState(window.history.state, '', `${window.location.pathname}?${params}${window.location.hash}`);
73
+ }, [pathname]);
74
+
75
+ return searchParams.get('embedded') === 'true' || initialEmbedParams !== null;
57
76
  };
@@ -1,58 +1,89 @@
1
- import classNames from 'classnames';
2
- import { useMemo } from 'preact/hooks';
1
+ import { CatalogIcon } from '@clidey/ux'
2
+ import type { ComponentChildren } from 'preact';
3
+
3
4
  import { Link } from 'wouter-preact';
5
+ import docsConfig from '../../docs/dory.json' with { type: 'json' };
6
+ import type { DoryConfig } from '../types/config';
7
+ import { DocsNavigationView, type NavigationPage } from '../ui/docs-navigation-view';
4
8
  import { usePathname } from './hooks';
5
- import { ALL_NAVIGATION } from './store';
9
+ import { ALL_PAGES, completeFrontMatter, getNavigationForPath } from './store';
10
+ import { contentScope, scopedPath } from '../utils/content-scope';
11
+ import { withBase } from '../utils/base-path';
12
+
13
+ const config = docsConfig as DoryConfig;
6
14
 
7
- export function Navigation() {
8
- const pathname = usePathname();
15
+ type Frontmatter = Record<string, any>;
9
16
 
10
- const effectivePath = pathname === '/'
11
- ? ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/'
12
- : pathname;
17
+ /** Sidebar label for a page. API pages show a method tag and their path or message name. */
18
+ function pageLabel(href: string, title: string, frontmatter: Frontmatter[]): { method?: string; title: string } {
19
+ const fm = frontmatter.find((entry) => entry.path === href);
20
+ if (typeof fm?.openapi === 'string') {
21
+ const [verb, path] = fm.openapi.split(' ');
22
+ const method = verb.toUpperCase() === 'DELETE' ? 'DEL' : verb.toUpperCase();
23
+ return { method, title: path ?? title };
24
+ }
25
+ if (typeof fm?.asyncapi === 'string') {
26
+ const [operation] = fm.asyncapi.split(' ');
27
+ return { method: operation.toLowerCase() === 'publish' ? 'PUB' : 'SUB', title };
28
+ }
29
+ return { title };
30
+ }
13
31
 
14
- const currentTab = useMemo(() => {
15
- return ALL_NAVIGATION.find(tab =>
16
- tab.groups.some(group =>
17
- group.pages.some(page => page.href === effectivePath)
18
- )
19
- );
20
- }, [effectivePath]);
32
+ /** Static-site adapter for Dory's shared documentation navigation. */
33
+ export function Navigation({ frontmatter = completeFrontMatter }: { frontmatter?: Frontmatter[] }) {
34
+ const pathname = usePathname();
35
+ const scope = contentScope(config, pathname);
36
+ const navigation = getNavigationForPath(pathname);
37
+ const switchTo = (locale: string, version: string) => {
38
+ const candidate = scopedPath(config, pathname, locale, version);
39
+ if (ALL_PAGES[candidate]) return candidate;
40
+ return getNavigationForPath(candidate)[0]?.groups[0]?.pages[0]?.href ?? '/';
41
+ };
42
+ const effectivePath = pathname === '/'
43
+ ? navigation[0]?.groups[0]?.pages[0]?.href ?? '/'
44
+ : pathname;
45
+ const groups = navigation.flatMap(tab => tab.groups).map(group => ({
46
+ title: group.title,
47
+ pages: group.pages.map(page => {
48
+ const translatedTitle = frontmatter.find(entry => entry.path === page.href)?.title ?? page.title;
49
+ return { ...page, ...pageLabel(page.href, translatedTitle, frontmatter) };
50
+ }),
51
+ }));
52
+ const currentVersion = config.versions?.find(version => version.id === scope.version);
53
+ const versionLabel = currentVersion?.label ?? config.version;
54
+ const versionSelect = config.versions && config.versions.length > 0 ? <>
55
+ <CatalogIcon name="up-down" aria-hidden="true" className="h-3.5 w-3.5 text-white/50" />
56
+ {/* Transparent native select over the card: keyboard and screen-reader friendly. */}
57
+ <select
58
+ aria-label="Version"
59
+ value={scope.version}
60
+ onChange={event => {
61
+ const id = (event.target as HTMLSelectElement).value;
62
+ const version = config.versions?.find(entry => entry.id === id || entry.url === id);
63
+ const target = version?.url ?? withBase(switchTo(scope.locale, id));
64
+ window.location.href = target;
65
+ }}
66
+ className="absolute inset-0 cursor-pointer opacity-0"
67
+ >
68
+ <option value="">{config.version}</option>
69
+ {config.versions.map(version => <option key={version.id ?? version.url} value={version.id ?? version.url}>{version.label}</option>)}
70
+ </select>
71
+ </> : undefined;
72
+ const languageSelect = config.i18n && config.i18n.locales.length > 1 ?
73
+ <label className="flex items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-white">
74
+ <span className="dory-mono-label">Language</span>
75
+ <select
76
+ aria-label="Language"
77
+ value={scope.locale}
78
+ onChange={event => { window.location.href = withBase(switchTo((event.target as HTMLSelectElement).value, scope.version)); }}
79
+ className="min-w-0 max-w-32 bg-transparent text-right text-sm text-white"
80
+ >
81
+ {config.i18n.locales.map(locale => <option key={locale.code} value={locale.code} className="text-black">{locale.label}</option>)}
82
+ </select>
83
+ </label> : undefined;
84
+ const renderPageLink = (page: NavigationPage, className: string, active: boolean, children: ComponentChildren) =>
85
+ <Link href={page.href} aria-current={active ? 'page' : undefined} className={className}>{children}</Link>;
21
86
 
22
- return (
23
- <nav className="dory-navigation text-base lg:text-sm">
24
- <ul role="list" className="space-y-9">
25
- {currentTab && (
26
- <li key={currentTab.title}>
27
- <h2 className="font-display font-medium text-xs">{currentTab.title}</h2>
28
- {currentTab.groups.map(group => (
29
- <div key={group.title}>
30
- <h3 className="mt-4 text-sm font-semibold">{group.title}</h3>
31
- <ul role="list" className="mt-2 space-y-2 border-l-2 border-border lg:mt-4 lg:space-y-4 lg:border-border">
32
- {group.pages.map((page) => {
33
- const isActive = effectivePath === page.href;
34
- return (
35
- <li key={page.href} className="relative">
36
- <Link
37
- href={page.href}
38
- className={classNames(
39
- 'block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full',
40
- {
41
- 'font-semibold text-brand-foreground': isActive,
42
- 'text-muted-foreground before:hidden before:bg-slate-300 hover:text-muted-foreground hover:before:block dark:text-muted-foreground dark:before:bg-slate-700 dark:hover:text-slate-300': !isActive
43
- },
44
- )}>
45
- {page.title}
46
- </Link>
47
- </li>
48
- );
49
- })}
50
- </ul>
51
- </div>
52
- ))}
53
- </li>
54
- )}
55
- </ul>
56
- </nav>
57
- );
87
+ return <DocsNavigationView groups={groups} activeHref={effectivePath} renderPageLink={renderPageLink}
88
+ versionLabel={versionLabel} versionSelect={versionSelect} languageSelect={languageSelect} />;
58
89
  }
@@ -1,11 +1,10 @@
1
- import { toast } from '@clidey/ux';
2
- import { InfoIcon } from 'lucide-react';
1
+ import { CatalogIcon, toast } from '@clidey/ux';
3
2
 
4
3
  export function useNotification() {
5
4
  return {
6
5
  showNotification: (message: string, description?: string) => {
7
6
  toast(message, {
8
- icon: <InfoIcon className="h-4 w-4" />,
7
+ icon: <CatalogIcon name="info" className="h-4 w-4" />,
9
8
  description: description,
10
9
  });
11
10
  }
@@ -1,26 +1,32 @@
1
1
  import { useEffect, useMemo } from 'preact/hooks';
2
2
  import { Route, Router, useLocation } from 'wouter-preact';
3
3
  import { usePathname } from './hooks';
4
- import { ALL_NAVIGATION, ALL_PAGES, pathFromFilename } from './store';
4
+ import { ALL_PAGES, getNavigationForPath, pathFromFilename } from './store';
5
+ import docsConfig from '../../docs/dory.json' with { type: 'json' };
6
+ import type { DoryConfig } from '../types/config';
7
+ import { contentScope } from '../utils/content-scope';
8
+ import { NotFoundPage } from '@clidey/ux';
5
9
 
6
10
 
7
11
  export default function Routes() {
8
12
  const pathname = usePathname();
9
13
  const [, navigate] = useLocation();
14
+ const scope = contentScope(docsConfig as DoryConfig, pathname);
15
+ const isScopeRoot = pathname === '/' || (Boolean(scope.prefix) && pathname === scope.prefix);
10
16
 
11
17
  // Redirect root to the first page so the URL reflects the actual content
12
18
  useEffect(() => {
13
- if (pathname === '/') {
14
- const firstPage = ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href;
19
+ if (isScopeRoot) {
20
+ const firstPage = getNavigationForPath(pathname)[0]?.groups[0]?.pages[0]?.href;
15
21
  if (firstPage) navigate(firstPage, { replace: true });
16
22
  }
17
23
  }, [pathname]);
18
24
 
19
25
  const route = useMemo(() => {
20
26
  let targetPath = pathname;
21
- if (pathname === '/') {
27
+ if (isScopeRoot) {
22
28
  const ssrRoute = typeof window !== 'undefined' ? window.__DORY_ROUTE__ : undefined;
23
- targetPath = ssrRoute || ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href || pathname;
29
+ targetPath = (pathname === '/' ? ssrRoute : undefined) || getNavigationForPath(pathname)[0]?.groups[0]?.pages[0]?.href || pathname;
24
30
  }
25
31
 
26
32
  const entry = Object.entries(ALL_PAGES).find(([path]) => path === targetPath);
@@ -28,7 +34,7 @@ export default function Routes() {
28
34
 
29
35
  const [path, mod] = entry;
30
36
  return {
31
- path: pathname === '/' ? '/' : pathFromFilename(path),
37
+ path: isScopeRoot ? pathname : pathFromFilename(path),
32
38
  component: (mod as any).default,
33
39
  };
34
40
  }, [pathname]);
@@ -36,19 +42,11 @@ export default function Routes() {
36
42
  // 404 — no MDX module for this path (even if it appears in navigation)
37
43
  if (!route && pathname !== '/') {
38
44
  return (
39
- <div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
40
- <h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
41
- <h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
42
- <p className="text-gray-600 dark:text-muted-foreground mb-6 text-center max-w-md">
43
- The page <code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm">{pathname}</code> doesn't exist.
44
- </p>
45
- <button
46
- onClick={() => navigate(ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/')}
47
- className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
48
- >
49
- Go to Home
50
- </button>
51
- </div>
45
+ <NotFoundPage
46
+ description={`The page ${pathname} does not exist.`}
47
+ onHome={() => navigate(getNavigationForPath(pathname)[0]?.groups[0]?.pages[0]?.href ?? '/')}
48
+ homeLabel="Go to home"
49
+ />
52
50
  );
53
51
  }
54
52
 
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createSearchIndex } from './search-index';
3
+
4
+ describe('createSearchIndex', () => {
5
+ it('keeps runtime locale indexes separate', async () => {
6
+ const english = createSearchIndex();
7
+ const french = createSearchIndex();
8
+ await english.add('docs/index.mdx', { title: 'Inventory' }, 'publish the app');
9
+ await french.add('docs/fr/index.mdx', { title: 'Inventaire' }, 'publier l’application');
10
+
11
+ expect(english.search('publish').map(item => item.url)).toEqual(['docs/index.mdx']);
12
+ expect(french.search('publish')).toEqual([]);
13
+ expect(french.search('publier').map(item => item.url)).toEqual(['docs/fr/index.mdx']);
14
+ });
15
+ });
@@ -1,54 +1,55 @@
1
- let searchIndex: any = null;
1
+ /** A separate index lets the embedded viewer search only its authorized docs. */
2
+ export function createSearchIndex() {
3
+ let searchIndex: any = null;
2
4
 
3
- async function getSearchIndex() {
4
- if (!searchIndex) {
5
- const { Document } = await import('flexsearch');
6
- searchIndex = new Document({
7
- tokenize: 'full',
8
- document: {
9
- id: 'url',
10
- index: 'content',
11
- store: ['title', 'pageTitle'],
12
- },
13
- context: {
14
- resolution: 9,
15
- depth: 2,
16
- bidirectional: true
17
- }
18
- });
5
+ async function getSearchIndex() {
6
+ if (!searchIndex) {
7
+ const { Document } = await import('flexsearch');
8
+ searchIndex = new Document({
9
+ tokenize: 'full',
10
+ document: {
11
+ id: 'url',
12
+ index: 'content',
13
+ store: ['title', 'pageTitle'],
14
+ },
15
+ context: {
16
+ resolution: 9,
17
+ depth: 2,
18
+ bidirectional: true
19
+ }
20
+ });
21
+ }
22
+ return searchIndex;
19
23
  }
20
- return searchIndex;
21
- }
22
24
 
23
- export async function addToSearchIndex(filename: string, data: any, content: string) {
24
- try {
25
- const index = await getSearchIndex();
26
- index.add({
27
- url: filename,
28
- title: data.title || '',
29
- content: content,
30
- pageTitle: data.title
31
- });
32
- } catch (error) {
33
- console.warn(`Failed to index ${filename}:`, error);
34
- }
25
+ return {
26
+ async add(filename: string, data: { title?: string }, content: string) {
27
+ try {
28
+ const index = await getSearchIndex();
29
+ index.add({
30
+ url: filename,
31
+ title: data.title || '',
32
+ content,
33
+ pageTitle: data.title,
34
+ });
35
+ } catch (error) {
36
+ console.warn(`Failed to index ${filename}:`, error);
37
+ }
38
+ },
39
+ search(query: string, options = {}): Array<{ url: string; title: string; pageTitle: string }> {
40
+ if (!searchIndex) return [];
41
+ const result = searchIndex.search(query, { ...options, enrich: true }) as any[];
42
+ if (result.length === 0) return [];
43
+ return result[0].result.map((item: any) => ({
44
+ url: item.id as string,
45
+ title: item.doc?.title || '',
46
+ pageTitle: item.doc?.pageTitle || '',
47
+ }));
48
+ },
49
+ };
35
50
  }
36
51
 
37
- export function search(query: string, options = {}) {
38
- if (!searchIndex) return [];
52
+ const defaultIndex = createSearchIndex();
39
53
 
40
- const result = searchIndex.search(query, {
41
- ...options,
42
- enrich: true,
43
- }) as any[];
44
-
45
- if (result.length === 0) {
46
- return [];
47
- }
48
-
49
- return result[0].result.map((item: any) => ({
50
- url: item.id,
51
- title: item.doc?.title || '',
52
- pageTitle: item.doc?.pageTitle || '',
53
- }));
54
- }
54
+ export const addToSearchIndex = defaultIndex.add;
55
+ export const search = defaultIndex.search;