@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.
Files changed (104) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/ISSUE_TEMPLATE/anything-else.md +10 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. package/.github/dependabot.yml +11 -0
  6. package/.github/workflows/claude.yml +37 -0
  7. package/CODE_OF_CONDUCT.md +128 -0
  8. package/CONTRIBUTING.md +91 -0
  9. package/LICENSE +201 -0
  10. package/README.md +185 -0
  11. package/SECURITY.md +11 -0
  12. package/assets/dory_logo.png +0 -0
  13. package/bin/dory.js +131 -0
  14. package/docs/README.md +2 -0
  15. package/docs/configuration/app-settings/feature-flags.mdx +157 -0
  16. package/docs/configuration/app-settings/user-settings.mdx +201 -0
  17. package/docs/configuration/installation-setup/env-vars.mdx +198 -0
  18. package/docs/configuration/installation-setup/installation.mdx +127 -0
  19. package/docs/dory.json +162 -0
  20. package/docs/integrations/analytics-monitoring/error-tracking.mdx +199 -0
  21. package/docs/integrations/analytics-monitoring/notifications.mdx +189 -0
  22. package/docs/integrations/analytics-monitoring/user-analytics.mdx +182 -0
  23. package/docs/integrations/shopify-integration/shopify-setup.mdx +226 -0
  24. package/docs/integrations/shopify-integration/shopify-webhooks.mdx +207 -0
  25. package/docs/overview/developer-overview/dev-architecture.mdx +292 -0
  26. package/docs/overview/product-intro/app-overview.mdx +167 -0
  27. package/docs/overview/product-intro/architecture.mdx +203 -0
  28. package/docs/overview/product-intro/core-concepts.mdx +267 -0
  29. package/docs/platform-admin/billing-subscriptions/billing.mdx +176 -0
  30. package/docs/platform-admin/system-maintenance/db-migrations.mdx +184 -0
  31. package/docs/platform-admin/system-maintenance/health-monitoring.mdx +222 -0
  32. package/docs/platform-admin/system-maintenance/queue-management.mdx +234 -0
  33. package/docs/platform-admin/user-team-management/teams.mdx +170 -0
  34. package/docs/platform-admin/user-team-management/users.mdx +185 -0
  35. package/docs/support/legal/dependency-analysis.mdx +132 -0
  36. package/docs/support/legal/privacy-terms.mdx +73 -0
  37. package/docs/support/troubleshooting/common-issues.mdx +159 -0
  38. package/docs/usage/api-usage/api-inventory.mdx +213 -0
  39. package/docs/usage/api-usage/api-products.mdx +249 -0
  40. package/docs/usage/api-usage/api-purchase-orders.mdx +289 -0
  41. package/docs/usage/api-usage/api-reports.mdx +227 -0
  42. package/docs/usage/api-usage/api-suppliers.mdx +171 -0
  43. package/docs/usage/api-usage/api-user-management.mdx +171 -0
  44. package/docs/usage/ui-workflows/bundles.mdx +157 -0
  45. package/docs/usage/ui-workflows/demand-planning.mdx +210 -0
  46. package/docs/usage/ui-workflows/favorites.mdx +167 -0
  47. package/docs/usage/ui-workflows/inventory-management.mdx +179 -0
  48. package/docs/usage/ui-workflows/onboarding.mdx +215 -0
  49. package/docs/usage/ui-workflows/purchase-orders.mdx +200 -0
  50. package/docs/usage/ui-workflows/reports.mdx +257 -0
  51. package/index.html +54 -0
  52. package/k8s/Dockerfile +15 -0
  53. package/k8s/config/nginx-default.conf +22 -0
  54. package/k8s/dev.Dockerfile +16 -0
  55. package/package.json +71 -0
  56. package/src/app.css +3 -0
  57. package/src/app.tsx +13 -0
  58. package/src/components/dark-mode-toggle.tsx +25 -0
  59. package/src/components/dropdown.tsx +49 -0
  60. package/src/components/hooks.tsx +28 -0
  61. package/src/components/loading.tsx +23 -0
  62. package/src/components/mdx-provider.tsx +13 -0
  63. package/src/components/navigation.tsx +53 -0
  64. package/src/components/notification.tsx +97 -0
  65. package/src/components/routes.tsx +89 -0
  66. package/src/components/search-index.ts +43 -0
  67. package/src/components/search.tsx +508 -0
  68. package/src/components/store.ts +144 -0
  69. package/src/index.css +24 -0
  70. package/src/main.tsx +18 -0
  71. package/src/mdx/accordion.tsx +48 -0
  72. package/src/mdx/api-playground.tsx +674 -0
  73. package/src/mdx/api.tsx +354 -0
  74. package/src/mdx/callout.tsx +80 -0
  75. package/src/mdx/card.tsx +48 -0
  76. package/src/mdx/code.tsx +289 -0
  77. package/src/mdx/expandable.tsx +40 -0
  78. package/src/mdx/fence.tsx +73 -0
  79. package/src/mdx/icon.tsx +73 -0
  80. package/src/mdx/latex.tsx +26 -0
  81. package/src/mdx/list.tsx +7 -0
  82. package/src/mdx/mdx.tsx +43 -0
  83. package/src/mdx/mermaid.tsx +102 -0
  84. package/src/mdx/open-api.tsx +325 -0
  85. package/src/mdx/source.tsx +47 -0
  86. package/src/mdx/step.tsx +49 -0
  87. package/src/mdx/table.tsx +15 -0
  88. package/src/mdx/tag.tsx +66 -0
  89. package/src/mdx/text.tsx +98 -0
  90. package/src/plugins/sanitize.ts +15 -0
  91. package/src/theme.scss +124 -0
  92. package/src/ui/header.tsx +88 -0
  93. package/src/ui/layout.tsx +167 -0
  94. package/src/ui/mobile-navigation.tsx +91 -0
  95. package/src/ui/prev-next-link.tsx +74 -0
  96. package/src/ui/table-of-content.tsx +166 -0
  97. package/src/utils/fonts.ts +20 -0
  98. package/src/utils/functions.ts +12 -0
  99. package/src/utils/hooks.ts +46 -0
  100. package/src/vite-env.d.ts +1 -0
  101. package/tsconfig.app.json +32 -0
  102. package/tsconfig.json +7 -0
  103. package/tsconfig.node.json +25 -0
  104. 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
+ &lsquo;{query}&rsquo;
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
+ )