@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
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'none'; img-src https: data: blob:; font-src 'self' data:; base-uri 'none'; form-action 'none'">
7
+ <title>Dory documentation</title>
8
+ <link rel="stylesheet" href="./dory-runtime.css">
9
+ </head>
10
+ <body style="margin:0;min-height:100vh">
11
+ <div id="dory-root"></div>
12
+ <script src="./dory-runtime.js"></script>
13
+ </body>
14
+ </html>
@@ -2,26 +2,34 @@ import type { ComponentType } from 'preact';
2
2
  import { renderToString } from 'preact-render-to-string';
3
3
  import { Router } from 'wouter-preact';
4
4
  import { MDXProvider } from '@mdx-js/preact';
5
- import { ThemeProvider } from '@clidey/ux';
5
+ import { ThemeProvider, Toaster } from '@clidey/ux';
6
6
  import * as mdxComponents from './mdx/mdx';
7
7
  import docsConfig from '../docs/dory.json' with { type: 'json' };
8
8
  import type { DoryConfig } from './types/config';
9
9
  import { PageMeta } from './ui/page-meta';
10
- import { BASE_PATH, assetUrl, withBase } from './utils/base-path';
10
+ import { Header } from './ui/header';
11
+ import { Navigation } from './components/navigation';
12
+ import { Breadcrumb } from './ui/breadcrumb';
13
+ import { PageFeedback } from './ui/page-feedback';
14
+ import { PageFooter } from './ui/page-footer';
15
+ import { PrevNextLinks } from './ui/prev-next-link';
16
+ import { TableOfContents } from './ui/table-of-content';
17
+ import { BASE_PATH, withBase } from './utils/base-path';
18
+ import { contentScope } from './utils/content-scope';
11
19
 
12
20
  const config = docsConfig as DoryConfig;
13
21
 
14
- const navigationTabs = config.navigation?.tabs ?? [];
15
22
  if (!config.navigation?.tabs) {
16
23
  console.warn('dory.json has no navigation.tabs — SSR navigation will be empty.');
17
24
  }
18
25
 
19
26
  function pathFromFilename(filename: string): string {
20
- return filename
27
+ const path = filename
21
28
  .replace(/^.*?docs/, '')
22
29
  .replace(/\/?index\.mdx$/, '/')
23
30
  .replace(/\.mdx$/, '')
24
31
  .toLowerCase();
32
+ return path.length > 1 ? path.replace(/\/$/, '') : path;
25
33
  }
26
34
 
27
35
  const pages = import.meta.glob('../docs/**/*.mdx');
@@ -44,19 +52,21 @@ interface NavTab {
44
52
  groups: NavGroup[];
45
53
  }
46
54
 
