@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.
- package/README.md +12 -1
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dory.js +252 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +159 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +175 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/dory.json +129 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/getting-started/whodb-ee-apps.mdx +122 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-runtime-parity.mjs +146 -0
- package/src/app.css +19 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +16 -4
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.tsx +3 -2
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.ts +4 -96
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +38 -6
- package/src/mdx/tabs.tsx +29 -21
- package/src/mdx/text.tsx +9 -9
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +4 -4
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +1 -1
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +3 -3
- package/vite.config.ts +21 -15
package/src/ui/layout.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { Navigation } from '../components/navigation';
|
|
4
|
-
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady } from '../components/store';
|
|
4
|
+
import { ALL_NAVIGATION, ALL_PAGES, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady, getNavigationForPath } from '../components/store';
|
|
5
5
|
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
6
6
|
import type { DoryConfig } from '../types/config';
|
|
7
7
|
import { usePathname } from '../components/hooks';
|
|
@@ -9,33 +9,42 @@ import { OpenAPI } from '../mdx/open-api';
|
|
|
9
9
|
import { AsyncAPI } from '../mdx/async-api';
|
|
10
10
|
import { Header } from './header';
|
|
11
11
|
import { PrevNextLinks } from './prev-next-link';
|
|
12
|
-
import { TableOfContents } from './table-of-content';
|
|
13
12
|
import { PageMeta } from './page-meta';
|
|
13
|
+
import { Breadcrumb } from './breadcrumb';
|
|
14
|
+
import { PageActions } from './page-actions';
|
|
15
|
+
import { PageFeedback } from './page-feedback';
|
|
16
|
+
import { PageFooter } from './page-footer';
|
|
17
|
+
import { TableOfContents } from './table-of-content';
|
|
18
|
+
import { DocsPageFrame } from './docs-page-frame';
|
|
14
19
|
import { Loading } from '../components/loading';
|
|
15
|
-
import Dropdown from '../components/dropdown';
|
|
16
|
-
import { useNotification } from '../components/notification';
|
|
17
|
-
import { SparkleIcon } from 'lucide-react';
|
|
18
20
|
import { useIsEmbedded } from '../components/hooks';
|
|
19
21
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
20
|
-
import {
|
|
21
|
-
import { withBase } from '../utils/base-path';
|
|
22
|
+
import { contentScope } from '../utils/content-scope';
|
|
22
23
|
|
|
23
24
|
const config = docsConfig as DoryConfig;
|
|
24
25
|
|
|
26
|
+
function embedParentOrigin(): string | null {
|
|
27
|
+
try {
|
|
28
|
+
const value = new URLSearchParams(window.location.search).get('parentOrigin');
|
|
29
|
+
if (!value) return null;
|
|
30
|
+
const origin = new URL(value);
|
|
31
|
+
if (origin.origin !== value || !['http:', 'https:'].includes(origin.protocol)) return null;
|
|
32
|
+
return origin.origin === window.location.origin || config.embed?.security?.allowedOrigins?.includes(origin.origin)
|
|
33
|
+
? origin.origin : null;
|
|
34
|
+
} catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
interface LayoutProps {
|
|
26
38
|
children: ComponentChildren;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
const prompt = (pathname: string) => `Please read and analyze the content from this documentation page: ${window.location.origin}${withBase(pathname)}.mdx\n\n` +
|
|
30
|
-
`Then, help me understand and answer any questions I have about it. ` +
|
|
31
|
-
`Please provide clear, detailed explanations and examples where relevant.`;
|
|
32
|
-
|
|
33
41
|
/**
|
|
34
|
-
* Main documentation layout: header, sidebar navigation,
|
|
35
|
-
*
|
|
42
|
+
* Main documentation layout: header, sidebar navigation, breadcrumb and page
|
|
43
|
+
* actions, article content, feedback, prev/next cards, footer, and the
|
|
44
|
+
* "On this page" rail.
|
|
36
45
|
*
|
|
37
46
|
* SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this layout's
|
|
38
|
-
* shell (the
|
|
47
|
+
* shell (the .dory-shell / aside / main / article wrappers in render()).
|
|
39
48
|
* If you change that markup structure here, update entry-server.tsx to match
|
|
40
49
|
* or hydration will mismatch.
|
|
41
50
|
*/
|
|
@@ -43,8 +52,22 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
43
52
|
const [loading, setLoading] = useState(!isFrontmatterReady());
|
|
44
53
|
const rawPathname = usePathname();
|
|
45
54
|
const pathname = rawPathname === '/' ? ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/' : rawPathname;
|
|
46
|
-
const { showNotification } = useNotification();
|
|
47
55
|
const isEmbedded = useIsEmbedded();
|
|
56
|
+
const parentOrigin = useRef<string | null>(typeof window === 'undefined' ? null : embedParentOrigin());
|
|
57
|
+
const embeddedReady = useRef(false);
|
|
58
|
+
const lastEmbeddedPath = useRef<string | null>(null);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isEmbedded || loading || !parentOrigin.current || window.parent === window) return;
|
|
62
|
+
if (!embeddedReady.current) {
|
|
63
|
+
window.parent.postMessage({ source: 'dory', type: 'dory:ready' }, parentOrigin.current);
|
|
64
|
+
embeddedReady.current = true;
|
|
65
|
+
}
|
|
66
|
+
if (lastEmbeddedPath.current !== pathname) {
|
|
67
|
+
window.parent.postMessage({ source: 'dory', type: 'dory:navigate', path: pathname }, parentOrigin.current);
|
|
68
|
+
lastEmbeddedPath.current = pathname;
|
|
69
|
+
}
|
|
70
|
+
}, [isEmbedded, loading, pathname]);
|
|
48
71
|
|
|
49
72
|
// Client-side navigation keeps the previous scroll position, so start each new
|
|
50
73
|
// page at the top. The initial load keeps the browser's own position, and a
|
|
@@ -60,49 +83,6 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
60
83
|
}
|
|
61
84
|
}, [pathname]);
|
|
62
85
|
|
|
63
|
-
const handleOpenMDX = useCallback(() => {
|
|
64
|
-
window.open(`${window.location.origin}${withBase(pathname)}.mdx`, '_blank');
|
|
65
|
-
}, [pathname]);
|
|
66
|
-
|
|
67
|
-
const handleCopyMDX = useCallback(async () => {
|
|
68
|
-
try {
|
|
69
|
-
const response = await fetch(`${withBase(pathname)}.mdx`);
|
|
70
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
71
|
-
await navigator.clipboard.writeText(await response.text());
|
|
72
|
-
showNotification('Copied to clipboard');
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.warn(`Failed to fetch ${pathname}.mdx:`, error);
|
|
75
|
-
showNotification('Could not copy MDX', 'The raw MDX file is not available.');
|
|
76
|
-
}
|
|
77
|
-
}, [pathname, showNotification]);
|
|
78
|
-
const handleOpenChatGPT = useCallback(() => {
|
|
79
|
-
const url = `https://chat.openai.com/?q=${encodeURIComponent(prompt(pathname))}`;
|
|
80
|
-
window.open(url, '_blank');
|
|
81
|
-
}, [pathname]);
|
|
82
|
-
const handleOpenAnthropic = useCallback(() => {
|
|
83
|
-
const url = `https://claude.ai/new?q=${encodeURIComponent(prompt(pathname))}`;
|
|
84
|
-
window.open(url, '_blank');
|
|
85
|
-
}, [pathname]);
|
|
86
|
-
|
|
87
|
-
const dropdownItems = useMemo(() => [
|
|
88
|
-
{
|
|
89
|
-
label: 'Open MDX',
|
|
90
|
-
onClick: handleOpenMDX,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
label: 'Copy MDX',
|
|
94
|
-
onClick: handleCopyMDX
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
label: 'Open in ChatGPT',
|
|
98
|
-
onClick: handleOpenChatGPT
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
label: 'Open in Anthropic',
|
|
102
|
-
onClick: handleOpenAnthropic
|
|
103
|
-
}
|
|
104
|
-
], [handleOpenMDX, handleCopyMDX, handleOpenChatGPT, handleOpenAnthropic]);
|
|
105
|
-
|
|
106
86
|
useEffect(() => {
|
|
107
87
|
// Try to preload frontmatter first (optimized approach).
|
|
108
88
|
// If inline data was available from SSR, preloadFrontmatter() resolves immediately.
|
|
@@ -124,14 +104,12 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
124
104
|
|
|
125
105
|
|
|
126
106
|
const { title: group, group: title, page } = useMemo(() => {
|
|
127
|
-
const group =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
);
|
|
107
|
+
const group = getNavigationForPath(pathname)
|
|
108
|
+
.flatMap((tab) => tab.groups)
|
|
109
|
+
.find((group) => group.pages.some((page) => page.href === pathname));
|
|
132
110
|
const page = completeFrontMatter.find((page) => page.path === pathname);
|
|
133
111
|
|
|
134
|
-
return { group: group
|
|
112
|
+
return { group: group?.title, title: page?.title, page };
|
|
135
113
|
}, [pathname, loading]);
|
|
136
114
|
|
|
137
115
|
// Update document title and meta tags when page changes
|
|
@@ -194,7 +172,7 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
194
172
|
}, [page]);
|
|
195
173
|
|
|
196
174
|
const isLandingPage = useMemo(() => {
|
|
197
|
-
const firstTab =
|
|
175
|
+
const firstTab = getNavigationForPath(pathname)[0];
|
|
198
176
|
if (!firstTab) return false;
|
|
199
177
|
return (
|
|
200
178
|
firstTab.groups.length === 1 &&
|
|
@@ -211,8 +189,9 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
211
189
|
if (tab == null || tab.openapi == null) {
|
|
212
190
|
return null;
|
|
213
191
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
192
|
+
const scope = contentScope(config, tab.path);
|
|
193
|
+
const firstDirectory = `/${scope.page.split('/')[1]}`;
|
|
194
|
+
return (ALL_OPENAPI[`${scope.prefix}${firstDirectory}/openapi.json`] ?? ALL_OPENAPI[`${firstDirectory}/openapi.json`])?.default ?? null;
|
|
216
195
|
}, [tab, loading]);
|
|
217
196
|
|
|
218
197
|
const { method, path } = useMemo(() => {
|
|
@@ -226,8 +205,9 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
226
205
|
if (tab == null || tab.asyncapi == null) {
|
|
227
206
|
return null;
|
|
228
207
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
208
|
+
const scope = contentScope(config, tab.path);
|
|
209
|
+
const firstDirectory = `/${scope.page.split('/')[1]}`;
|
|
210
|
+
return (ALL_ASYNCAPI[`${scope.prefix}${firstDirectory}/asyncapi.json`] ?? ALL_ASYNCAPI[`${firstDirectory}/asyncapi.json`])?.default ?? null;
|
|
231
211
|
}, [tab, loading]);
|
|
232
212
|
|
|
233
213
|
const { operation, channel } = useMemo(() => {
|
|
@@ -239,95 +219,61 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
239
219
|
|
|
240
220
|
if (loading) return <div className="h-[25vh] flex grow"><Loading /></div>;
|
|
241
221
|
|
|
242
|
-
|
|
222
|
+
const errorFallback = (error: Error) => (
|
|
223
|
+
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
|
224
|
+
<p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
225
|
+
This page failed to render
|
|
226
|
+
</p>
|
|
227
|
+
<pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
|
|
228
|
+
{error.message}
|
|
229
|
+
</pre>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const content = openAPIJSON && tab && "openapi" in tab && method && path
|
|
234
|
+
? <OpenAPI openAPIJson={JSON.parse(openAPIJSON)} method={method} path={path} />
|
|
235
|
+
: asyncAPIJSON && tab && "asyncapi" in tab && operation && channel
|
|
236
|
+
? <AsyncAPI asyncAPIJson={JSON.parse(asyncAPIJSON)} operation={operation} channel={channel} />
|
|
237
|
+
: children;
|
|
238
|
+
|
|
239
|
+
const scope = contentScope(config, pathname);
|
|
240
|
+
const isScopeRoot = Boolean(scope.prefix) && pathname === scope.prefix;
|
|
241
|
+
const isNotFound = !ALL_PAGES[pathname] && !isScopeRoot;
|
|
242
|
+
const showSidebar = !isEmbedded && !isLandingPage;
|
|
243
|
+
// OpenAPI pages lay out their own two columns, so they skip the TOC and take its width.
|
|
244
|
+
const isOpenAPIPage = openAPIJSON != null;
|
|
245
|
+
|
|
246
|
+
if (isNotFound) {
|
|
243
247
|
return (
|
|
244
|
-
<
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
250
|
-
This page failed to render
|
|
251
|
-
</p>
|
|
252
|
-
<pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
|
|
253
|
-
{error.message}
|
|
254
|
-
</pre>
|
|
255
|
-
</div>
|
|
256
|
-
)}>
|
|
257
|
-
{children}
|
|
258
|
-
</ErrorBoundary>
|
|
259
|
-
</main>
|
|
260
|
-
</div>
|
|
248
|
+
<main className="dory-main flex min-h-dvh w-full items-center justify-center bg-background px-6 py-12 text-foreground">
|
|
249
|
+
<div className="w-full">
|
|
250
|
+
<ErrorBoundary fallback={errorFallback}>{children}</ErrorBoundary>
|
|
251
|
+
</div>
|
|
252
|
+
</main>
|
|
261
253
|
);
|
|
262
254
|
}
|
|
263
255
|
|
|
264
256
|
return (
|
|
265
|
-
<div className="flex w-full flex-col">
|
|
257
|
+
isLandingPage ? <div className="dory-shell flex min-h-screen w-full flex-col bg-[var(--sidebar)]">
|
|
266
258
|
<Header />
|
|
267
|
-
<div
|
|
268
|
-
"
|
|
269
|
-
|
|
270
|
-
})}>
|
|
271
|
-
<div className={cn("hidden lg:relative lg:block lg:flex-none", {
|
|
272
|
-
"lg:hidden": isEmbedded,
|
|
273
|
-
})}>
|
|
274
|
-
<div className="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">
|
|
275
|
-
<Navigation />
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
<main className={cn("flex grow max-w-full", {
|
|
279
|
-
"px-4 py-16 sm:px-6 lg:px-8": !isEmbedded,
|
|
280
|
-
"p-8": isEmbedded,
|
|
281
|
-
})}>
|
|
282
|
-
<div className="min-w-0 w-full">
|
|
283
|
-
<article className="h-full flex flex-1 flex-col">
|
|
284
|
-
<div className="flex justify-between items-start">
|
|
285
|
-
<header className="mb-8">
|
|
286
|
-
{title && (
|
|
287
|
-
<p className="dory-subtitle text-sm font-medium tracking-widest uppercase mb-2">
|
|
288
|
-
{title}
|
|
289
|
-
</p>
|
|
290
|
-
)}
|
|
291
|
-
{group && (
|
|
292
|
-
<h1 className="dory-page-title text-4xl tracking-tight">
|
|
293
|
-
{group}
|
|
294
|
-
</h1>
|
|
295
|
-
)}
|
|
296
|
-
{page?.lead && <p className="dory-lead">{page.lead}</p>}
|
|
297
|
-
</header>
|
|
298
|
-
{
|
|
299
|
-
openAPIJSON == null && asyncAPIJSON == null &&
|
|
300
|
-
<Dropdown className="gap-0" buttonLabel={<SparkleIcon className="w-4 h-4" />} items={dropdownItems} />
|
|
301
|
-
}
|
|
302
|
-
</div>
|
|
303
|
-
<PageMeta meta={page?.meta} />
|
|
304
|
-
<div className="flex-1 min-h-[calc(100vh-2rem)]">
|
|
305
|
-
<ErrorBoundary fallback={(error) => (
|
|
306
|
-
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
|
307
|
-
<p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
308
|
-
This page failed to render
|
|
309
|
-
</p>
|
|
310
|
-
<pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
|
|
311
|
-
{error.message}
|
|
312
|
-
</pre>
|
|
313
|
-
</div>
|
|
314
|
-
)}>
|
|
315
|
-
{openAPIJSON && tab && "openapi" in tab && method && path ?
|
|
316
|
-
<OpenAPI openAPIJson={JSON.parse(openAPIJSON)} method={method} path={path} />
|
|
317
|
-
: asyncAPIJSON && tab && "asyncapi" in tab && operation && channel ?
|
|
318
|
-
<AsyncAPI asyncAPIJson={JSON.parse(asyncAPIJSON)} operation={operation} channel={channel} />
|
|
319
|
-
: children}
|
|
320
|
-
</ErrorBoundary>
|
|
321
|
-
</div>
|
|
322
|
-
<PrevNextLinks />
|
|
323
|
-
</article>
|
|
324
|
-
</div>
|
|
325
|
-
{
|
|
326
|
-
openAPIJSON == null && asyncAPIJSON == null && !isEmbedded &&
|
|
327
|
-
<TableOfContents />
|
|
328
|
-
}
|
|
259
|
+
<div className="flex flex-1">
|
|
260
|
+
<main className="dory-main min-w-0 flex-1 bg-background px-6 py-10 text-foreground sm:px-10">
|
|
261
|
+
<ErrorBoundary fallback={errorFallback}>{children}</ErrorBoundary>
|
|
329
262
|
</main>
|
|
330
263
|
</div>
|
|
331
|
-
</div>
|
|
264
|
+
</div> : <DocsPageFrame
|
|
265
|
+
header={<Header />}
|
|
266
|
+
sidebar={showSidebar ? <Navigation /> : undefined}
|
|
267
|
+
breadcrumb={<Breadcrumb group={title} title={group} />}
|
|
268
|
+
actions={<PageActions pathname={pathname} />}
|
|
269
|
+
title={group}
|
|
270
|
+
lead={page?.lead}
|
|
271
|
+
meta={<PageMeta meta={page?.meta} />}
|
|
272
|
+
content={<ErrorBoundary fallback={errorFallback}>{content}</ErrorBoundary>}
|
|
273
|
+
afterContent={<><PageFeedback pathname={pathname} /><PrevNextLinks /><PageFooter /></>}
|
|
274
|
+
toc={!isOpenAPIPage && <TableOfContents />}
|
|
275
|
+
embedded={isEmbedded}
|
|
276
|
+
wide={isOpenAPIPage}
|
|
277
|
+
/>
|
|
332
278
|
);
|
|
333
|
-
}
|
|
279
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Sheet, SheetContent, Button, ModeToggle } from '@clidey/ux'
|
|
4
|
-
import { createContext, Suspense, useCallback, useContext, useRef, useState } from 'react'
|
|
3
|
+
import { CatalogIcon, Sheet, SheetContent, Button, ModeToggle } from '@clidey/ux'
|
|
4
|
+
import { createContext, Suspense, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
|
5
5
|
|
|
6
|
-
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline'
|
|
7
6
|
import { Navigation } from '../components/navigation'
|
|
8
|
-
import { useIsEmbedded } from '../components/hooks'
|
|
7
|
+
import { useIsEmbedded, usePathname } from '../components/hooks'
|
|
9
8
|
|
|
10
9
|
const IsInsideMobileNavigationContext = createContext(false)
|
|
11
10
|
|
|
@@ -34,8 +33,11 @@ export function useMobileNavigation() {
|
|
|
34
33
|
export function MobileNavigation() {
|
|
35
34
|
const isInsideMobileNavigation = useIsInsideMobileNavigation();
|
|
36
35
|
const { isOpen, open, close } = useMobileNavigation();
|
|
37
|
-
const ToggleIcon = isOpen ? XMarkIcon : Bars3Icon;
|
|
38
36
|
const isEmbedded = useIsEmbedded();
|
|
37
|
+
const pathname = usePathname();
|
|
38
|
+
|
|
39
|
+
// Picking a page navigates client-side, so close the menu once the route changes.
|
|
40
|
+
useEffect(() => close(), [pathname, close]);
|
|
39
41
|
|
|
40
42
|
return (
|
|
41
43
|
<IsInsideMobileNavigationContext.Provider value={true}>
|
|
@@ -47,15 +49,15 @@ export function MobileNavigation() {
|
|
|
47
49
|
<Button
|
|
48
50
|
variant="outline"
|
|
49
51
|
size="icon"
|
|
50
|
-
className="relative flex size-6 lg:hidden p-4"
|
|
52
|
+
className="relative flex size-6 border-white/15 bg-white/5 text-white hover:bg-white/10 lg:hidden p-4"
|
|
51
53
|
aria-label="Toggle navigation"
|
|
52
54
|
onClick={open}
|
|
53
55
|
>
|
|
54
|
-
<
|
|
56
|
+
<CatalogIcon name={isOpen ? 'close' : 'menu'} className="w-8 stroke-white" />
|
|
55
57
|
</Button>
|
|
56
58
|
{!isInsideMobileNavigation && (
|
|
57
59
|
<Suspense fallback={null}>
|
|
58
|
-
<SheetContent side="left" className="w-full sm:max-w-sm
|
|
60
|
+
<SheetContent side="left" className="w-full sm:max-w-sm overflow-y-auto border-white/10 bg-[var(--sidebar)] p-4 pt-12 text-white">
|
|
59
61
|
<Navigation />
|
|
60
62
|
{
|
|
61
63
|
isEmbedded &&
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useCallback } from 'preact/hooks'
|
|
2
|
+
|
|
3
|
+
import { CatalogIcon, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@clidey/ux'
|
|
4
|
+
import { useNotification } from '../components/notification'
|
|
5
|
+
import { withBase } from '../utils/base-path'
|
|
6
|
+
|
|
7
|
+
const prompt = (pathname: string) => `Please read and analyze the content from this documentation page: ${window.location.origin}${withBase(pathname)}.mdx\n\n` +
|
|
8
|
+
`Then, help me understand and answer any questions I have about it. ` +
|
|
9
|
+
`Please provide clear, detailed explanations and examples where relevant.`
|
|
10
|
+
|
|
11
|
+
/** The page action controls shared by built sites and sandboxed docs. */
|
|
12
|
+
export function PageActionsView({ onCopy, onOpenMDX, onOpenChatGPT, onOpenClaude }: {
|
|
13
|
+
onCopy: () => void;
|
|
14
|
+
onOpenMDX?: () => void;
|
|
15
|
+
onOpenChatGPT: () => void;
|
|
16
|
+
onOpenClaude: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
const items = [
|
|
19
|
+
...(onOpenMDX ? [{ label: 'Open MDX', action: onOpenMDX }] : []),
|
|
20
|
+
{ label: 'Open in ChatGPT', action: onOpenChatGPT },
|
|
21
|
+
{ label: 'Open in Claude', action: onOpenClaude },
|
|
22
|
+
]
|
|
23
|
+
return (
|
|
24
|
+
<div className="dory-page-actions">
|
|
25
|
+
<button type="button" onClick={onCopy}>
|
|
26
|
+
<CatalogIcon name="copy" aria-hidden="true" className="h-3.5 w-3.5" />
|
|
27
|
+
Copy page
|
|
28
|
+
</button>
|
|
29
|
+
<DropdownMenu>
|
|
30
|
+
<DropdownMenuTrigger className="dory-page-actions-ai">
|
|
31
|
+
<CatalogIcon name="ai" aria-hidden="true" className="h-3.5 w-3.5 text-[var(--primary)]" />
|
|
32
|
+
AI actions
|
|
33
|
+
<CatalogIcon name="chevron-down" aria-hidden="true" className="h-3.5 w-3.5" />
|
|
34
|
+
</DropdownMenuTrigger>
|
|
35
|
+
<DropdownMenuContent align="end">
|
|
36
|
+
{items.map((item) => (
|
|
37
|
+
<DropdownMenuItem key={item.label} className="cursor-pointer" onClick={item.action}>
|
|
38
|
+
{item.label}
|
|
39
|
+
</DropdownMenuItem>
|
|
40
|
+
))}
|
|
41
|
+
</DropdownMenuContent>
|
|
42
|
+
</DropdownMenu>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** "Copy page" and "AI actions" connected to a built Dory site's raw MDX URLs. */
|
|
48
|
+
export function PageActions({ pathname }: { pathname: string }) {
|
|
49
|
+
const { showNotification } = useNotification()
|
|
50
|
+
const handleCopyMDX = useCallback(async () => {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`${withBase(pathname)}.mdx`)
|
|
53
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
54
|
+
await navigator.clipboard.writeText(await response.text())
|
|
55
|
+
showNotification('Copied to clipboard')
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`Failed to fetch ${pathname}.mdx:`, error)
|
|
58
|
+
showNotification('Could not copy MDX', 'The raw MDX file is not available.')
|
|
59
|
+
}
|
|
60
|
+
}, [pathname, showNotification])
|
|
61
|
+
return <PageActionsView
|
|
62
|
+
onCopy={handleCopyMDX}
|
|
63
|
+
onOpenMDX={() => window.open(`${window.location.origin}${withBase(pathname)}.mdx`, '_blank')}
|
|
64
|
+
onOpenChatGPT={() => window.open(`https://chat.openai.com/?q=${encodeURIComponent(prompt(pathname))}`, '_blank')}
|
|
65
|
+
onOpenClaude={() => window.open(`https://claude.ai/new?q=${encodeURIComponent(prompt(pathname))}`, '_blank')}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks'
|
|
2
|
+
import { captureDocsEvent } from '../utils/analytics'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* "Was this page helpful?" prompt and a small page engagement signal.
|
|
6
|
+
*/
|
|
7
|
+
export function PageFeedbackView({ pathname, onAnswer }: { pathname: string; onAnswer: (helpful: boolean) => void }) {
|
|
8
|
+
const [answered, setAnswered] = useState(false)
|
|
9
|
+
|
|
10
|
+
useEffect(() => setAnswered(false), [pathname])
|
|
11
|
+
|
|
12
|
+
const answer = (helpful: boolean) => {
|
|
13
|
+
onAnswer(helpful)
|
|
14
|
+
setAnswered(true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="dory-feedback mt-16 flex items-center justify-between border-y border-[var(--border)] py-4">
|
|
19
|
+
{answered ? (
|
|
20
|
+
<p className="text-sm text-muted-foreground" role="status">Thanks for the feedback.</p>
|
|
21
|
+
) : (
|
|
22
|
+
<>
|
|
23
|
+
<p className="text-sm font-medium text-foreground">Was this page helpful?</p>
|
|
24
|
+
<div className="flex gap-2">
|
|
25
|
+
<button type="button" onClick={() => answer(true)}>Yes</button>
|
|
26
|
+
<button type="button" onClick={() => answer(false)}>No</button>
|
|
27
|
+
</div>
|
|
28
|
+
</>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PageFeedback({ pathname }: { pathname: string }) {
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let visibleSeconds = 0
|
|
38
|
+
const timer = window.setInterval(() => {
|
|
39
|
+
if (document.visibilityState !== 'visible') return
|
|
40
|
+
visibleSeconds += 1
|
|
41
|
+
if (visibleSeconds === 15) {
|
|
42
|
+
captureDocsEvent('docs_page_engaged', { path: pathname, active_seconds: 15 })
|
|
43
|
+
window.clearInterval(timer)
|
|
44
|
+
}
|
|
45
|
+
}, 1000)
|
|
46
|
+
return () => window.clearInterval(timer)
|
|
47
|
+
}, [pathname])
|
|
48
|
+
|
|
49
|
+
return <PageFeedbackView pathname={pathname} onAnswer={helpful => captureDocsEvent('docs_page_feedback', { path: pathname, helpful })} />
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import docsConfig from '../../docs/dory.json' with { type: 'json' }
|
|
2
|
+
import type { DoryConfig } from '../types/config'
|
|
3
|
+
import { DORY_REPO_URL, DoryMark } from './dory-mark'
|
|
4
|
+
|
|
5
|
+
const config = docsConfig as DoryConfig
|
|
6
|
+
|
|
7
|
+
/** Article footer: "Powered by Dory" and the copyright line. */
|
|
8
|
+
export function PageFooterView({ copyright, name }: { copyright?: string; name: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<footer className="dory-page-footer mt-12 flex items-center justify-between pb-4">
|
|
11
|
+
<a href={DORY_REPO_URL} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
|
|
12
|
+
<DoryMark className="h-3.5 w-3.5" />
|
|
13
|
+
Powered by Dory
|
|
14
|
+
</a>
|
|
15
|
+
<span>© {new Date().getFullYear()} {copyright ?? name}</span>
|
|
16
|
+
</footer>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Article footer configured for the current built Dory site. */
|
|
21
|
+
export function PageFooter() {
|
|
22
|
+
return <PageFooterView copyright={config.copyright} name={config.name} />
|
|
23
|
+
}
|
|
@@ -1,75 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { withBase } from '../utils/base-path';
|
|
3
|
-
import classNames from "classnames";
|
|
4
|
-
import { ALL_NAVIGATION } from "../components/store";
|
|
1
|
+
import { completeFrontMatter, getNavigationForPath, type Navigation } from "../components/store";
|
|
5
2
|
import { usePathname } from "../components/hooks";
|
|
3
|
+
import { PrevNextLinksView, type PageEntry } from './prev-next-view';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}: Omit<React.ComponentPropsWithoutRef<'div'>, 'dir' | 'title'> & {
|
|
13
|
-
title: string
|
|
14
|
-
href: string
|
|
15
|
-
dir?: 'previous' | 'next'
|
|
16
|
-
}) {
|
|
17
|
-
return (
|
|
18
|
-
<div {...props}>
|
|
19
|
-
<p className="font-display text-xs font-medium text-muted-foreground">
|
|
20
|
-
{dir === 'next' ? 'Next' : 'Previous'}
|
|
21
|
-
</p>
|
|
22
|
-
<div className="mt-1">
|
|
23
|
-
<a href={withBase(href)} className={classNames('flex items-center gap-x-1 text-sm font-semibold hover:opacity-80', {
|
|
24
|
-
'flex-row-reverse': dir === 'previous',
|
|
25
|
-
},
|
|
26
|
-
)}>
|
|
27
|
-
<p className={classNames("flex items-center gap-x-1 text-sm font-semibold hover:opacity-80", {
|
|
28
|
-
"flex-row-reverse": dir === "previous"
|
|
29
|
-
})}>
|
|
30
|
-
{title}
|
|
31
|
-
<ArrowRightIcon
|
|
32
|
-
className={classNames(
|
|
33
|
-
'h-4 w-4 flex-none fill-current',
|
|
34
|
-
{
|
|
35
|
-
'rotate-180': dir === 'previous',
|
|
36
|
-
},
|
|
37
|
-
)}
|
|
38
|
-
/>
|
|
39
|
-
</p>
|
|
40
|
-
</a>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
export function PrevNextLinks() {
|
|
5
|
+
/**
|
|
6
|
+
* Previous/next page cards in navigation order. Pass `navigation` when
|
|
7
|
+
* rendering on the server, where store titles are not yet loaded.
|
|
8
|
+
*/
|
|
9
|
+
export function PrevNextLinks({ navigation }: { navigation?: Navigation[] }) {
|
|
46
10
|
const pathname = usePathname();
|
|
47
|
-
const
|
|
48
|
-
tab.groups.flatMap(group =>
|
|
49
|
-
group.pages.map(page =>
|
|
11
|
+
const pages: PageEntry[] = (navigation ?? getNavigationForPath(pathname)).flatMap(tab =>
|
|
12
|
+
tab.groups.flatMap(group =>
|
|
13
|
+
group.pages.map(page => ({
|
|
14
|
+
title: completeFrontMatter.find(entry => entry.path === page.href)?.title ?? page.title,
|
|
15
|
+
href: page.href,
|
|
16
|
+
group: group.title,
|
|
17
|
+
}))
|
|
50
18
|
)
|
|
51
19
|
);
|
|
52
|
-
|
|
53
|
-
const previousPage = linkIndex > -1 ? allLinks[linkIndex - 1] : null;
|
|
54
|
-
const nextPage = linkIndex > -1 ? allLinks[linkIndex + 1] : null;
|
|
55
|
-
|
|
56
|
-
const previousPageTitle = previousPage ? ALL_NAVIGATION
|
|
57
|
-
.flatMap(tab => tab.groups)
|
|
58
|
-
.flatMap(group => group.pages)
|
|
59
|
-
.find(page => page.href === previousPage)?.title : null;
|
|
60
|
-
const nextPageTitle = nextPage ? ALL_NAVIGATION
|
|
61
|
-
.flatMap(tab => tab.groups)
|
|
62
|
-
.flatMap(group => group.pages)
|
|
63
|
-
.find(page => page.href === nextPage)?.title : null;
|
|
64
|
-
|
|
65
|
-
if (!nextPage && !previousPage) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className="mt-12 pt-6 flex border-t border-black/20 dark:border-white/20">
|
|
71
|
-
{previousPage && <PageLink dir="previous" title={previousPageTitle ?? ''} href={previousPage} />}
|
|
72
|
-
{nextPage && <PageLink className="ml-auto text-right" title={nextPageTitle ?? ''} href={nextPage} />}
|
|
73
|
-
</div>
|
|
74
|
-
)
|
|
20
|
+
return <PrevNextLinksView pages={pages} pathname={pathname} />;
|
|
75
21
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { Link } from 'wouter-preact';
|
|
3
|
+
|
|
4
|
+
export type PageEntry = { title: string; href: string; group: string };
|
|
5
|
+
|
|
6
|
+
function PageCard({ page, dir, className, onNavigate }: { page: PageEntry; dir: 'previous' | 'next'; className?: string; onNavigate?: (href: string) => void }) {
|
|
7
|
+
const contents = <>
|
|
8
|
+
<span className="dory-pager-label">{dir === 'previous' ? `← ${page.group}` : `${page.group} →`}</span>
|
|
9
|
+
<span className="dory-pager-title">{page.title}</span>
|
|
10
|
+
</>;
|
|
11
|
+
const classes = classNames('dory-pager-card', className, { 'text-right': dir === 'next' });
|
|
12
|
+
return onNavigate
|
|
13
|
+
? <a href="#" className={classes} onClick={event => { event.preventDefault(); onNavigate(page.href); }}>{contents}</a>
|
|
14
|
+
: <Link href={page.href} className={classes}>{contents}</Link>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Navigation cards shared by the built site and the app-owned runtime. */
|
|
18
|
+
export function PrevNextLinksView({ pages, pathname, onNavigate }: { pages: PageEntry[]; pathname: string; onNavigate?: (href: string) => void }) {
|
|
19
|
+
const index = pages.findIndex(page => page.href === pathname);
|
|
20
|
+
const previousPage = index > 0 ? pages[index - 1] : null;
|
|
21
|
+
const nextPage = index > -1 ? pages[index + 1] : null;
|
|
22
|
+
if (!nextPage && !previousPage) return null;
|
|
23
|
+
return <div className="mt-6 grid gap-4 sm:grid-cols-2">
|
|
24
|
+
{previousPage && <PageCard dir="previous" page={previousPage} onNavigate={onNavigate} />}
|
|
25
|
+
{nextPage && <PageCard dir="next" page={nextPage} onNavigate={onNavigate} className={classNames({ 'sm:col-span-2': !previousPage })} />}
|
|
26
|
+
</div>;
|
|
27
|
+
}
|