@clidey/dory 1.0.7 → 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
@@ -40,9 +40,15 @@ export interface DoryNavigationPage {
40
40
  [key: string]: string;
41
41
  }
42
42
 
43
+ /** Page entry with extras, e.g. { "page": "components/overview", "badge": "New" }. */
44
+ export interface DoryNavigationPageEntry {
45
+ page: string;
46
+ badge?: string;
47
+ }
48
+
43
49
  export interface DoryNavigationGroup {
44
50
  group: string;
45
- pages: string[];
51
+ pages: Array<string | DoryNavigationPageEntry>;
46
52
  }
47
53
 
48
54
  export interface DoryNavigationTab {
@@ -109,6 +115,8 @@ export interface DorySeo {
109
115
  author?: string;
110
116
  /** Absolute URL or path resolved against config.url. */
111
117
  ogImage?: string;
118
+ /** Generate a branded PNG for each page at build time. Default: true. */
119
+ generateSocialImages?: boolean;
112
120
  twitterCard?: 'summary' | 'summary_large_image';
113
121
  /** e.g. "en_US" */
114
122
  locale?: string;
@@ -123,6 +131,40 @@ export interface DoryTheme {
123
131
  radius?: string;
124
132
  }
125
133
 
134
+ /** One entry in the sidebar version picker. */
135
+ export interface DoryVersion {
136
+ label: string;
137
+ /** Local docs folder and URL prefix, e.g. "v1" for docs/v1/*.mdx. */
138
+ id?: string;
139
+ /** External published site; retained for existing configurations. */
140
+ url?: string;
141
+ navigation?: DoryNavigation;
142
+ }
143
+
144
+ export interface DoryLocale {
145
+ code: string;
146
+ label: string;
147
+ /** Optional translated tab and group labels; page keys remain the same. */
148
+ navigation?: DoryNavigation;
149
+ /** Navigation for translated older versions, keyed by version ID. */
150
+ versions?: Record<string, DoryNavigation>;
151
+ }
152
+
153
+ export interface DoryI18n {
154
+ defaultLocale: string;
155
+ locales: DoryLocale[];
156
+ }
157
+
158
+ /** Source repository for "Edit this page" and "Report an issue" links. GitHub-style URLs. */
159
+ export interface DoryRepository {
160
+ /** Repository URL, e.g. "https://github.com/clidey/dory". */
161
+ url: string;
162
+ /** Branch the edit link opens. Default: "main". */
163
+ branch?: string;
164
+ /** Folder holding the docs inside the repository, e.g. "docs". Default: the repository root. */
165
+ path?: string;
166
+ }
167
+
126
168
  export interface DoryConfig {
127
169
  name: string;
128
170
  title?: string;
@@ -132,6 +174,15 @@ export interface DoryConfig {
132
174
  url?: string;
133
175
  /** Path prefix the site is served under, e.g. "/docs". Default: served at the root. */
134
176
  basePath?: string;
177
+ /** Shown in the sidebar version card, e.g. "1.0.8 · Latest". */
178
+ version?: string;
179
+ /** Other published versions; when set, the version card becomes a picker that opens the chosen URL. */
180
+ versions?: DoryVersion[];
181
+ /** Place translations in docs/<locale>/ with the same page keys. */
182
+ i18n?: DoryI18n;
183
+ /** Name in the footer copyright line. Default: `name`. */
184
+ copyright?: string;
185
+ repository?: DoryRepository;
135
186
  twitter?: string;
136
187
  favicon: string;
137
188
  colors?: DoryColors;
@@ -141,5 +192,6 @@ export interface DoryConfig {
141
192
  logo: DoryLogo;
142
193
  embed?: DoryEmbedConfig;
143
194
  analytics?: DoryAnalytics;
195
+ chat?: { suggestedQuestions?: string[] };
144
196
  seo?: DorySeo;
145
197
  }
@@ -0,0 +1,17 @@
1
+ /** Mono "Docs / Group / Page" trail above the page title. */
2
+ export function Breadcrumb({ group, title }: { group?: string; title?: string }) {
3
+ const parts = ['Docs', group, title].filter((part): part is string => Boolean(part))
4
+
5
+ return (
6
+ <nav aria-label="Breadcrumb" className="dory-breadcrumb">
7
+ <ol className="flex flex-wrap items-center gap-2">
8
+ {parts.map((part, index) => (
9
+ <li key={index} className="flex items-center gap-2 whitespace-nowrap" aria-current={index === parts.length - 1 ? 'page' : undefined}>
10
+ {index > 0 && <span aria-hidden="true">/</span>}
11
+ <span>{part}</span>
12
+ </li>
13
+ ))}
14
+ </ol>
15
+ </nav>
16
+ )
17
+ }
@@ -0,0 +1,32 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { Badge } from '@clidey/ux';
3
+
4
+ export type HeaderTab = { title: string; href: string; active: boolean };
5
+
6
+ type LinkRenderer = (href: string, className: string, children: ComponentChildren) => ComponentChildren;
7
+
8
+ /** The top bar shared by static Dory sites and runtime-loaded documentation. */
9
+ export function DocsHeaderView({ name, logo, homeHref, tabs, renderLink, actions, docsLabel = 'Docs', sectionsLabel = 'Sections', homeClassName, embedded = false }: {
10
+ name: string;
11
+ logo?: string;
12
+ homeHref: string;
13
+ tabs: HeaderTab[];
14
+ renderLink: LinkRenderer;
15
+ actions?: ComponentChildren;
16
+ docsLabel?: string;
17
+ sectionsLabel?: string;
18
+ homeClassName?: string;
19
+ embedded?: boolean;
20
+ }) {
21
+ return <header className="dory-topbar sticky top-0 z-50 flex h-14 shrink-0 items-stretch bg-[var(--sidebar)] text-white">
22
+ {renderLink(homeHref, homeClassName ?? 'flex w-60 shrink-0 items-center gap-2.5 px-4', <>
23
+ {logo && <img data-testid={embedded ? 'embedded-logo' : undefined} src={logo} alt="" className="h-7 w-7" />}
24
+ <span data-testid={embedded ? 'embedded-site-name' : undefined} className="font-heading text-lg font-bold tracking-tight text-white">{name}</span>
25
+ <Badge variant="outline" className="dory-docs-pill">{docsLabel}</Badge>
26
+ </>)}
27
+ <nav className="hidden md:flex items-stretch" aria-label={sectionsLabel}>
28
+ {tabs.map(tab => renderLink(tab.href, `dory-topbar-tab${tab.active ? ' active' : ''}`, tab.title))}
29
+ </nav>
30
+ <div className="ml-auto flex items-center gap-3 pr-4" data-testid="header-actions">{actions}</div>
31
+ </header>;
32
+ }
@@ -0,0 +1,69 @@
1
+ import classNames from 'classnames';
2
+ import type { ComponentChildren } from 'preact';
3
+ import { useEffect, useRef } from 'preact/hooks';
4
+ import { Badge } from '@clidey/ux';
5
+ import { DORY_REPO_URL, DoryMark } from './dory-mark';
6
+
7
+ export type NavigationPage = { href: string; title: string; badge?: string; method?: string };
8
+ export type NavigationGroup = { title: string; pages: NavigationPage[] };
9
+
10
+ const methodTone: Record<string, string> = {
11
+ GET: 'text-[var(--sidebar-primary)]',
12
+ POST: 'text-[var(--highlight)]',
13
+ PUT: 'text-[#f4781c]',
14
+ PATCH: 'text-[#f4781c]',
15
+ DEL: 'text-[#ff6668]',
16
+ PUB: 'text-[#f4781c]',
17
+ SUB: 'text-[var(--sidebar-primary)]',
18
+ WS: 'text-[var(--sidebar-primary)]',
19
+ };
20
+
21
+ type PageLinkRenderer = (page: NavigationPage, className: string, active: boolean, children: ComponentChildren) => ComponentChildren;
22
+
23
+ /** The navigation tree shared by static Dory sites and runtime-loaded documentation. */
24
+ export function DocsNavigationView({ groups, activeHref, renderPageLink, versionLabel, versionSelect, languageSelect, ariaLabel = 'Documentation', versionCaption = 'Version', footerLabel = 'Built with Dory' }: {
25
+ groups: NavigationGroup[];
26
+ activeHref: string;
27
+ renderPageLink: PageLinkRenderer;
28
+ versionLabel?: string;
29
+ versionSelect?: ComponentChildren;
30
+ languageSelect?: ComponentChildren;
31
+ ariaLabel?: string;
32
+ versionCaption?: string;
33
+ footerLabel?: string;
34
+ }) {
35
+ const navRef = useRef<HTMLElement>(null);
36
+ useEffect(() => {
37
+ const nav = navRef.current;
38
+ const container = nav?.parentElement;
39
+ const active = nav?.querySelector('a[aria-current="page"], button[aria-current="page"]');
40
+ if (!container || !active) return;
41
+ const item = active.getBoundingClientRect();
42
+ const viewport = container.getBoundingClientRect();
43
+ if (item.top < viewport.top) container.scrollTop += item.top - viewport.top;
44
+ else if (item.bottom > viewport.bottom) container.scrollTop += item.bottom - viewport.bottom;
45
+ }, [activeHref]);
46
+
47
+ return <nav ref={navRef} className="dory-sidebar flex min-h-full flex-col gap-6 text-[13px]" aria-label={ariaLabel}>
48
+ {versionLabel && <div className="relative flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-3 py-2.5">
49
+ <div><p className="dory-mono-label">{versionCaption}</p><p className="mt-0.5 text-sm font-semibold text-white">{versionLabel}</p></div>
50
+ {versionSelect}
51
+ </div>}
52
+ {languageSelect}
53
+ {groups.map((group, index) => <section key={`${group.title}-${index}`}>
54
+ <h3 className="dory-mono-label flex gap-2 px-2.5 !text-[var(--sidebar-primary)]">
55
+ <span>{String(index + 1).padStart(2, '0')}</span><span className="text-white/55">{group.title}</span>
56
+ </h3>
57
+ <ul role="list" className="mt-2 space-y-0.5">{group.pages.map(page => {
58
+ const isActive = activeHref === page.href;
59
+ const contents = <>
60
+ {page.method && <span className={classNames('dory-method', methodTone[page.method] ?? 'text-white/60')}>{page.method}</span>}
61
+ <span className="truncate">{page.title}</span>
62
+ {page.badge && <Badge variant="secondary" className="dory-badge">{page.badge}</Badge>}
63
+ </>;
64
+ return <li key={page.href}>{renderPageLink(page, classNames('dory-sidebar-link', { active: isActive }), isActive, contents)}</li>;
65
+ })}</ul>
66
+ </section>)}
67
+ <a href={DORY_REPO_URL} target="_blank" rel="noopener noreferrer" className="dory-sidebar-footer mt-auto flex items-center gap-2.5 border-t border-white/10 px-2.5 pt-4"><DoryMark className="h-4 w-4" /><span className="dory-mono-label">{footerLabel}</span></a>
68
+ </nav>;
69
+ }
@@ -0,0 +1,50 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { cn } from '@clidey/ux';
3
+
4
+ type DocsPageFrameProps = {
5
+ className?: string;
6
+ header: ComponentChildren;
7
+ sidebar?: ComponentChildren;
8
+ breadcrumb: ComponentChildren;
9
+ actions?: ComponentChildren;
10
+ title?: string;
11
+ lead?: string;
12
+ meta?: ComponentChildren;
13
+ content: ComponentChildren;
14
+ afterContent?: ComponentChildren;
15
+ toc?: ComponentChildren;
16
+ embedded?: boolean;
17
+ wide?: boolean;
18
+ };
19
+
20
+ /** The page structure shared by a Dory site and its sandboxed app viewer. */
21
+ export function DocsPageFrame({ className, header, sidebar, breadcrumb, actions, title, lead, meta, content, afterContent, toc, embedded = false, wide = false }: DocsPageFrameProps) {
22
+ return <div className={cn('dory-shell flex min-h-screen w-full flex-col bg-[var(--sidebar)]', className)}>
23
+ {header}
24
+ <div className="flex flex-1">
25
+ {sidebar && <aside className="hidden w-60 shrink-0 lg:block">
26
+ <div className="sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto px-3 pb-6 pt-4">
27
+ {sidebar}
28
+ </div>
29
+ </aside>}
30
+ <main className={cn('dory-main min-w-0 flex-1 bg-background text-foreground', embedded ? 'p-8' : 'px-6 py-10 sm:px-10 lg:px-12')}>
31
+ <div className="mx-auto flex w-full max-w-[66rem] justify-center gap-14">
32
+ <article className={cn('dory-article flex min-w-0 flex-1 flex-col', wide ? 'max-w-[66rem]' : 'max-w-[47rem]')}>
33
+ <div className="mb-6 flex min-h-9 flex-wrap items-center justify-between gap-4">
34
+ {breadcrumb}
35
+ {actions}
36
+ </div>
37
+ <header className="mb-10">
38
+ {title && <h1 className="dory-page-title">{title}</h1>}
39
+ {lead && <p className="dory-lead">{lead}</p>}
40
+ </header>
41
+ {meta}
42
+ <div className="flex-1 min-h-[calc(100vh-2rem)]">{content}</div>
43
+ {afterContent}
44
+ </article>
45
+ {toc}
46
+ </div>
47
+ </main>
48
+ </div>
49
+ </div>;
50
+ }
@@ -0,0 +1,9 @@
1
+ const mark = "<svg width=\"22\" height=\"24\" viewBox=\"0 0 22 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21.9941 4.99951C19.9947 3.00079 19.9947 3.00079 16.9956 0.00271368V4.99951H21.9941Z\" fill=\"#92BFFA\"/>\n<path d=\"M7.49782 3.91843C7.49782 4.5155 7.98183 4.99951 8.5789 4.99951H16.9951V0.00271606H8.5789C7.98183 0.00271606 7.49782 0.486733 7.49782 1.0838V3.91843Z\" fill=\"url(#paint0_linear_66_680)\"/>\n<path d=\"M21.9932 5.01123C22.0049 8.20987 21.9983 17.7036 21.9951 21.9995C21.9943 23.1041 21.0977 23.9995 19.9932 23.9995H2C0.895431 23.9995 3.87055e-07 23.1041 0 21.9995V8.59131C0 7.99426 0.48401 7.51028 1.08105 7.51025H3.91797C4.51481 7.51052 4.99902 7.99441 4.99902 8.59131V19.5024H16.9951V5.01123H21.9932Z\" fill=\"url(#paint1_linear_66_680)\"/>\n<ellipse cx=\"12.3217\" cy=\"14.276\" rx=\"1.2973\" ry=\"1.29647\" fill=\"#1967D2\"/>\n<path d=\"M5 0.00634813C5.55157 0.00730227 5.99805 0.454718 5.99805 1.00635V4.01221C5.99805 4.64068 6.29707 5.23179 6.80273 5.60498L7.46777 6.09619C7.81171 6.35003 8.22878 6.48682 8.65625 6.48682H14.4951C15.0472 6.48682 15.4949 6.93475 15.4951 7.48682V16.98C15.4951 17.5322 15.0474 17.98 14.4951 17.98H7.49805C6.94583 17.9799 6.49808 17.5322 6.49805 16.98V8.67139C6.49806 8.23724 6.35689 7.81407 6.0957 7.46729L5.58691 6.79248C5.21558 6.29954 4.63759 6.00515 4.02051 5.99561H1C0.447722 5.9956 0 5.54789 0 4.99561V1.00049C0.000165845 0.447951 0.448441 -4.60729e-05 1.00098 0.00048876L5 0.00634813Z\" fill=\"#FFC236\"/>\n<ellipse cx=\"12.9961\" cy=\"15.505\" rx=\"1\" ry=\"0.999359\" fill=\"#3630C4\"/>\n<defs>\n<linearGradient id=\"paint0_linear_66_680\" x1=\"10.9999\" y1=\"0.00271606\" x2=\"10.9999\" y2=\"23.9995\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#528FF4\"/>\n<stop offset=\"1\" stop-color=\"#92BFFA\"/>\n</linearGradient>\n<linearGradient id=\"paint1_linear_66_680\" x1=\"10.9999\" y1=\"0.00271606\" x2=\"10.9999\" y2=\"23.9995\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#528FF4\"/>\n<stop offset=\"1\" stop-color=\"#92BFFA\"/>\n</linearGradient>\n</defs>\n</svg>"
2
+
3
+ /** Dory project page, linked from the "Built with Dory" and "Powered by Dory" credits. */
4
+ export const DORY_REPO_URL = 'https://github.com/clidey/dory'
5
+
6
+ /** Dory logo mark for the "Built with Dory" footer. */
7
+ export function DoryMark({ className }: { className?: string }) {
8
+ return <img src={`data:image/svg+xml;utf8,${encodeURIComponent(mark)}`} alt="" className={className} />
9
+ }
package/src/ui/header.tsx CHANGED
@@ -1,110 +1,53 @@
1
- import classNames from 'classnames';
2
- import { useEffect, useMemo, useState } from 'preact/hooks';
1
+ import type { ComponentChildren } from 'preact';
2
+ import { useMemo } from 'preact/hooks';
3
3
  import { Link } from 'wouter-preact';
4
4
  import { usePathname } from '../components/hooks';
5
5
  import docsConfig from '../../docs/dory.json' with { type: 'json' };
6
6
  import type { DoryConfig } from '../types/config';
7
7
  import { MobileSearch, Search } from '../components/search';
8
- import { ALL_NAVIGATION } from '../components/store';
8
+ import { getNavigationForPath } from '../components/store';
9
9
  import { MobileNavigation } from './mobile-navigation';
10
10
  import { ModeToggle } from '@clidey/ux';
11
11
  import { useIsEmbedded } from '../components/hooks';
12
12
  import { assetUrl } from '../utils/base-path';
13
+ import { DocsHeaderView } from './docs-header-view';
13
14
 
14
15
  const config = docsConfig as DoryConfig;
15
16
  const isThemeForced = config.theme?.mode === 'dark' || config.theme?.mode === 'light';
16
17
 
17
- function TopLevelNavItem({ href, children, isActive }: { href: string; children: string; isActive?: boolean }) {
18
- return (
19
- <li>
20
- <Link
21
- to={href}
22
- className={classNames(
23
- "text-xs transition hover:opacity-80",
24
- {
25
- "text-brand-foreground": isActive,
26
- },
27
- )}
28
- >
29
- {children}
30
- </Link>
31
- </li>
32
- );
33
- }
34
-
35
18
  /**
36
- * Site header with logo, top-level navigation, search, and theme toggle.
37
- *
38
- * SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this header's
39
- * structure (the <header> block inside render()). If you change the markup
40
- * structure here, update entry-server.tsx to match or hydration will mismatch.
19
+ * Dark top bar: logo and site name with a DOCS pill, one underlined tab per
20
+ * navigation tab, search with a ⌘K hint, theme toggle and the mobile menu.
21
+ * With `ssr` the client-only widgets (search, toggle, menu) are left out and
22
+ * mount during hydration.
41
23
  */
42
- export function Header({ className }: { className?: string }) {
43
- const [isScrolled, setIsScrolled] = useState(false);
24
+ export function Header({ ssr = false }: { ssr?: boolean }) {
44
25
  const pathname = usePathname();
26
+ const navigation = getNavigationForPath(pathname);
45
27
  const isEmbedded = useIsEmbedded();
46
-
47
- const { light, dark } = useMemo(() => {
48
- return { light: assetUrl(config.logo.light), dark: assetUrl(config.logo.dark) };
49
- }, []);
50
-
51
- useEffect(() => {
52
- function onScroll() {
53
- setIsScrolled(window.scrollY > 0);
54
- }
55
- onScroll();
56
- window.addEventListener('scroll', onScroll, { passive: true });
57
- return () => {
58
- window.removeEventListener('scroll', onScroll);
59
- }
60
- }, []);
28
+ const logo = useMemo(() => config.logo?.light ? assetUrl(config.logo.light) : '', []);
29
+ const renderLink = (href: string, className: string, children: ComponentChildren) =>
30
+ <Link key={href} href={href} className={className}>{children}</Link>;
61
31
 
62
32
  return (
63
- <header
64
- className={classNames(
65
- 'dory-header sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between px-4 py-5 sm:px-6 lg:px-8 border-b',
66
- className,
67
- {
68
- 'backdrop-blur-sm': isScrolled,
69
- },
70
- )}
71
- style={{ backgroundColor: 'var(--background)', borderColor: 'var(--border)' }}
72
- >
73
- <div className="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
74
- <div className="flex items-center grow gap-2">
75
- <img src={light} alt="logo" className="w-8 hidden dark:block" />
76
- <img src={dark} alt="logo" className="w-8 block dark:hidden" />
77
- <h1 className="text-base lg:text-2xl font-bold">{config.name}</h1>
78
- </div>
79
- <nav className="hidden md:block">
80
- <ul role="list" className="flex items-center gap-8">
81
- {ALL_NAVIGATION.map((tab) => {
82
- const isActive = tab.groups.some(group =>
83
- group.pages.some(page => page.href === pathname)
84
- );
85
- return (
86
- <TopLevelNavItem
87
- key={tab.title}
88
- href={tab.groups[0].pages[0].href}
89
- isActive={isActive}
90
- >
91
- {tab.title}
92
- </TopLevelNavItem>
93
- );
94
- })}
95
- </ul>
96
- </nav>
97
- <div className="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
98
- <div className="flex items-center gap-3" data-testid="header-actions">
99
- <MobileSearch />
100
- <Search />
101
- {
102
- !isEmbedded && !isThemeForced &&
103
- <ModeToggle data-testid="mode-toggle" />
104
- }
105
- <MobileNavigation />
106
- </div>
107
- </div>
108
- </header>
33
+ <DocsHeaderView name={config.name} logo={logo} homeHref={navigation[0]?.groups[0]?.pages[0]?.href ?? '/'}
34
+ tabs={navigation.map(tab => ({ title: tab.title, href: tab.groups[0].pages[0].href, active: tab.groups.some(group => group.pages.some(page => page.href === pathname)) }))}
35
+ renderLink={renderLink}
36
+ actions={!ssr && (
37
+ <>
38
+ <div className="dory-topbar-search relative hidden lg:block">
39
+ <Search />
40
+ <kbd className="dory-kbd" aria-hidden="true">⌘K</kbd>
41
+ </div>
42
+ <div className="lg:hidden [&_[data-slot=search-input-wrapper]]:w-9 [&_[data-slot=search-input-wrapper]]:border-transparent [&_[data-slot=search-input-wrapper]]:bg-white/8 [&_[data-slot=search-input-wrapper]]:px-2.5 [&_svg]:mr-0 [&_svg]:text-white/70 [&_input]:hidden">
43
+ <MobileSearch />
44
+ </div>
45
+ {!isEmbedded && !isThemeForced && (
46
+ <span className="dory-mode"><ModeToggle data-testid="mode-toggle" /></span>
47
+ )}
48
+ <MobileNavigation />
49
+ </>
50
+ )}
51
+ />
109
52
  );
110
- }
53
+ }