@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
|
@@ -6,44 +6,83 @@ import {
|
|
|
6
6
|
} from '@algolia/autocomplete-core'
|
|
7
7
|
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
|
8
8
|
import {
|
|
9
|
-
forwardRef,
|
|
10
9
|
Fragment,
|
|
10
|
+
forwardRef,
|
|
11
11
|
Suspense,
|
|
12
12
|
useCallback,
|
|
13
13
|
useEffect,
|
|
14
14
|
useId,
|
|
15
15
|
useRef,
|
|
16
16
|
useState,
|
|
17
|
-
type JSX,
|
|
18
17
|
} from 'react'
|
|
18
|
+
import type { TargetedEvent, TargetedKeyboardEvent, TargetedMouseEvent } from 'preact'
|
|
19
19
|
import Highlighter from 'react-highlight-words'
|
|
20
|
-
import { cn, Spinner, SearchInput as UxSearchInput } from '@clidey/ux'
|
|
20
|
+
import { CatalogIcon, cn, Spinner, SearchInput as UxSearchInput } from '@clidey/ux'
|
|
21
|
+
|
|
21
22
|
import classNames from 'classnames'
|
|
22
23
|
import { useLocation } from 'wouter-preact'
|
|
23
24
|
import { useMobileNavigation } from '../ui/mobile-navigation'
|
|
24
25
|
import { useIsEmbedded, usePathname, useSearchParams } from './hooks'
|
|
25
26
|
import { search } from './search-index'
|
|
26
|
-
import {
|
|
27
|
+
import { ensureSearchContent, getNavigationForPath } from './store'
|
|
28
|
+
import { captureDocsEvent } from '../utils/analytics'
|
|
29
|
+
import { ChatAssistant, useChatAssistant } from './chat-assistant'
|
|
27
30
|
|
|
28
31
|
type EmptyObject = Record<string, never>
|
|
29
|
-
type Result =
|
|
32
|
+
type Result = { url: string; title: string; pageTitle?: string; kind?: 'ask' }
|
|
33
|
+
const ASK_RESULT_URL = '__dory_ask__'
|
|
30
34
|
|
|
31
35
|
type Autocomplete = AutocompleteApi<
|
|
32
36
|
Result,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
TargetedEvent,
|
|
38
|
+
TargetedMouseEvent<HTMLElement>,
|
|
39
|
+
TargetedKeyboardEvent<HTMLElement>
|
|
36
40
|
>
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
const SUGGESTED_PAGES = 3
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Results shown before the reader types: the current page, its sections, and
|
|
46
|
+
* the next few pages in navigation order.
|
|
47
|
+
*/
|
|
48
|
+
function suggestions(pathname: string): Result[] {
|
|
49
|
+
const pages = getNavigationForPath(pathname).flatMap(tab => tab.groups.flatMap(group => group.pages))
|
|
50
|
+
const index = Math.max(0, pages.findIndex(page => page.href === pathname))
|
|
51
|
+
const current = pages[index]
|
|
52
|
+
if (!current) return []
|
|
53
|
+
const sections = Array.from(document.querySelectorAll('.dory-article h2[id]'))
|
|
54
|
+
.filter(heading => !heading.closest('.dory-specimen'))
|
|
55
|
+
.map(heading => ({
|
|
56
|
+
url: `${current.href}#${heading.id}`,
|
|
57
|
+
title: Array.from(heading.childNodes)
|
|
58
|
+
.filter(node => !(node instanceof Element && node.classList.contains('dory-anchor')))
|
|
59
|
+
.map(node => node.textContent ?? '')
|
|
60
|
+
.join('')
|
|
61
|
+
.trim(),
|
|
62
|
+
}))
|
|
63
|
+
const next = pages.slice(index + 1, index + 1 + SUGGESTED_PAGES)
|
|
64
|
+
return [current, ...sections, ...next].map(item => ({ url: 'href' in item ? item.href : item.url, title: item.title }))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function useAutocomplete({ onNavigate, onAsk, chatEnabled }: { onNavigate: () => void; onAsk: (query: string) => void; chatEnabled: boolean }) {
|
|
39
68
|
const id = useId()
|
|
40
69
|
const [, nav] = useLocation()
|
|
70
|
+
// getSources is created once, so read the current page through a ref.
|
|
71
|
+
const pathnameRef = useRef('')
|
|
72
|
+
pathnameRef.current = usePathname()
|
|
73
|
+
const queryRef = useRef('')
|
|
74
|
+
const onAskRef = useRef(onAsk)
|
|
75
|
+
onAskRef.current = onAsk
|
|
76
|
+
const chatEnabledRef = useRef(chatEnabled)
|
|
77
|
+
chatEnabledRef.current = chatEnabled
|
|
41
78
|
const [autocompleteState, setAutocompleteState] = useState<
|
|
42
79
|
AutocompleteState<Result> | EmptyObject
|
|
43
80
|
>({})
|
|
44
81
|
|
|
45
82
|
function navigate({ itemUrl }: { itemUrl?: string }) {
|
|
46
83
|
if (itemUrl) {
|
|
84
|
+
const query = queryRef.current.trim()
|
|
85
|
+
if (query) captureDocsEvent('docs_search_result_clicked', { query, result: itemUrl, path: pathnameRef.current })
|
|
47
86
|
nav(itemUrl);
|
|
48
87
|
}
|
|
49
88
|
|
|
@@ -53,18 +92,20 @@ function useAutocomplete({ onNavigate }: { onNavigate: () => void }) {
|
|
|
53
92
|
const [autocomplete] = useState<Autocomplete>(() =>
|
|
54
93
|
createAutocomplete<
|
|
55
94
|
Result,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
TargetedEvent,
|
|
96
|
+
TargetedMouseEvent<HTMLElement>,
|
|
97
|
+
TargetedKeyboardEvent<HTMLElement>
|
|
59
98
|
>({
|
|
60
99
|
id,
|
|
61
|
-
placeholder: '
|
|
100
|
+
placeholder: 'Search documentation',
|
|
62
101
|
defaultActiveItemId: 0,
|
|
63
102
|
onStateChange({ state }) {
|
|
103
|
+
queryRef.current = state.query
|
|
64
104
|
setAutocompleteState(state)
|
|
65
105
|
},
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
openOnFocus: true,
|
|
107
|
+
shouldPanelOpen() {
|
|
108
|
+
return true
|
|
68
109
|
},
|
|
69
110
|
navigator: {
|
|
70
111
|
navigate,
|
|
@@ -73,13 +114,21 @@ function useAutocomplete({ onNavigate }: { onNavigate: () => void }) {
|
|
|
73
114
|
return [
|
|
74
115
|
{
|
|
75
116
|
sourceId: 'documentation',
|
|
76
|
-
getItems() {
|
|
77
|
-
|
|
117
|
+
async getItems() {
|
|
118
|
+
if (!query) return suggestions(pathnameRef.current)
|
|
119
|
+
await ensureSearchContent()
|
|
120
|
+
const pages = search(query, { limit: 8 })
|
|
121
|
+
return chatEnabledRef.current
|
|
122
|
+
? [{ url: ASK_RESULT_URL, title: query, kind: 'ask' as const }, ...pages]
|
|
123
|
+
: pages
|
|
78
124
|
},
|
|
79
125
|
getItemUrl({ item }) {
|
|
80
|
-
return item.url
|
|
126
|
+
return item.kind === 'ask' ? undefined : item.url
|
|
127
|
+
},
|
|
128
|
+
onSelect({ item }) {
|
|
129
|
+
if (item.kind === 'ask') onAskRef.current(item.title)
|
|
130
|
+
else navigate({ itemUrl: item.url })
|
|
81
131
|
},
|
|
82
|
-
onSelect: navigate,
|
|
83
132
|
},
|
|
84
133
|
]
|
|
85
134
|
},
|
|
@@ -89,11 +138,10 @@ function useAutocomplete({ onNavigate }: { onNavigate: () => void }) {
|
|
|
89
138
|
return { autocomplete, autocompleteState }
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
|
|
93
141
|
function HighlightQuery({ text, query }: { text: string; query: string }) {
|
|
94
142
|
return (
|
|
95
143
|
<Highlighter
|
|
96
|
-
highlightClassName="
|
|
144
|
+
highlightClassName="dory-search-highlight"
|
|
97
145
|
searchWords={[query]}
|
|
98
146
|
autoEscape={true}
|
|
99
147
|
textToHighlight={text}
|
|
@@ -114,64 +162,46 @@ function SearchResult({
|
|
|
114
162
|
}) {
|
|
115
163
|
const id = useId()
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
if (result.kind === 'ask') {
|
|
166
|
+
return (
|
|
167
|
+
<li className="dory-search-result dory-search-ask-result flex cursor-default items-center gap-4 px-5 py-3" {...autocomplete.getItemProps({ item: result, source: collection.source })}>
|
|
168
|
+
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--primary)] text-[var(--primary-foreground)]" aria-hidden="true"><CatalogIcon name="ai" className="h-4 w-4" /></span>
|
|
169
|
+
<span className="min-w-0 flex-1">
|
|
170
|
+
<span className="block truncate text-sm font-semibold text-foreground">{result.title}</span>
|
|
171
|
+
<span className="block text-xs text-muted-foreground">Ask Dory · answers cite these docs</span>
|
|
172
|
+
</span>
|
|
173
|
+
<span className="hidden gap-1 text-xs text-muted-foreground sm:flex" aria-hidden="true"><kbd className="dory-search-key">Tab</kbd><kbd className="dory-search-key">↵</kbd></span>
|
|
174
|
+
</li>
|
|
123
175
|
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const page = group?.groups.find(group => group.pages.find(page => page.href === currentPage));
|
|
176
|
+
}
|
|
127
177
|
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
)
|
|
178
|
+
const [currentPage, hash] = result.url.split('#')
|
|
179
|
+
const group = getNavigationForPath(currentPage).flatMap(tab => tab.groups)
|
|
180
|
+
.find(group => group.pages.some(page => page.href === currentPage))
|
|
181
|
+
const pageTitle = group?.pages.find(page => page.href === currentPage)?.title
|
|
182
|
+
// Sections show "Group · Page" above their title; pages show only their group.
|
|
183
|
+
const eyebrow = [group?.title, hash ? pageTitle : undefined].filter(Boolean).join(' · ')
|
|
134
184
|
|
|
135
185
|
return (
|
|
136
186
|
<li
|
|
137
|
-
className=
|
|
138
|
-
'group block cursor-default px-4 py-3 aria-selected:bg-zinc-50 dark:aria-selected:bg-zinc-800/50',
|
|
139
|
-
)}
|
|
187
|
+
className="dory-search-result group flex cursor-default items-center gap-4 px-8 py-3"
|
|
140
188
|
aria-labelledby={`${id}-hierarchy ${id}-title`}
|
|
141
189
|
{...autocomplete.getItemProps({
|
|
142
190
|
item: result,
|
|
143
191
|
source: collection.source,
|
|
144
192
|
})}
|
|
145
193
|
>
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
aria-hidden="true"
|
|
149
|
-
|
|
150
|
-
>
|
|
151
|
-
<HighlightQuery text={result.title} query={query} />
|
|
152
|
-
</div>
|
|
153
|
-
{hierarchy.length > 0 && (
|
|
154
|
-
<div
|
|
155
|
-
id={`${id}-hierarchy`}
|
|
156
|
-
aria-hidden="true"
|
|
157
|
-
className="mt-1 truncate text-2xs whitespace-nowrap text-muted-foreground"
|
|
158
|
-
>
|
|
159
|
-
{hierarchy.map((item, itemIndex, items) => (
|
|
160
|
-
<Fragment key={itemIndex}>
|
|
161
|
-
<HighlightQuery text={item} query={query} />
|
|
162
|
-
<span
|
|
163
|
-
className={
|
|
164
|
-
itemIndex === items.length - 1
|
|
165
|
-
? 'sr-only'
|
|
166
|
-
: 'mx-2 text-zinc-300 dark:text-zinc-700'
|
|
167
|
-
}
|
|
168
|
-
>
|
|
169
|
-
/
|
|
170
|
-
</span>
|
|
171
|
-
</Fragment>
|
|
172
|
-
))}
|
|
194
|
+
<span className="dory-search-result-icon flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-[var(--border)]" aria-hidden="true"><CatalogIcon name="file" className="h-4 w-4" /></span>
|
|
195
|
+
<div className="min-w-0 flex-1">
|
|
196
|
+
<div id={`${id}-title`} aria-hidden="true" className="dory-search-title">
|
|
197
|
+
<HighlightQuery text={result.title} query={query} />
|
|
173
198
|
</div>
|
|
174
|
-
|
|
199
|
+
{eyebrow && (
|
|
200
|
+
<div id={`${id}-hierarchy`} aria-hidden="true" className="dory-search-eyebrow truncate">
|
|
201
|
+
<HighlightQuery text={eyebrow} query={query} />
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
175
205
|
</li>
|
|
176
206
|
)
|
|
177
207
|
}
|
|
@@ -207,15 +237,17 @@ function SearchResults({
|
|
|
207
237
|
}
|
|
208
238
|
|
|
209
239
|
return (
|
|
210
|
-
<ul {...autocomplete.getListProps()}>
|
|
211
|
-
{collection.items.map((result) => (
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
240
|
+
<ul className="py-3" {...autocomplete.getListProps()}>
|
|
241
|
+
{collection.items.map((result, index) => (
|
|
242
|
+
<Fragment key={result.url}>
|
|
243
|
+
{index === 1 && collection.items[0]?.kind === 'ask' && <li className="px-8 pb-1 pt-4 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground" role="presentation">Pages</li>}
|
|
244
|
+
<SearchResult
|
|
245
|
+
result={result}
|
|
246
|
+
autocomplete={autocomplete}
|
|
247
|
+
collection={collection}
|
|
248
|
+
query={query}
|
|
249
|
+
/>
|
|
250
|
+
</Fragment>
|
|
219
251
|
))}
|
|
220
252
|
</ul>
|
|
221
253
|
)
|
|
@@ -227,24 +259,26 @@ const SearchInput = forwardRef<
|
|
|
227
259
|
autocomplete: Autocomplete
|
|
228
260
|
autocompleteState: AutocompleteState<Result> | EmptyObject
|
|
229
261
|
onClose: () => void
|
|
262
|
+
onAsk: () => void
|
|
263
|
+
chatEnabled: boolean
|
|
230
264
|
}
|
|
231
|
-
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
|
|
265
|
+
>(function SearchInput({ autocomplete, autocompleteState, onClose, onAsk, chatEnabled }, inputRef) {
|
|
232
266
|
const inputProps = autocomplete.getInputProps({ inputElement: null })
|
|
233
267
|
|
|
234
268
|
return (
|
|
235
|
-
<div className="
|
|
236
|
-
<
|
|
269
|
+
<div className="relative flex h-16 items-center gap-2 px-4 sm:gap-3 sm:px-7">
|
|
270
|
+
<CatalogIcon name="search" aria-hidden="true" className="h-5 w-5 shrink-0 text-muted-foreground" />
|
|
271
|
+
<input
|
|
237
272
|
ref={inputRef}
|
|
238
273
|
data-autofocus
|
|
239
|
-
className=
|
|
240
|
-
'pr-11': autocompleteState.status === 'stalled',
|
|
241
|
-
'pr-4': autocompleteState.status !== 'stalled',
|
|
242
|
-
})}
|
|
274
|
+
className="dory-search-input h-full min-w-0 flex-1 bg-transparent outline-none"
|
|
243
275
|
{...inputProps}
|
|
244
276
|
onKeyDown={(event) => {
|
|
245
|
-
if (
|
|
277
|
+
if (event.key === 'Tab' && !event.shiftKey && chatEnabled) {
|
|
278
|
+
event.preventDefault()
|
|
279
|
+
onAsk()
|
|
280
|
+
} else if (
|
|
246
281
|
event.key === 'Escape' &&
|
|
247
|
-
!autocompleteState.isOpen &&
|
|
248
282
|
autocompleteState.query === ''
|
|
249
283
|
) {
|
|
250
284
|
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
|
|
@@ -259,11 +293,11 @@ const SearchInput = forwardRef<
|
|
|
259
293
|
}
|
|
260
294
|
}}
|
|
261
295
|
/>
|
|
262
|
-
{autocompleteState.status === 'stalled'
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
296
|
+
{autocompleteState.status === 'stalled'
|
|
297
|
+
? <Spinner className="h-5 w-5 animate-spin text-brand-foreground" />
|
|
298
|
+
: null}
|
|
299
|
+
<span className="dory-search-active" aria-current="page">Search</span>
|
|
300
|
+
{chatEnabled && <button type="button" className="dory-search-ask-button" onClick={onAsk}>Ask Dory</button>}
|
|
267
301
|
</div>
|
|
268
302
|
)
|
|
269
303
|
})
|
|
@@ -282,19 +316,63 @@ function SearchDialog({
|
|
|
282
316
|
const formRef = useRef<React.ElementRef<'form'>>(null)
|
|
283
317
|
const panelRef = useRef<React.ElementRef<'div'>>(null)
|
|
284
318
|
const inputRef = useRef<React.ElementRef<typeof SearchInput>>(null)
|
|
319
|
+
const pathname = usePathname()
|
|
320
|
+
const searchParams = useSearchParams()
|
|
321
|
+
const chat = useChatAssistant()
|
|
322
|
+
const [mode, setMode] = useState<'search' | 'chat'>('search')
|
|
285
323
|
const { autocomplete, autocompleteState } = useAutocomplete({
|
|
286
324
|
onNavigate() {
|
|
287
325
|
onNavigate()
|
|
288
326
|
setOpen(false)
|
|
289
327
|
},
|
|
328
|
+
onAsk: askSearchQuery,
|
|
329
|
+
chatEnabled: chat.enabled,
|
|
290
330
|
})
|
|
291
|
-
|
|
292
|
-
|
|
331
|
+
|
|
332
|
+
function askSearchQuery(query: string) {
|
|
333
|
+
if (!chat.enabled) return
|
|
334
|
+
captureDocsEvent('docs_chat_opened', { path: window.location.pathname })
|
|
335
|
+
setMode('chat')
|
|
336
|
+
if (query.trim().length >= 3) void chat.ask(query)
|
|
337
|
+
else chat.setDraft(query)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function showChat() {
|
|
341
|
+
if (!chat.enabled) return
|
|
342
|
+
const query = autocompleteState.query?.trim()
|
|
343
|
+
if (query) chat.setDraft(query)
|
|
344
|
+
captureDocsEvent('docs_chat_opened', { path: window.location.pathname })
|
|
345
|
+
setMode('chat')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function showSearch() {
|
|
349
|
+
if (chat.draft.trim() && !autocompleteState.query?.trim()) autocomplete.setQuery(chat.draft)
|
|
350
|
+
setMode('search')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
if (chat.enabled && autocompleteState.query) void autocomplete.refresh()
|
|
355
|
+
}, [chat.enabled])
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!open || !autocompleteState.isOpen || !autocompleteState.collections) return
|
|
359
|
+
const query = autocompleteState.query?.trim()
|
|
360
|
+
if (!query) return
|
|
361
|
+
const resultCount = autocompleteState.collections.reduce((count, collection) => count + collection.items.filter(item => item.kind !== 'ask').length, 0)
|
|
362
|
+
const timeout = window.setTimeout(() => {
|
|
363
|
+
captureDocsEvent('docs_search', { query, result_count: resultCount, path: pathname })
|
|
364
|
+
}, 600)
|
|
365
|
+
return () => window.clearTimeout(timeout)
|
|
366
|
+
}, [open, autocompleteState.query, autocompleteState.collections, autocompleteState.isOpen, pathname])
|
|
293
367
|
|
|
294
368
|
useEffect(() => {
|
|
295
369
|
setOpen(false)
|
|
296
370
|
}, [pathname, searchParams, setOpen])
|
|
297
371
|
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (!open) setMode('search')
|
|
374
|
+
}, [open])
|
|
375
|
+
|
|
298
376
|
useEffect(() => {
|
|
299
377
|
if (open) {
|
|
300
378
|
return
|
|
@@ -331,9 +409,9 @@ function SearchDialog({
|
|
|
331
409
|
<div className="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh] max-w-[calc(100vw-16px)]">
|
|
332
410
|
<DialogPanel
|
|
333
411
|
transition
|
|
334
|
-
className="max-sm:mt-[
|
|
412
|
+
className="dory-search-panel max-sm:mt-[4vh] mx-auto transform-gpu overflow-hidden data-closed:scale-95 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:max-w-5xl"
|
|
335
413
|
>
|
|
336
|
-
<div {...autocomplete.getRootProps({})}>
|
|
414
|
+
{mode === 'search' ? <div {...autocomplete.getRootProps({})}>
|
|
337
415
|
<form
|
|
338
416
|
ref={formRef}
|
|
339
417
|
{...autocomplete.getFormProps({
|
|
@@ -345,13 +423,15 @@ function SearchDialog({
|
|
|
345
423
|
autocomplete={autocomplete}
|
|
346
424
|
autocompleteState={autocompleteState}
|
|
347
425
|
onClose={() => setOpen(false)}
|
|
426
|
+
onAsk={showChat}
|
|
427
|
+
chatEnabled={chat.enabled}
|
|
348
428
|
/>
|
|
349
429
|
<div
|
|
350
430
|
ref={panelRef}
|
|
351
|
-
className="border-t border-border
|
|
431
|
+
className="max-h-[60vh] overflow-y-auto border-t border-[var(--border)] empty:hidden"
|
|
352
432
|
{...autocomplete.getPanelProps({})}
|
|
353
433
|
>
|
|
354
|
-
{autocompleteState.isOpen && (
|
|
434
|
+
{autocompleteState.isOpen && autocompleteState.collections[0] && (
|
|
355
435
|
<SearchResults
|
|
356
436
|
autocomplete={autocomplete}
|
|
357
437
|
query={autocompleteState.query}
|
|
@@ -360,7 +440,21 @@ function SearchDialog({
|
|
|
360
440
|
)}
|
|
361
441
|
</div>
|
|
362
442
|
</form>
|
|
363
|
-
|
|
443
|
+
<div className="dory-search-footer" aria-hidden="true">
|
|
444
|
+
<span>↑ ↓ Navigate</span><span>↵ Open</span>{chat.enabled && <span>Tab Ask Dory</span>}<span>Esc Close</span>
|
|
445
|
+
</div>
|
|
446
|
+
</div> : <>
|
|
447
|
+
<div className="dory-search-tabs flex items-center gap-2 border-b border-[var(--border)] px-5 py-4 sm:px-7">
|
|
448
|
+
<div className="min-w-0 flex-1">
|
|
449
|
+
<h2 className="text-base font-semibold text-foreground">Ask Dory</h2>
|
|
450
|
+
<p className="text-xs text-muted-foreground">Grounded in these docs</p>
|
|
451
|
+
</div>
|
|
452
|
+
{chat.messages.length > 0 && <button type="button" onClick={chat.reset} className="rounded-lg px-3 py-2 text-sm text-muted-foreground hover:bg-[var(--accent)]">+ New</button>}
|
|
453
|
+
<button type="button" onClick={showSearch} className="rounded-lg px-3 py-2 text-sm text-muted-foreground hover:bg-[var(--accent)]">Search</button>
|
|
454
|
+
<span className="rounded-lg bg-[var(--brand-primary)] px-3 py-2 text-sm font-semibold text-white">Ask Dory</span>
|
|
455
|
+
</div>
|
|
456
|
+
<ChatAssistant chat={chat} onNavigate={() => { onNavigate(); setOpen(false) }} onSearch={showSearch} />
|
|
457
|
+
</>}
|
|
364
458
|
</DialogPanel>
|
|
365
459
|
</div>
|
|
366
460
|
</Dialog>
|
|
@@ -383,8 +477,9 @@ function useSearchProps() {
|
|
|
383
477
|
open,
|
|
384
478
|
setOpen: useCallback(
|
|
385
479
|
(open: boolean) => {
|
|
480
|
+
const trigger = inputRef.current?.closest('[data-slot="search-input-wrapper"]') ?? inputRef.current
|
|
386
481
|
const { width = 0, height = 0 } =
|
|
387
|
-
|
|
482
|
+
trigger?.getBoundingClientRect?.() ?? {}
|
|
388
483
|
if (!open || (width !== 0 && height !== 0)) {
|
|
389
484
|
setOpen(open)
|
|
390
485
|
}
|
|
@@ -407,8 +502,8 @@ export function Search() {
|
|
|
407
502
|
onClick={isEmbedded ? buttonProps.onClick : undefined}
|
|
408
503
|
>
|
|
409
504
|
<UxSearchInput
|
|
410
|
-
aria-label="
|
|
411
|
-
placeholder="
|
|
505
|
+
aria-label="Search documentation"
|
|
506
|
+
placeholder="Search documentation"
|
|
412
507
|
{...buttonProps}
|
|
413
508
|
/>
|
|
414
509
|
<Suspense fallback={null}>
|
|
@@ -426,16 +521,25 @@ export function MobileSearch() {
|
|
|
426
521
|
return (
|
|
427
522
|
<div
|
|
428
523
|
className={cn({
|
|
429
|
-
"
|
|
524
|
+
"inline-flex": !isEmbedded,
|
|
430
525
|
"[&_input]:hidden [&_svg]:mr-0 cursor-pointer": isEmbedded,
|
|
431
526
|
}, "lg:hidden")}
|
|
432
|
-
|
|
527
|
+
role="button"
|
|
528
|
+
tabIndex={0}
|
|
529
|
+
aria-label="Search documentation"
|
|
530
|
+
onClick={buttonProps.onClick}
|
|
531
|
+
onKeyDown={event => {
|
|
532
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
533
|
+
event.preventDefault()
|
|
534
|
+
buttonProps.onClick()
|
|
535
|
+
}
|
|
536
|
+
}}
|
|
433
537
|
>
|
|
434
538
|
<UxSearchInput
|
|
435
539
|
className="text-sm"
|
|
436
|
-
aria-label="
|
|
540
|
+
aria-label="Search documentation"
|
|
437
541
|
{...buttonProps}
|
|
438
|
-
placeholder="
|
|
542
|
+
placeholder="Search documentation"
|
|
439
543
|
/>
|
|
440
544
|
<Suspense fallback={null}>
|
|
441
545
|
<SearchDialog
|
package/src/components/store.ts
CHANGED
|
@@ -4,6 +4,7 @@ import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
|
4
4
|
import type { DoryConfig } from '../types/config';
|
|
5
5
|
import { addToSearchIndex } from './search-index';
|
|
6
6
|
import { withBase } from '../utils/base-path';
|
|
7
|
+
import { contentScope } from '../utils/content-scope';
|
|
7
8
|
|
|
8
9
|
const config = docsConfig as DoryConfig;
|
|
9
10
|
|
|
@@ -37,30 +38,39 @@ export interface Navigation {
|
|
|
37
38
|
pages: Array<{
|
|
38
39
|
title: string;
|
|
39
40
|
href: string;
|
|
41
|
+
badge?: string;
|
|
40
42
|
}>;
|
|
41
43
|
}>;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export
|
|
46
|
+
export function getNavigationForPath(pathname: string): Navigation[] {
|
|
47
|
+
const scope = contentScope(config, pathname);
|
|
48
|
+
return scope.navigation.tabs.map(tab => ({
|
|
45
49
|
title: tab.tab,
|
|
46
50
|
groups: tab.groups.map(group => ({
|
|
47
51
|
title: group.group,
|
|
48
|
-
pages: group.pages.map(
|
|
49
|
-
const
|
|
52
|
+
pages: group.pages.map(entry => {
|
|
53
|
+
const page = typeof entry === 'string' ? entry : entry.page;
|
|
54
|
+
const path = `${scope.prefix}/${page}`;
|
|
50
55
|
return {
|
|
51
56
|
title: page.charAt(0).toUpperCase() + page.slice(1).replace(/-/g, ' '),
|
|
52
|
-
href: path
|
|
57
|
+
href: path,
|
|
58
|
+
badge: typeof entry === 'string' ? undefined : entry.badge,
|
|
53
59
|
};
|
|
54
|
-
})
|
|
60
|
+
}).filter(page => !scope.prefix || Boolean(ALL_PAGES[page.href]))
|
|
55
61
|
}))
|
|
56
|
-
}));
|
|
62
|
+
})).filter(tab => tab.groups.some(group => group.pages.length > 0));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ALL_NAVIGATION: Navigation[] = getNavigationForPath('/');
|
|
57
66
|
|
|
58
67
|
export function pathFromFilename(filename: string): string {
|
|
59
|
-
|
|
68
|
+
const path = filename
|
|
60
69
|
.replace('../../docs', '')
|
|
61
70
|
.replace(/\/?index\.mdx$/, '/')
|
|
62
71
|
.replace(/\.mdx$/, '')
|
|
63
72
|
.toLowerCase();
|
|
73
|
+
return path.length > 1 ? path.replace(/\/$/, '') : path;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
|
|
@@ -77,7 +87,7 @@ if (typeof window !== 'undefined' && window.__DORY_FRONTMATTER__) {
|
|
|
77
87
|
}
|
|
78
88
|
delete window.__DORY_FRONTMATTER__;
|
|
79
89
|
// Build search index in the background after the page has fully rendered
|
|
80
|
-
setTimeout(() =>
|
|
90
|
+
setTimeout(() => void ensureSearchContent(), 2000);
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
// Preload frontmatter from JSON file (fallback when no inline data)
|
|
@@ -98,7 +108,7 @@ export async function preloadFrontmatter() {
|
|
|
98
108
|
|
|
99
109
|
// Build search index in the background after the page has fully rendered.
|
|
100
110
|
// Delay avoids competing with the route chunk for network connections.
|
|
101
|
-
setTimeout(() =>
|
|
111
|
+
setTimeout(() => void ensureSearchContent(), 2000);
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
114
|
} catch (error) {
|
|
@@ -106,6 +116,14 @@ export async function preloadFrontmatter() {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
let searchContentPromise: Promise<void> | undefined;
|
|
120
|
+
|
|
121
|
+
// Start immediately when search is used; otherwise load in the background.
|
|
122
|
+
export function ensureSearchContent(): Promise<void> {
|
|
123
|
+
searchContentPromise ??= addPreloadedContentToSearch();
|
|
124
|
+
return searchContentPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
// Add preloaded content to search index (runs in background, never blocks rendering).
|
|
110
128
|
// Fetches pre-built search-content.json generated at build time — no ?raw glob needed.
|
|
111
129
|
async function addPreloadedContentToSearch() {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
3
|
+
import { remarkSafeVars } from '../plugins/sanitize.ts';
|
|
4
|
+
import { rehypeCodeMeta } from '../plugins/code-meta.ts';
|
|
5
|
+
|
|
6
|
+
/** MDX plugins that work in both the static builder and the sandboxed runtime. */
|
|
7
|
+
export const getCommonMdxConfig = (development = false) => ({
|
|
8
|
+
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
9
|
+
rehypePlugins: [rehypeSlug, rehypeCodeMeta],
|
|
10
|
+
development,
|
|
11
|
+
});
|
package/src/config/mdx.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { remarkSafeVars } from '../plugins/sanitize';
|
|
4
|
-
import { rehypeCodeMeta } from '../plugins/code-meta';
|
|
5
|
-
import { remarkRelativeAssets } from '../plugins/relative-assets';
|
|
1
|
+
import { getCommonMdxConfig } from './mdx-common.ts';
|
|
2
|
+
import { remarkRelativeAssets } from '../plugins/relative-assets.ts';
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Shared MDX configuration used by the main build (vite.config.ts),
|
|
@@ -13,9 +10,11 @@ import { remarkRelativeAssets } from '../plugins/relative-assets';
|
|
|
13
10
|
* remark-relative-assets turns relative image paths into root paths so they
|
|
14
11
|
* work on pages served with a trailing slash.
|
|
15
12
|
*/
|
|
16
|
-
export const getMdxConfig = (isDevelopment: boolean = false) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
export const getMdxConfig = (isDevelopment: boolean = false) => {
|
|
14
|
+
const common = getCommonMdxConfig(isDevelopment);
|
|
15
|
+
return {
|
|
16
|
+
providerImportSource: '@mdx-js/preact',
|
|
17
|
+
...common,
|
|
18
|
+
remarkPlugins: [...common.remarkPlugins, remarkRelativeAssets],
|
|
19
|
+
};
|
|
20
|
+
};
|
package/src/embed/loader.ts
CHANGED
|
@@ -171,9 +171,6 @@ if (typeof window !== 'undefined' && window.DoryDocs) {
|
|
|
171
171
|
on(event: string, handler: Function) {
|
|
172
172
|
const eventKey = `on${event.charAt(0).toUpperCase()}${event.slice(1)}` as keyof DoryEmbedConfig;
|
|
173
173
|
(config as any)[eventKey] = handler;
|
|
174
|
-
if (widgetInstance) {
|
|
175
|
-
widgetInstance.on(event, handler);
|
|
176
|
-
}
|
|
177
174
|
return this;
|
|
178
175
|
},
|
|
179
176
|
};
|
package/src/embed/types.ts
CHANGED