@clidey/dory 0.1.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/.dockerignore +4 -0
- package/.github/ISSUE_TEMPLATE/anything-else.md +10 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/claude.yml +37 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +91 -0
- package/LICENSE +201 -0
- package/README.md +185 -0
- package/SECURITY.md +11 -0
- package/assets/dory_logo.png +0 -0
- package/bin/dory.js +131 -0
- package/docs/README.md +2 -0
- package/docs/configuration/app-settings/feature-flags.mdx +157 -0
- package/docs/configuration/app-settings/user-settings.mdx +201 -0
- package/docs/configuration/installation-setup/env-vars.mdx +198 -0
- package/docs/configuration/installation-setup/installation.mdx +127 -0
- package/docs/dory.json +162 -0
- package/docs/integrations/analytics-monitoring/error-tracking.mdx +199 -0
- package/docs/integrations/analytics-monitoring/notifications.mdx +189 -0
- package/docs/integrations/analytics-monitoring/user-analytics.mdx +182 -0
- package/docs/integrations/shopify-integration/shopify-setup.mdx +226 -0
- package/docs/integrations/shopify-integration/shopify-webhooks.mdx +207 -0
- package/docs/overview/developer-overview/dev-architecture.mdx +292 -0
- package/docs/overview/product-intro/app-overview.mdx +167 -0
- package/docs/overview/product-intro/architecture.mdx +203 -0
- package/docs/overview/product-intro/core-concepts.mdx +267 -0
- package/docs/platform-admin/billing-subscriptions/billing.mdx +176 -0
- package/docs/platform-admin/system-maintenance/db-migrations.mdx +184 -0
- package/docs/platform-admin/system-maintenance/health-monitoring.mdx +222 -0
- package/docs/platform-admin/system-maintenance/queue-management.mdx +234 -0
- package/docs/platform-admin/user-team-management/teams.mdx +170 -0
- package/docs/platform-admin/user-team-management/users.mdx +185 -0
- package/docs/support/legal/dependency-analysis.mdx +132 -0
- package/docs/support/legal/privacy-terms.mdx +73 -0
- package/docs/support/troubleshooting/common-issues.mdx +159 -0
- package/docs/usage/api-usage/api-inventory.mdx +213 -0
- package/docs/usage/api-usage/api-products.mdx +249 -0
- package/docs/usage/api-usage/api-purchase-orders.mdx +289 -0
- package/docs/usage/api-usage/api-reports.mdx +227 -0
- package/docs/usage/api-usage/api-suppliers.mdx +171 -0
- package/docs/usage/api-usage/api-user-management.mdx +171 -0
- package/docs/usage/ui-workflows/bundles.mdx +157 -0
- package/docs/usage/ui-workflows/demand-planning.mdx +210 -0
- package/docs/usage/ui-workflows/favorites.mdx +167 -0
- package/docs/usage/ui-workflows/inventory-management.mdx +179 -0
- package/docs/usage/ui-workflows/onboarding.mdx +215 -0
- package/docs/usage/ui-workflows/purchase-orders.mdx +200 -0
- package/docs/usage/ui-workflows/reports.mdx +257 -0
- package/index.html +54 -0
- package/k8s/Dockerfile +15 -0
- package/k8s/config/nginx-default.conf +22 -0
- package/k8s/dev.Dockerfile +16 -0
- package/package.json +71 -0
- package/src/app.css +3 -0
- package/src/app.tsx +13 -0
- package/src/components/dark-mode-toggle.tsx +25 -0
- package/src/components/dropdown.tsx +49 -0
- package/src/components/hooks.tsx +28 -0
- package/src/components/loading.tsx +23 -0
- package/src/components/mdx-provider.tsx +13 -0
- package/src/components/navigation.tsx +53 -0
- package/src/components/notification.tsx +97 -0
- package/src/components/routes.tsx +89 -0
- package/src/components/search-index.ts +43 -0
- package/src/components/search.tsx +508 -0
- package/src/components/store.ts +144 -0
- package/src/index.css +24 -0
- package/src/main.tsx +18 -0
- package/src/mdx/accordion.tsx +48 -0
- package/src/mdx/api-playground.tsx +674 -0
- package/src/mdx/api.tsx +354 -0
- package/src/mdx/callout.tsx +80 -0
- package/src/mdx/card.tsx +48 -0
- package/src/mdx/code.tsx +289 -0
- package/src/mdx/expandable.tsx +40 -0
- package/src/mdx/fence.tsx +73 -0
- package/src/mdx/icon.tsx +73 -0
- package/src/mdx/latex.tsx +26 -0
- package/src/mdx/list.tsx +7 -0
- package/src/mdx/mdx.tsx +43 -0
- package/src/mdx/mermaid.tsx +102 -0
- package/src/mdx/open-api.tsx +325 -0
- package/src/mdx/source.tsx +47 -0
- package/src/mdx/step.tsx +49 -0
- package/src/mdx/table.tsx +15 -0
- package/src/mdx/tag.tsx +66 -0
- package/src/mdx/text.tsx +98 -0
- package/src/plugins/sanitize.ts +15 -0
- package/src/theme.scss +124 -0
- package/src/ui/header.tsx +88 -0
- package/src/ui/layout.tsx +167 -0
- package/src/ui/mobile-navigation.tsx +91 -0
- package/src/ui/prev-next-link.tsx +74 -0
- package/src/ui/table-of-content.tsx +166 -0
- package/src/utils/fonts.ts +20 -0
- package/src/utils/functions.ts +12 -0
- package/src/utils/hooks.ts +46 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAutocomplete,
|
|
3
|
+
type AutocompleteApi,
|
|
4
|
+
type AutocompleteCollection,
|
|
5
|
+
type AutocompleteState,
|
|
6
|
+
} from '@algolia/autocomplete-core'
|
|
7
|
+
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
|
8
|
+
import {
|
|
9
|
+
forwardRef,
|
|
10
|
+
Fragment,
|
|
11
|
+
Suspense,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useId,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
type JSX,
|
|
18
|
+
} from 'react'
|
|
19
|
+
import Highlighter from 'react-highlight-words'
|
|
20
|
+
|
|
21
|
+
import { useMobileNavigation } from '../ui/mobile-navigation'
|
|
22
|
+
import { useLocation } from 'wouter-preact'
|
|
23
|
+
import classNames from 'classnames'
|
|
24
|
+
import { usePathname, useSearchParams } from './hooks'
|
|
25
|
+
import { ALL_NAVIGATION } from './store'
|
|
26
|
+
import { search } from './search-index'
|
|
27
|
+
|
|
28
|
+
type EmptyObject = Record<string, never>
|
|
29
|
+
type Result = any;
|
|
30
|
+
|
|
31
|
+
type Autocomplete = AutocompleteApi<
|
|
32
|
+
Result,
|
|
33
|
+
JSX.TargetedEvent,
|
|
34
|
+
JSX.TargetedMouseEvent<HTMLElement>,
|
|
35
|
+
JSX.TargetedKeyboardEvent<HTMLElement>
|
|
36
|
+
>
|
|
37
|
+
|
|
38
|
+
function useAutocomplete({ onNavigate }: { onNavigate: () => void }) {
|
|
39
|
+
const id = useId()
|
|
40
|
+
const [, nav] = useLocation()
|
|
41
|
+
const [autocompleteState, setAutocompleteState] = useState<
|
|
42
|
+
AutocompleteState<Result> | EmptyObject
|
|
43
|
+
>({})
|
|
44
|
+
|
|
45
|
+
function navigate({ itemUrl }: { itemUrl?: string }) {
|
|
46
|
+
if (itemUrl) {
|
|
47
|
+
nav(itemUrl);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onNavigate()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [autocomplete] = useState<Autocomplete>(() =>
|
|
54
|
+
createAutocomplete<
|
|
55
|
+
Result,
|
|
56
|
+
JSX.TargetedEvent,
|
|
57
|
+
JSX.TargetedMouseEvent<HTMLElement>,
|
|
58
|
+
JSX.TargetedKeyboardEvent<HTMLElement>
|
|
59
|
+
>({
|
|
60
|
+
id,
|
|
61
|
+
placeholder: 'Find something...',
|
|
62
|
+
defaultActiveItemId: 0,
|
|
63
|
+
onStateChange({ state }) {
|
|
64
|
+
setAutocompleteState(state)
|
|
65
|
+
},
|
|
66
|
+
shouldPanelOpen({ state }) {
|
|
67
|
+
return state.query !== ''
|
|
68
|
+
},
|
|
69
|
+
navigator: {
|
|
70
|
+
navigate,
|
|
71
|
+
},
|
|
72
|
+
getSources({ query }) {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
sourceId: 'documentation',
|
|
76
|
+
getItems() {
|
|
77
|
+
return search(query, { limit: 5 })
|
|
78
|
+
},
|
|
79
|
+
getItemUrl({ item }) {
|
|
80
|
+
return item.url
|
|
81
|
+
},
|
|
82
|
+
onSelect: navigate,
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return { autocomplete, autocompleteState }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function SearchIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
93
|
+
return (
|
|
94
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
|
95
|
+
<path
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeLinejoin="round"
|
|
98
|
+
d="M12.01 12a4.25 4.25 0 1 0-6.02-6 4.25 4.25 0 0 0 6.02 6Zm0 0 3.24 3.25"
|
|
99
|
+
/>
|
|
100
|
+
</svg>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function NoResultsIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
105
|
+
return (
|
|
106
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
|
107
|
+
<path
|
|
108
|
+
strokeLinecap="round"
|
|
109
|
+
strokeLinejoin="round"
|
|
110
|
+
d="M12.01 12a4.237 4.237 0 0 0 1.24-3c0-.62-.132-1.207-.37-1.738M12.01 12A4.237 4.237 0 0 1 9 13.25c-.635 0-1.237-.14-1.777-.388M12.01 12l3.24 3.25m-3.715-9.661a4.25 4.25 0 0 0-5.975 5.908M4.5 15.5l11-11"
|
|
111
|
+
/>
|
|
112
|
+
</svg>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function LoadingIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
117
|
+
const id = useId()
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
|
121
|
+
<circle cx="10" cy="10" r="5.5" strokeLinejoin="round" />
|
|
122
|
+
<path
|
|
123
|
+
stroke={`url(#${id})`}
|
|
124
|
+
strokeLinecap="round"
|
|
125
|
+
strokeLinejoin="round"
|
|
126
|
+
d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5"
|
|
127
|
+
/>
|
|
128
|
+
<defs>
|
|
129
|
+
<linearGradient
|
|
130
|
+
id={id}
|
|
131
|
+
x1="13"
|
|
132
|
+
x2="9.5"
|
|
133
|
+
y1="9"
|
|
134
|
+
y2="15"
|
|
135
|
+
gradientUnits="userSpaceOnUse"
|
|
136
|
+
>
|
|
137
|
+
<stop stopColor="currentColor" />
|
|
138
|
+
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
|
|
139
|
+
</linearGradient>
|
|
140
|
+
</defs>
|
|
141
|
+
</svg>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function HighlightQuery({ text, query }: { text: string; query: string }) {
|
|
146
|
+
return (
|
|
147
|
+
<Highlighter
|
|
148
|
+
highlightClassName="underline bg-transparent text-emerald-500"
|
|
149
|
+
searchWords={[query]}
|
|
150
|
+
autoEscape={true}
|
|
151
|
+
textToHighlight={text}
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function SearchResult({
|
|
157
|
+
result,
|
|
158
|
+
resultIndex,
|
|
159
|
+
autocomplete,
|
|
160
|
+
collection,
|
|
161
|
+
query,
|
|
162
|
+
}: {
|
|
163
|
+
result: Result
|
|
164
|
+
resultIndex: number
|
|
165
|
+
autocomplete: Autocomplete
|
|
166
|
+
collection: AutocompleteCollection<Result>
|
|
167
|
+
query: string
|
|
168
|
+
}) {
|
|
169
|
+
const id = useId()
|
|
170
|
+
|
|
171
|
+
const currentPage = result.url.split('#')[0];
|
|
172
|
+
|
|
173
|
+
// Get the group containing the current page
|
|
174
|
+
const group = ALL_NAVIGATION.find(group =>
|
|
175
|
+
group.groups.some(group =>
|
|
176
|
+
group.pages.some(page => page.href === currentPage)
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const page = group?.groups.find(group => group.pages.find(page => page.href === currentPage));
|
|
181
|
+
|
|
182
|
+
const sectionTitle = group?.title;
|
|
183
|
+
const pageTitle = page?.title;
|
|
184
|
+
|
|
185
|
+
const hierarchy: any[] = [sectionTitle, pageTitle].filter(
|
|
186
|
+
(x): x is string => typeof x === 'string',
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<li
|
|
191
|
+
className={classNames(
|
|
192
|
+
'group block cursor-default px-4 py-3 aria-selected:bg-zinc-50 dark:aria-selected:bg-zinc-800/50',
|
|
193
|
+
{
|
|
194
|
+
'border-t border-zinc-100 dark:border-zinc-800': resultIndex > 0,
|
|
195
|
+
}
|
|
196
|
+
)}
|
|
197
|
+
aria-labelledby={`${id}-hierarchy ${id}-title`}
|
|
198
|
+
{...autocomplete.getItemProps({
|
|
199
|
+
item: result,
|
|
200
|
+
source: collection.source,
|
|
201
|
+
})}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
id={`${id}-title`}
|
|
205
|
+
aria-hidden="true"
|
|
206
|
+
className="text-sm font-medium text-zinc-900 group-aria-selected:text-emerald-500 dark:text-white"
|
|
207
|
+
>
|
|
208
|
+
<HighlightQuery text={result.title} query={query} />
|
|
209
|
+
</div>
|
|
210
|
+
{hierarchy.length > 0 && (
|
|
211
|
+
<div
|
|
212
|
+
id={`${id}-hierarchy`}
|
|
213
|
+
aria-hidden="true"
|
|
214
|
+
className="mt-1 truncate text-2xs whitespace-nowrap text-zinc-500"
|
|
215
|
+
>
|
|
216
|
+
{hierarchy.map((item, itemIndex, items) => (
|
|
217
|
+
<Fragment key={itemIndex}>
|
|
218
|
+
<HighlightQuery text={item} query={query} />
|
|
219
|
+
<span
|
|
220
|
+
className={
|
|
221
|
+
itemIndex === items.length - 1
|
|
222
|
+
? 'sr-only'
|
|
223
|
+
: 'mx-2 text-zinc-300 dark:text-zinc-700'
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
/
|
|
227
|
+
</span>
|
|
228
|
+
</Fragment>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</li>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function SearchResults({
|
|
237
|
+
autocomplete,
|
|
238
|
+
query,
|
|
239
|
+
collection,
|
|
240
|
+
}: {
|
|
241
|
+
autocomplete: Autocomplete
|
|
242
|
+
query: string
|
|
243
|
+
collection: AutocompleteCollection<Result>
|
|
244
|
+
}) {
|
|
245
|
+
if (collection.items.length === 0) {
|
|
246
|
+
return (
|
|
247
|
+
<div className="p-6 text-center">
|
|
248
|
+
<NoResultsIcon className="mx-auto h-5 w-5 stroke-zinc-900 dark:stroke-zinc-600" />
|
|
249
|
+
<p className="mt-2 text-xs text-zinc-700 dark:text-zinc-400">
|
|
250
|
+
Nothing found for{' '}
|
|
251
|
+
<strong className="font-semibold break-words text-zinc-900 dark:text-white">
|
|
252
|
+
‘{query}’
|
|
253
|
+
</strong>
|
|
254
|
+
. Please try again.
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<ul {...autocomplete.getListProps()}>
|
|
262
|
+
{collection.items.map((result, resultIndex) => (
|
|
263
|
+
<SearchResult
|
|
264
|
+
key={result.url}
|
|
265
|
+
result={result}
|
|
266
|
+
resultIndex={resultIndex}
|
|
267
|
+
autocomplete={autocomplete}
|
|
268
|
+
collection={collection}
|
|
269
|
+
query={query}
|
|
270
|
+
/>
|
|
271
|
+
))}
|
|
272
|
+
</ul>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const SearchInput = forwardRef<
|
|
277
|
+
React.ElementRef<'input'>,
|
|
278
|
+
{
|
|
279
|
+
autocomplete: Autocomplete
|
|
280
|
+
autocompleteState: AutocompleteState<Result> | EmptyObject
|
|
281
|
+
onClose: () => void
|
|
282
|
+
}
|
|
283
|
+
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
|
|
284
|
+
const inputProps = autocomplete.getInputProps({ inputElement: null })
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className="group relative flex h-12">
|
|
288
|
+
<SearchIcon className="pointer-events-none absolute top-0 left-3 h-full w-5 stroke-zinc-500" />
|
|
289
|
+
<input
|
|
290
|
+
ref={inputRef}
|
|
291
|
+
data-autofocus
|
|
292
|
+
className={classNames(
|
|
293
|
+
'flex-auto appearance-none bg-transparent pl-10 text-zinc-900 outline-hidden placeholder:text-zinc-500 focus:w-full focus:flex-none sm:text-sm dark:text-white [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden',
|
|
294
|
+
{
|
|
295
|
+
'pr-11': autocompleteState.status === 'stalled',
|
|
296
|
+
'pr-4': autocompleteState.status !== 'stalled',
|
|
297
|
+
}
|
|
298
|
+
)}
|
|
299
|
+
{...inputProps}
|
|
300
|
+
onKeyDown={(event) => {
|
|
301
|
+
if (
|
|
302
|
+
event.key === 'Escape' &&
|
|
303
|
+
!autocompleteState.isOpen &&
|
|
304
|
+
autocompleteState.query === ''
|
|
305
|
+
) {
|
|
306
|
+
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
|
|
307
|
+
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
|
|
308
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
309
|
+
document.activeElement.blur()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
onClose()
|
|
313
|
+
} else {
|
|
314
|
+
inputProps.onKeyDown(event)
|
|
315
|
+
}
|
|
316
|
+
}}
|
|
317
|
+
/>
|
|
318
|
+
{autocompleteState.status === 'stalled' && (
|
|
319
|
+
<div className="absolute inset-y-0 right-3 flex items-center">
|
|
320
|
+
<LoadingIcon className="h-5 w-5 animate-spin stroke-zinc-200 text-zinc-900 dark:stroke-zinc-800 dark:text-emerald-400" />
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
function SearchDialog({
|
|
328
|
+
open,
|
|
329
|
+
setOpen,
|
|
330
|
+
className,
|
|
331
|
+
onNavigate = () => {},
|
|
332
|
+
}: {
|
|
333
|
+
open: boolean
|
|
334
|
+
setOpen: (open: boolean) => void
|
|
335
|
+
className?: string
|
|
336
|
+
onNavigate?: () => void
|
|
337
|
+
}) {
|
|
338
|
+
const formRef = useRef<React.ElementRef<'form'>>(null)
|
|
339
|
+
const panelRef = useRef<React.ElementRef<'div'>>(null)
|
|
340
|
+
const inputRef = useRef<React.ElementRef<typeof SearchInput>>(null)
|
|
341
|
+
const { autocomplete, autocompleteState } = useAutocomplete({
|
|
342
|
+
onNavigate() {
|
|
343
|
+
onNavigate()
|
|
344
|
+
setOpen(false)
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
const pathname = usePathname()
|
|
348
|
+
const searchParams = useSearchParams()
|
|
349
|
+
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
setOpen(false)
|
|
352
|
+
}, [pathname, searchParams, setOpen])
|
|
353
|
+
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (open) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
360
|
+
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
|
361
|
+
event.preventDefault()
|
|
362
|
+
setOpen(true)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
window.addEventListener('keydown', onKeyDown)
|
|
367
|
+
|
|
368
|
+
return () => {
|
|
369
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
370
|
+
}
|
|
371
|
+
}, [open, setOpen])
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<Dialog
|
|
375
|
+
open={open}
|
|
376
|
+
onClose={() => {
|
|
377
|
+
setOpen(false)
|
|
378
|
+
autocomplete.setQuery('')
|
|
379
|
+
}}
|
|
380
|
+
className={classNames('fixed inset-0 z-50', className)}
|
|
381
|
+
>
|
|
382
|
+
<DialogBackdrop
|
|
383
|
+
transition
|
|
384
|
+
className="fixed inset-0 bg-zinc-400/25 backdrop-blur-xs data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-black/40"
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
<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)]">
|
|
388
|
+
<DialogPanel
|
|
389
|
+
transition
|
|
390
|
+
className="max-sm:mt-[25vh] mx-auto transform-gpu overflow-hidden rounded-lg bg-zinc-50 shadow-xl ring-1 ring-zinc-900/7.5 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-xl dark:bg-zinc-900 dark:ring-zinc-800"
|
|
391
|
+
>
|
|
392
|
+
<div {...autocomplete.getRootProps({})}>
|
|
393
|
+
<form
|
|
394
|
+
ref={formRef}
|
|
395
|
+
{...autocomplete.getFormProps({
|
|
396
|
+
inputElement: inputRef.current,
|
|
397
|
+
})}
|
|
398
|
+
>
|
|
399
|
+
<SearchInput
|
|
400
|
+
ref={inputRef}
|
|
401
|
+
autocomplete={autocomplete}
|
|
402
|
+
autocompleteState={autocompleteState}
|
|
403
|
+
onClose={() => setOpen(false)}
|
|
404
|
+
/>
|
|
405
|
+
<div
|
|
406
|
+
ref={panelRef}
|
|
407
|
+
className="border-t border-zinc-200 bg-white empty:hidden dark:border-zinc-100/5 dark:bg-white/2.5"
|
|
408
|
+
{...autocomplete.getPanelProps({})}
|
|
409
|
+
>
|
|
410
|
+
{autocompleteState.isOpen && (
|
|
411
|
+
<SearchResults
|
|
412
|
+
autocomplete={autocomplete}
|
|
413
|
+
query={autocompleteState.query}
|
|
414
|
+
collection={autocompleteState.collections[0]}
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</form>
|
|
419
|
+
</div>
|
|
420
|
+
</DialogPanel>
|
|
421
|
+
</div>
|
|
422
|
+
</Dialog>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function useSearchProps() {
|
|
427
|
+
const buttonRef = useRef<React.ElementRef<'button'>>(null)
|
|
428
|
+
const [open, setOpen] = useState(false)
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
buttonProps: {
|
|
432
|
+
ref: buttonRef,
|
|
433
|
+
onClick() {
|
|
434
|
+
setOpen(true)
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
dialogProps: {
|
|
438
|
+
open,
|
|
439
|
+
setOpen: useCallback(
|
|
440
|
+
(open: boolean) => {
|
|
441
|
+
const { width = 0, height = 0 } =
|
|
442
|
+
buttonRef.current?.getBoundingClientRect() ?? {}
|
|
443
|
+
if (!open || (width !== 0 && height !== 0)) {
|
|
444
|
+
setOpen(open)
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
[setOpen],
|
|
448
|
+
),
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function Search() {
|
|
454
|
+
const [modifierKey, setModifierKey] = useState<string>()
|
|
455
|
+
const { buttonProps, dialogProps } = useSearchProps()
|
|
456
|
+
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
setModifierKey(
|
|
459
|
+
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl ',
|
|
460
|
+
)
|
|
461
|
+
}, [])
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<div className="hidden lg:block lg:max-w-md lg:flex-auto">
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
className="hidden h-8 w-full items-center gap-2 rounded-full bg-white pr-3 pl-2 text-sm text-zinc-500 ring-1 ring-zinc-900/10 transition hover:ring-zinc-900/20 lg:flex dark:bg-white/5 dark:text-zinc-400 dark:ring-white/10 dark:ring-inset dark:hover:ring-white/20"
|
|
468
|
+
{...buttonProps}
|
|
469
|
+
>
|
|
470
|
+
<SearchIcon className="h-5 w-5 stroke-current" />
|
|
471
|
+
Can I help you find something?
|
|
472
|
+
<kbd className="ml-auto text-2xs text-zinc-400 dark:text-zinc-500">
|
|
473
|
+
<kbd className="font-sans">{modifierKey}</kbd>
|
|
474
|
+
<kbd className="font-sans">K</kbd>
|
|
475
|
+
</kbd>
|
|
476
|
+
</button>
|
|
477
|
+
<Suspense fallback={null}>
|
|
478
|
+
<SearchDialog className="hidden lg:block" {...dialogProps} />
|
|
479
|
+
</Suspense>
|
|
480
|
+
</div>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function MobileSearch() {
|
|
485
|
+
const { close } = useMobileNavigation()
|
|
486
|
+
const { buttonProps, dialogProps } = useSearchProps()
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<div className="contents lg:hidden">
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
className="relative flex size-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 lg:hidden dark:hover:bg-white/5"
|
|
493
|
+
aria-label="Find something..."
|
|
494
|
+
{...buttonProps}
|
|
495
|
+
>
|
|
496
|
+
<span className="absolute size-12 [@media(pointer:fine)]:hidden" />
|
|
497
|
+
<SearchIcon className="h-8 w-8 stroke-zinc-900 dark:stroke-white" />
|
|
498
|
+
</button>
|
|
499
|
+
<Suspense fallback={null}>
|
|
500
|
+
<SearchDialog
|
|
501
|
+
className="lg:hidden"
|
|
502
|
+
onNavigate={close}
|
|
503
|
+
{...dialogProps}
|
|
504
|
+
/>
|
|
505
|
+
</Suspense>
|
|
506
|
+
</div>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ComponentType } from 'preact';
|
|
2
|
+
import { type RouteComponentProps } from 'wouter-preact';
|
|
3
|
+
import docsConfig from '../../docs/dory.json';
|
|
4
|
+
import { searchIndex } from './search-index';
|
|
5
|
+
|
|
6
|
+
export const ALL_PAGES = Object.fromEntries(
|
|
7
|
+
Object.entries(import.meta.glob<{ default: ComponentType; }>('../../docs/**/*.mdx'))
|
|
8
|
+
.map(([path, loader]) => [pathFromFilename(path), loader])
|
|
9
|
+
);
|
|
10
|
+
export const ALL_OPENAPI = Object.fromEntries(
|
|
11
|
+
Object.entries(import.meta.glob<{ default: string }>('../../docs/**/openapi.json', { query: 'raw', eager: true }))
|
|
12
|
+
.map(([path, loader]) => [pathFromFilename(path), loader])
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export interface Route {
|
|
16
|
+
path: string;
|
|
17
|
+
component: ComponentType<RouteComponentProps<{ [param: number]: string | undefined; }>>;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Navigation {
|
|
23
|
+
title: string;
|
|
24
|
+
groups: Array<{
|
|
25
|
+
title: string;
|
|
26
|
+
pages: Array<{
|
|
27
|
+
title: string;
|
|
28
|
+
href: string;
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ALL_NAVIGATION: Navigation[] = docsConfig.navigation.tabs.map(tab => ({
|
|
34
|
+
title: tab.tab,
|
|
35
|
+
groups: tab.groups.map(group => ({
|
|
36
|
+
title: group.group,
|
|
37
|
+
pages: group.pages.map(page => {
|
|
38
|
+
const path = `/${page}`;
|
|
39
|
+
return {
|
|
40
|
+
title: page.charAt(0).toUpperCase() + page.slice(1).replace(/-/g, ' '),
|
|
41
|
+
href: path
|
|
42
|
+
};
|
|
43
|
+
})
|
|
44
|
+
}))
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
export function pathFromFilename(filename: string): string {
|
|
48
|
+
return filename
|
|
49
|
+
.replace('../../docs', '')
|
|
50
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
51
|
+
.replace(/\.mdx$/, '')
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseFrontMatter(content: string) {
|
|
56
|
+
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
57
|
+
if (!frontMatterMatch) return { data: {}, content };
|
|
58
|
+
|
|
59
|
+
const frontMatter = frontMatterMatch[1];
|
|
60
|
+
const data: Record<string, any> = {};
|
|
61
|
+
|
|
62
|
+
frontMatter.split('\n').forEach(line => {
|
|
63
|
+
const [key, ...valueParts] = line.split(':');
|
|
64
|
+
if (key && valueParts.length) {
|
|
65
|
+
const value = valueParts.join(':').trim();
|
|
66
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, ''); // Remove quotes if present
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { data, content: content.slice(frontMatterMatch[0].length) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export let completeFrontMatter: Record<string, any>[] = [];
|
|
74
|
+
|
|
75
|
+
// Load frontmatter for a specific pathname
|
|
76
|
+
export async function loadMDXFrontMatterForPath(pathname: string) {
|
|
77
|
+
if (completeFrontMatter.find(fm => fm.path === pathname)) return;
|
|
78
|
+
|
|
79
|
+
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
80
|
+
const fileEntries = Object.entries(mdxFiles);
|
|
81
|
+
|
|
82
|
+
const fileEntry = fileEntries.find(([path]) => pathFromFilename(path) === pathname);
|
|
83
|
+
if (!fileEntry) return;
|
|
84
|
+
|
|
85
|
+
const [path, loader] = fileEntry;
|
|
86
|
+
const rawContent = (await loader()).default;
|
|
87
|
+
const { data, content } = parseFrontMatter(rawContent);
|
|
88
|
+
const filename = pathFromFilename(path);
|
|
89
|
+
|
|
90
|
+
updateNavigationTitle(filename, data.title);
|
|
91
|
+
searchIndex.add({
|
|
92
|
+
url: filename,
|
|
93
|
+
title: data.title || '',
|
|
94
|
+
content,
|
|
95
|
+
pageTitle: data.title || ''
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const frontmatterItem = { ...data, path: filename };
|
|
99
|
+
completeFrontMatter = [...completeFrontMatter, frontmatterItem];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Load all remaining frontmatter (after initial load)
|
|
103
|
+
export async function loadAllMDXFrontMatter(pathname: string) {
|
|
104
|
+
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
105
|
+
const fileEntries = Object.entries(mdxFiles);
|
|
106
|
+
|
|
107
|
+
const remainingEntries = fileEntries.filter(([path]) => pathFromFilename(path) !== pathname);
|
|
108
|
+
const newFrontmatter: Record<string, any>[] = [];
|
|
109
|
+
|
|
110
|
+
await Promise.all(
|
|
111
|
+
remainingEntries.map(async ([path, loader]) => {
|
|
112
|
+
const rawContent = (await loader()).default;
|
|
113
|
+
const { data, content } = parseFrontMatter(rawContent);
|
|
114
|
+
const filename = pathFromFilename(path);
|
|
115
|
+
|
|
116
|
+
updateNavigationTitle(filename, data.title);
|
|
117
|
+
searchIndex.add({
|
|
118
|
+
url: filename,
|
|
119
|
+
title: data.title || '',
|
|
120
|
+
content,
|
|
121
|
+
pageTitle: data.title || ''
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
newFrontmatter.push({ ...data, path: filename });
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
completeFrontMatter = [...completeFrontMatter, ...newFrontmatter];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Shared helper
|
|
132
|
+
function updateNavigationTitle(filename: string, title?: string) {
|
|
133
|
+
if (!title) return;
|
|
134
|
+
|
|
135
|
+
for (const tab of ALL_NAVIGATION) {
|
|
136
|
+
for (const group of tab.groups) {
|
|
137
|
+
for (const page of group.pages) {
|
|
138
|
+
if (page.href === filename) {
|
|
139
|
+
page.title = title;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: var(--font-family, system-ui, Avenir, Helvetica, Arial, sans-serif);
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
|
|
6
|
+
font-synthesis: none;
|
|
7
|
+
text-rendering: optimizeLegibility;
|
|
8
|
+
-webkit-font-smoothing: antialiased;
|
|
9
|
+
-moz-osx-font-smoothing: grayscale;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
article h2:first-of-type {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
article hr:first-of-type {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
article img {
|
|
22
|
+
margin-top: 1rem;
|
|
23
|
+
margin-bottom: 1rem;
|
|
24
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { render } from 'preact'
|
|
2
|
+
import './index.css'
|
|
3
|
+
import { App } from './app.tsx'
|
|
4
|
+
import { withMDX } from './components/mdx-provider.tsx'
|
|
5
|
+
import { loadFonts } from './utils/fonts'
|
|
6
|
+
import { NotificationProvider } from './components/notification.tsx'
|
|
7
|
+
|
|
8
|
+
// Load fonts before rendering the app
|
|
9
|
+
loadFonts()
|
|
10
|
+
|
|
11
|
+
const AppWithMDX = withMDX(App)
|
|
12
|
+
|
|
13
|
+
render(
|
|
14
|
+
<NotificationProvider>
|
|
15
|
+
<AppWithMDX />
|
|
16
|
+
</NotificationProvider>,
|
|
17
|
+
document.getElementById('app')!
|
|
18
|
+
)
|