47
- function buildNavigation(frontmatter?: Record<string, any>[]): NavTab[] {
48
- return navigationTabs.map(tab => ({
55
+ function buildNavigation(routePath: string, frontmatter?: Record<string, any>[]): NavTab[] {
56
+ const scope = contentScope(config, routePath);
57
+ return scope.navigation.tabs.map(tab => ({
49
58
  title: tab.tab,
50
59
  groups: tab.groups.map(group => ({
51
60
  title: group.group,
52
- pages: group.pages.map(page => {
53
- const href = `/${page}`;
61
+ pages: group.pages.map(entry => {
62
+ const page = typeof entry === 'string' ? entry : entry.page;
63
+ const href = `${scope.prefix}/${page}`;
54
64
  const fm = frontmatter?.find(f => f.path === href);
55
65
  return {
56
66
  title: fm?.title || page.charAt(0).toUpperCase() + page.slice(1).replace(/-/g, ' '),
57
67
  href,
58
68
  };
59
- })
69
+ }).filter(page => !scope.prefix || Boolean(allPages[page.href]))
60
70
  }))
61
71
  }));
62
72
  }
@@ -69,7 +79,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
69
79
  const module = await loader() as { default: ComponentType };
70
80
  const Content = module.default;
71
81
 
72
- const navigation = buildNavigation(frontmatter);
82
+ const navigation = buildNavigation(routePath, frontmatter);
73
83
  const pageFm = frontmatter?.find(fm => fm.path === routePath);
74
84
 
75
85
  // Find group/page titles (Layout uses swapped names: "title: group, group: title")
@@ -87,107 +97,35 @@ export async function render(routePath: string, frontmatter?: Record<string, any
87
97
  }
88
98
  }
89
99
 
90
- // Prev/next
91
- const allLinks = navigation.flatMap(tab =>
92
- tab.groups.flatMap(group => group.pages)
93
- );
94
- const linkIndex = allLinks.findIndex(link => link.href === routePath);
95
- const prevPage = linkIndex > 0 ? allLinks[linkIndex - 1] : null;
96
- const nextPage = linkIndex < allLinks.length - 1 ? allLinks[linkIndex + 1] : null;
100
+ const isOpenAPIPage = Boolean(pageFm?.openapi);
97
101
 
98
102
  return renderToString(
99
103
  <ThemeProvider>
104
+ <Toaster />
100
105
  <Router base={BASE_PATH} ssrPath={withBase(routePath)}>
101
106
  <MDXProvider components={mdxComponents}>
102
- <div class="flex w-full flex-col">
103
- {/* SYNC NOTE: the static shell below (header chrome, sidebar nav skeleton,
104
- main/article wrappers) hand-mirrors the client components:
105
- - <header> block -> src/ui/header.tsx (Header component JSX)
106
- - #container/nav -> src/ui/layout.tsx (non-landing return) + src/components/navigation.tsx
107
- When editing structure or classes in either place, update the counterpart,
108
- or hydration will mismatch. Client-only widgets (Search, ModeToggle,
109
- MobileNav) are intentionally NOT rendered here — see placeholder below. */}
110
- {/* Header — matches src/ui/header.tsx structure */}
111
- <header class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-gray-900/5 sm:px-6 lg:px-8 dark:shadow-none dark:bg-transparent">
112
- <div class="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
113
- <div class="flex items-center grow gap-2">
114
- {config.logo && (
115
- <>
116
- <img src={assetUrl(config.logo.light)} alt="logo" class="w-8 hidden dark:block" />
117
- <img src={assetUrl(config.logo.dark)} alt="logo" class="w-8 block dark:hidden" />
118
- </>
119
- )}
120
- <h1 class="text-base lg:text-2xl font-bold">{config.name}</h1>
121
- </div>
122
- <nav class="hidden md:block">
123
- <ul role="list" class="flex items-center gap-8">
124
- {navigation.map(tab => {
125
- const isActive = tab === currentTab;
126
- return (
127
- <li key={tab.title}>
128
- <a
129
- href={withBase(tab.groups[0].pages[0].href)}
130
- class={`text-xs transition hover:opacity-80${isActive ? ' text-brand-foreground' : ''}`}
131
- >
132
- {tab.title}
133
- </a>
134
- </li>
135
- );
136
- })}
137
- </ul>
138
- </nav>
139
- <div class="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
140
- {/* Search, ModeToggle, MobileNav — rendered client-side during hydration.
141
- Static placeholders cause ref mismatches with Algolia autocomplete. */}
142
- <div class="flex items-center gap-3" data-testid="header-actions" />
143
- </div>
144
- </header>
145
-
146
- {/* Container — matches src/ui/layout.tsx structure */}
147
- <div id="container" class="relative flex w-full max-w-8xl sm:px-2 lg:px-8">
148
- <div class="hidden lg:relative lg:block lg:flex-none">
149
- <div class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-x-hidden overflow-y-auto py-16 pr-8 pl-0.5 xl:w-72 xl:pr-16">
150
- <nav class="dory-navigation text-base lg:text-sm">
151
- <ul role="list" class="space-y-9">
152
- {currentTab && (
153
- <li>
154
- <h2 class="font-display font-medium text-xs">{currentTab.title}</h2>
155
- {currentTab.groups.map(group => (
156
- <div key={group.title}>
157
- <h3 class="mt-4 text-sm font-semibold">{group.title}</h3>
158
- <ul role="list" class="mt-2 space-y-2 border-l-2 border-border lg:mt-4 lg:space-y-4 lg:border-border">
159
- {group.pages.map(page => (
160
- <li key={page.href} class="relative">
161
- <a
162
- href={withBase(page.href)}
163
- class={routePath === page.href
164
- ? 'block w-full pl-3.5 font-semibold text-brand-foreground 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'
165
- : 'block w-full pl-3.5 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 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'}
166
- >
167
- {page.title}
168
- </a>
169
- </li>
170
- ))}
171
- </ul>
172
- </div>
173
- ))}
174
- </li>
175
- )}
176
- </ul>
177
- </nav>
107
+ {/* SYNC NOTE: this shell mirrors src/ui/layout.tsx (non-landing return).
108
+ Header and Navigation are the real components; Header gets `ssr` so the
109
+ client-only widgets (search, theme toggle, mobile menu) mount on hydration. */}
110
+ <div class="dory-shell flex min-h-screen w-full flex-col bg-[var(--sidebar)]">
111
+ <Header ssr />
112
+ <div class="flex flex-1">
113
+ <aside class="hidden w-60 shrink-0 lg:block">
114
+ <div class="sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto px-3 pb-6 pt-4">
115
+ <Navigation frontmatter={frontmatter} />
178
116
  </div>
179
- </div>
180
-
181
- <main class="flex grow max-w-full px-4 py-16 sm:px-6 lg:px-8">
182
- <div class="min-w-0 w-full">
183
- <article class="h-full flex flex-1 flex-col">
184
- <div class="flex justify-between items-start">
185
- <header class="mb-8">
186
- {groupTitle && <h1 class="font-display text-lg tracking-tight">{groupTitle}</h1>}
187
- {pageFm?.title && <p class="text-4xl tracking-tight">{pageFm.title}</p>}
188
- {pageFm?.lead && <p class="dory-lead">{pageFm.lead}</p>}
189
- </header>
117
+ </aside>
118
+ <main class="dory-main min-w-0 flex-1 bg-background text-foreground px-6 py-10 sm:px-10 lg:px-12">
119
+ <div class="mx-auto flex w-full max-w-[66rem] justify-center gap-14">
120
+ <article class={`dory-article flex min-w-0 flex-1 flex-col ${isOpenAPIPage ? 'max-w-[66rem]' : 'max-w-[47rem]'}`}>
121
+ {/* Page actions (Copy page / AI actions) are client-only and mount on hydration. */}
122
+ <div class="mb-6 flex min-h-9 flex-wrap items-center justify-between gap-4">
123
+ <Breadcrumb group={groupTitle} title={pageFm?.title} />
190
124
  </div>
125
+ <header class="mb-10">
126
+ {pageFm?.title && <h1 class="dory-page-title">{pageFm.title}</h1>}
127
+ {pageFm?.lead && <p class="dory-lead">{pageFm.lead}</p>}
128
+ </header>
191
129
  <PageMeta meta={pageFm?.meta} />
192
130
  <div class="flex-1 min-h-[calc(100vh-2rem)]">
193
131
  {/* Matches App > div.antialiased > div.flex > Routes > Content */}
@@ -197,27 +135,11 @@ export async function render(routePath: string, frontmatter?: Record<string, any
197
135
  </div>
198
136
  </div>
199
137
  </div>
200
- {(prevPage || nextPage) && (
201
- <div class="mt-12 pt-6 flex border-t border-black/20 dark:border-white/20">
202
- {prevPage && (
203
- <div>
204
- <p class="font-display text-sm font-medium">Previous</p>
205
- <div class="mt-1">
206
- <a href={withBase(prevPage.href)} class="text-base font-semibold hover:opacity-80">{prevPage.title}</a>
207
- </div>
208
- </div>
209
- )}
210
- {nextPage && (
211
- <div class="ml-auto text-right">
212
- <p class="font-display text-sm font-medium">Next</p>
213
- <div class="mt-1">
214
- <a href={withBase(nextPage.href)} class="text-base font-semibold hover:opacity-80">{nextPage.title}</a>
215
- </div>
216
- </div>
217
- )}
218
- </div>
219
- )}
138
+ <PageFeedback pathname={routePath} />
139
+ <PrevNextLinks navigation={navigation} />
140
+ <PageFooter />
220
141
  </article>
142
+ {!isOpenAPIPage && <TableOfContents />}
221
143
  </div>
222
144
  </main>
223
145
  </div>
package/src/main.tsx CHANGED
@@ -2,6 +2,7 @@ import { render, hydrate } from 'preact'
2
2
  import { Router } from 'wouter-preact'
3
3
  import '@clidey/ux/styles.css';
4
4
  import '@clidey/ux/brand.css';
5
+ import '@clidey/ux/theme-fonts.css';
5
6
  import { App } from './app.tsx'
6
7
  import { withMDX } from './components/mdx-provider.tsx'
7
8
  import { ErrorBoundary } from './components/error-boundary.tsx'
@@ -13,7 +14,7 @@ import { ThemeProvider } from '@clidey/ux'
13
14
  import { preloadFrontmatter } from './components/store'
14
15
  import docsConfig from '../docs/dory.json' with { type: 'json' };
15
16
  import type { DoryConfig } from './types/config';
16
- import { BASE_PATH } from './utils/base-path';
17
+ import { BASE_PATH, withBase } from './utils/base-path';
17
18
 
18
19
  const config = docsConfig as DoryConfig;
19
20
 
@@ -29,11 +30,19 @@ const AppWithMDX = withMDX(App)
29
30
 
30
31
  const appRoot = document.getElementById('app')!
31
32
  const hasSSRContent = appRoot.children.length > 0
33
+ const currentPath = window.location.pathname.replace(/\/$/, '') || '/'
34
+ const renderedPath = window.__DORY_ROUTE__ ? withBase(window.__DORY_ROUTE__).replace(/\/$/, '') || '/' : undefined
35
+ const rootPath = withBase('/').replace(/\/$/, '') || '/'
36
+ const canHydrate = hasSSRContent && (!renderedPath || currentPath === renderedPath || currentPath === rootPath)
32
37
 
33
- const defaultTheme = config.theme?.mode === 'dark' ? 'dark' : config.theme?.mode === 'light' ? 'light' : 'system';
38
+ const embedParams = new URLSearchParams(window.location.search);
39
+ const requestedTheme = embedParams.get('embedded') === 'true' ? embedParams.get('theme') : null;
40
+ const embedTheme = requestedTheme === 'dark' || requestedTheme === 'light' ? requestedTheme : null;
41
+ const defaultTheme = embedTheme ?? (config.theme?.mode === 'dark' ? 'dark' : config.theme?.mode === 'light' ? 'light' : 'system');
42
+ if (embedTheme) document.documentElement.style.colorScheme = embedTheme;
34
43
 
35
44
  const tree = (
36
- <ThemeProvider defaultTheme={defaultTheme} storageKey="@clidey/dory/theme">
45
+ <ThemeProvider defaultTheme={defaultTheme} storageKey={embedTheme ? `@clidey/dory/embed-theme-${embedTheme}` : '@clidey/dory/theme'}>
37
46
  <Toaster />
38
47
  <ErrorBoundary>
39
48
  <Router base={BASE_PATH}>
@@ -45,8 +54,11 @@ const tree = (
45
54
 
46
55
  // If SSR content exists, hydrate (attach JS to existing DOM).
47
56
  // Otherwise, render from scratch (dev mode, non-SSR pages).
48
- if (hasSSRContent) {
57
+ if (canHydrate) {
49
58
  hydrate(tree, appRoot)
50
59
  } else {
60
+ // A static host may serve the prerendered index.html for an unknown URL.
61
+ // Its first-page markup cannot hydrate into the 404 view.
62
+ if (hasSSRContent) appRoot.replaceChildren()
51
63
  render(tree, appRoot)
52
64
  }
@@ -1,11 +1,11 @@
1
- import {
1
+ import { CatalogIcon,
2
2
  Accordion as UXAccordion,
3
3
  AccordionItem,
4
4
  AccordionTrigger,
5
5
  AccordionContent
6
6
  } from '@clidey/ux'
7
7
  import classNames from 'classnames'
8
- import { Plus } from 'lucide-react'
8
+
9
9
  import { Icon } from './icon'
10
10
 
11
11
  export function AccordionGroup({ children, numbered = false }: { children: React.ReactNode; numbered?: boolean }) {
@@ -32,7 +32,7 @@ export function Accordion({
32
32
  {icon && <Icon icon={icon} className="h-3.5 w-3.5 !stroke-[var(--primary)] text-[var(--primary)]" />}
33
33
  <span className="text-sm font-semibold">{title}</span>
34
34
  </div>
35
- <Plus aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-45" />
35
+ <CatalogIcon name="plus" aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-45" />
36
36
  </AccordionTrigger>
37
37
  <AccordionContent>
38
38
  <div className="px-4 pb-3 leading-relaxed text-muted-foreground">
@@ -1,8 +1,11 @@
1
- import { Button, TextArea, useTheme, Input } from '@clidey/ux'
1
+ import { CatalogIcon, Button, TextArea, useTheme, Input as UxInput } from '@clidey/ux'
2
+
3
+ // The ux Input is typed against React's input props, which Preact 11's compat types no longer expose.
4
+ const Input = UxInput as unknown as (props: preact.JSX.IntrinsicElements['input']) => preact.JSX.Element
2
5
  import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
3
- import { CheckIcon } from '@heroicons/react/24/outline'
6
+
4
7
  import classNames from 'classnames'
5
- import { PencilIcon, PlusIcon, TrashIcon, XIcon } from 'lucide-react'
8
+
6
9
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
7
10
  import { Highlight, themes } from 'prism-react-renderer'
8
11
  import Dropdown from '../components/dropdown'
@@ -176,10 +179,10 @@ export const HeadersTab = ({ requestConfig, setRequestConfig }: HeadersTabProps)
176
179
  className="flex-1 text-sm"
177
180
  />
178
181
  <Button onClick={() => saveHeader(idx)}>
179
- <CheckIcon className="w-4 h-4" />
182
+ <CatalogIcon name="check" className="w-4 h-4" />
180
183
  </Button>
181
184
  <Button onClick={() => cancelEdit(idx)}>
182
- <XIcon className="w-4 h-4" />
185
+ <CatalogIcon name="close" className="w-4 h-4" />
183
186
  </Button>
184
187
  </>
185
188
  ) : (
@@ -187,17 +190,17 @@ export const HeadersTab = ({ requestConfig, setRequestConfig }: HeadersTabProps)
187
190
  <span className="text-sm w-32 truncate">{hdr.key}</span>
188
191
  <span className="text-sm flex-1 truncate">{hdr.value}</span>
189
192
  <Button onClick={() => editHeader(idx)}>
190
- <PencilIcon className="w-4 h-4" />
193
+ <CatalogIcon name="edit" className="w-4 h-4" />
191
194
  </Button>
192
195
  <Button onClick={() => removeHeader(idx)} className="text-red-400">
193
- <TrashIcon className="w-4 h-4" />
196
+ <CatalogIcon name="trash" className="w-4 h-4" />
194
197
  </Button>
195
198
  </>
196
199
  )}
197
200
  </div>
198
201
  ))}
199
202
  <Button onClick={addEmptyHeaderRow} className="text-sm mt-2 w-fit">
200
- <PlusIcon className="w-4 h-4" /> Add Header
203
+ <CatalogIcon name="plus" className="w-4 h-4" /> Add Header
201
204
  </Button>
202
205
  </div>
203
206
  )
@@ -284,10 +287,10 @@ export const ParamsTab = ({ queryParams, setQueryParams }: ParamsTabProps) => {
284
287
  <Input value={p.name} onChange={e => updateParam(i, 'name', (e.target as HTMLInputElement).value)} placeholder="Param name" className="w-32 text-sm" />
285
288
  <Input value={p.value} onChange={e => updateParam(i, 'value', (e.target as HTMLInputElement).value)} placeholder="Param value" className="flex-1 text-sm" />
286
289
  <Button onClick={() => saveParam(i)}>
287
- <CheckIcon className="w-4 h-4" />
290
+ <CatalogIcon name="check" className="w-4 h-4" />
288
291
  </Button>
289
292
  <Button onClick={() => cancelEdit(i)}>
290
- <XIcon className="w-4 h-4" />
293
+ <CatalogIcon name="close" className="w-4 h-4" />
291
294
  </Button>
292
295
  </>
293
296
  ) : (
@@ -295,23 +298,22 @@ export const ParamsTab = ({ queryParams, setQueryParams }: ParamsTabProps) => {
295
298
  <span className="w-32 truncate text-sm">{p.name}</span>
296
299
  <span className="flex-1 truncate text-sm">{p.value}</span>
297
300
  <Button onClick={() => editParam(i)}>
298
- <PencilIcon className="w-4 h-4" />
301
+ <CatalogIcon name="edit" className="w-4 h-4" />
299
302
  </Button>
300
303
  <Button onClick={() => removeParam(i)} className="text-red-400">
301
- <TrashIcon className="w-4 h-4" />
304
+ <CatalogIcon name="trash" className="w-4 h-4" />
302
305
  </Button>
303
306
  </>
304
307
  )}
305
308
  </div>
306
309
  ))}
307
310
  <Button onClick={addParamRow} className="mt-2 text-sm self-end">
308
- <PlusIcon className="w-4 h-4" /> Add Parameter
311
+ <CatalogIcon name="plus" className="w-4 h-4" /> Add Parameter
309
312
  </Button>
310
313
  </div>
311
314
  );
312
315
  };
313
316
 
314
-
315
317
  export function APIPlayground({
316
318
  method,
317
319
  url,
@@ -596,7 +598,7 @@ export function APIPlayground({
596
598
  {/* Response */}
597
599
  <div className="flex-1">
598
600
  <div className="px-4 py-3">
599
- <h3 className="text-sm font-semibold">Response</h3>
601
+ <h3 className="text-sm font-semibold" data-toc-ignore>Response</h3>
600
602
  </div>
601
603
 
602
604
  <div className="p-4">
@@ -659,4 +661,4 @@ export function APIPlayground({
659
661
  </div>
660
662
  </div>
661
663
  )
662
- }
664
+ }
@@ -131,7 +131,7 @@ export function AsyncAPI({
131
131
 
132
132
  return (
133
133
  <div className="dory-asyncapi max-w-none w-full">
134
- {(operation.description || operation.summary) && <p className="dory-lead -mt-4 mb-6">{operation.description || operation.summary}</p>}
134
+ {(operation.description || operation.summary) && <p className="dory-lead -mt-4! mb-6">{operation.description || operation.summary}</p>}
135
135
 
136
136
  <h2 id="connect" className="dory-h2 font-heading mb-3 mt-10"><span>Connect</span></h2>
137
137
  <WebSocketPanel url={channel.address ?? selectedChannel} servers={wsServers} headers={defaultHeaders} />
@@ -150,7 +150,7 @@ export function AsyncAPI({
150
150
  <tbody>
151
151
  {rows.map((row) => (
152
152
  <tr key={`${row.direction}-${row.name}`}>
153
- <Td><span className="font-mono text-xs">{row.name}</span></Td>
153
+ <Td><span className="font-mono text-[13px] text-foreground">{row.name}</span></Td>
154
154
  <Td>
155
155
  <span className={row.direction === 'SEND' ? 'font-mono text-xs font-semibold text-[var(--brand-readable,#b65401)]' : 'font-mono text-xs font-semibold text-[var(--primary)]'}>
156
156
  {row.direction}
package/src/mdx/card.tsx CHANGED
@@ -2,6 +2,7 @@ import classNames from "classnames";
2
2
  import { withBase } from '../utils/base-path';
3
3
  import { Icon } from "./icon";
4
4
  import type { ComponentChildren } from "preact";
5
+ import { Card as UxCard } from '@clidey/ux';
5
6
 
6
7
  export function CardGroup({ children, cols = 2, numbered = false }: { children: React.ReactNode; cols?: number; numbered?: boolean }) {
7
8
  return (
@@ -31,7 +32,7 @@ export function Card({
31
32
  children?: ComponentChildren
32
33
  }) {
33
34
  return (
34
- <div className="dory-card group relative flex flex-col rounded-lg border border-[var(--border)] bg-card p-4 transition-shadow hover:shadow-[0_2px_10px_rgb(14_34_64/0.07)]">
35
+ <UxCard className="dory-card group relative gap-0 rounded-lg border-[var(--border)] bg-card p-4 shadow-none transition-shadow hover:shadow-[0_2px_10px_rgb(14_34_64/0.07)]">
35
36
  {href && <a href={withBase(href)} className="absolute inset-0 z-10 rounded-lg" aria-label={title} />}
36
37
  {(icon || href) && (
37
38
  <div className="mb-3 flex items-start justify-between">
@@ -47,6 +48,6 @@ export function Card({
47
48
  <div className="mt-1 text-[13px] leading-normal text-muted-foreground [&_p]:mb-0">
48
49
  {description ?? children}
49
50
  </div>
50
- </div>
51
+ </UxCard>
51
52
  )
52
53
  }
package/src/mdx/code.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
2
- import { ClipboardIcon } from '@heroicons/react/24/outline'
2
+
3
3
  import classNames from 'classnames'
4
4
  import {
5
5
  Children,
@@ -8,12 +8,13 @@ import {
8
8
  useEffect,
9
9
  useState
10
10
  } from 'react'
11
- import { File, SquareTerminal } from 'lucide-react'
11
+
12
12
  import { Tag } from './tag'
13
13
  import { diffBaseLanguage, isTerminalSession, parseFenceMeta, type FenceMeta } from './fence-meta'
14
14
  import { toTitleCase } from '../utils/functions'
15
- import { Button } from '@clidey/ux'
16
- import { CheckIcon } from 'lucide-react'
15
+ import { CatalogIcon, Button } from '@clidey/ux'
16
+
17
+ import { captureDocsEvent } from '../utils/analytics'
17
18
 
18
19
  export const supportedLanguages = {
19
20
  'python': 'Python',
@@ -87,7 +88,7 @@ function languageName(language: string) {
87
88
  return supportedLanguages[language as keyof typeof supportedLanguages] || toTitleCase(language)
88
89
  }
89
90
 
90
- function CopyButton({ code }: { code: string }) {
91
+ export function CopyButton({ code }: { code: string }) {
91
92
  const [copyCount, setCopyCount] = useState(0)
92
93
  const copied = copyCount > 0
93
94
 
@@ -111,6 +112,7 @@ function CopyButton({ code }: { code: string }) {
111
112
  onClick={() => {
112
113
  window.navigator.clipboard.writeText(code).then(() => {
113
114
  setCopyCount((count) => count + 1)
115
+ captureDocsEvent('docs_code_copied', { path: window.location.pathname })
114
116
  })
115
117
  }}
116
118
  >
@@ -121,7 +123,7 @@ function CopyButton({ code }: { code: string }) {
121
123
  copied && '-translate-y-1.5 opacity-0',
122
124
  )}
123
125
  >
124
- <ClipboardIcon className="h-3.5 w-3.5 stroke-muted-foreground transition-colors group-hover/button:stroke-foreground" />
126
+ <CatalogIcon name="clipboard" className="h-3.5 w-3.5 stroke-muted-foreground transition-colors group-hover/button:stroke-foreground" />
125
127
  </span>
126
128
  <span
127
129
  aria-hidden={!copied}
@@ -130,7 +132,7 @@ function CopyButton({ code }: { code: string }) {
130
132
  !copied && 'translate-y-1.5 opacity-0',
131
133
  )}
132
134
  >
133
- <CheckIcon className="h-3.5 w-3.5" />
135
+ <CatalogIcon name="check" className="h-3.5 w-3.5" />
134
136
  </span>
135
137
  </Button>
136
138
  )
@@ -161,8 +163,8 @@ export function FenceHeader({ language, code, meta }: FenceProps) {
161
163
  return (
162
164
  <div className="flex min-h-10 items-center gap-2 border-b border-[var(--border)] bg-muted pl-4 pr-12">
163
165
  {terminal
164
- ? <SquareTerminal aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
165
- : meta.title && <File aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
166
+ ? <CatalogIcon name="terminal" aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
167
+ : meta.title && <CatalogIcon name="file" aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
166
168
  <p className="mr-auto truncate font-mono text-xs font-medium text-foreground">{title}</p>
167
169
  {diff && (
168
170
  <span className="font-mono text-xs">
package/src/mdx/fence.tsx CHANGED
@@ -8,13 +8,14 @@ import classNames from 'classnames'
8
8
 
9
9
  // Colors resolve through CSS variables so one theme serves both light and dark mode.
10
10
  const codeTheme: PrismTheme = {
11
- plain: { color: 'var(--foreground)', backgroundColor: 'transparent' },
11
+ plain: { color: 'var(--syntax-plain)', backgroundColor: 'transparent' },
12
12
  styles: [
13
- { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--muted-foreground)', fontStyle: 'italic' } },
14
- { types: ['keyword', 'builtin', 'function', 'tag', 'selector', 'number', 'boolean', 'constant', 'property', 'class-name'], style: { color: 'var(--primary)' } },
15
- { types: ['string', 'char', 'attr-value', 'url', 'regex', 'inserted'], style: { color: 'var(--dory-code-string)' } },
13
+ { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--syntax-comment)', fontStyle: 'italic' } },
14
+ { types: ['keyword', 'builtin', 'function', 'tag', 'selector', 'property', 'class-name'], style: { color: 'var(--syntax-keyword)' } },
15
+ { types: ['number', 'boolean', 'constant'], style: { color: 'var(--syntax-number)' } },
16
+ { types: ['string', 'char', 'attr-value', 'url', 'regex', 'inserted'], style: { color: 'var(--syntax-string)' } },
16
17
  { types: ['deleted'], style: { color: 'var(--destructive)' } },
17
- { types: ['punctuation', 'operator'], style: { color: 'var(--foreground)' } },
18
+ { types: ['punctuation', 'operator'], style: { color: 'var(--syntax-plain)' } },
18
19
  ],
19
20
  }
20
21
 
@@ -1,8 +1,11 @@
1
1
  import { useNotification } from '../components/notification'
2
2
  import { Icon } from './icon'
3
+ import { icons as catalog } from '@clidey/ux/icons'
4
+
5
+ const iconNames = Object.keys(catalog).sort()
3
6
 
4
7
  /** Grid of icon tiles. Clicking a tile copies its `<Icon icon="…" />` tag. */
5
- export function IconGrid({ icons, cols = 4 }: { icons: string[]; cols?: number }) {
8
+ export function IconGrid({ icons = iconNames, cols = 4 }: { icons?: string[]; cols?: number }) {
6
9
  const { showNotification } = useNotification()
7
10
 
8
11
  const copy = (name: string) => {
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolveIconName } from './icon-names'
3
+
4
+ describe('MDX icon name compatibility', () => {
5
+ it('keeps source-prefixed variants distinct from the default icon', () => {
6
+ expect(resolveIconName('chevron-down')).toBe('chevron-down')
7
+ expect(resolveIconName('enterprise/chevron-down')).toBe('chevron-down-alt-2')
8
+ })
9
+
10
+ it('preserves legacy document names', () => {
11
+ expect(resolveIconName('Search')).toBe('search')
12
+ expect(resolveIconName('alert-triangle')).toBe('warning')
13
+ expect(resolveIconName('sparkles')).toBe('ai')
14
+ })
15
+ })