@kirchdev/duxt 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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/app.config.ts +13 -0
  4. package/app/app.vue +122 -0
  5. package/app/assets/css/duxt.css +555 -0
  6. package/app/assets/css/typeset.css +498 -0
  7. package/app/components/DuxtBrand.vue +53 -0
  8. package/app/components/DuxtBreadcrumb.vue +37 -0
  9. package/app/components/DuxtCodeBlock.vue +143 -0
  10. package/app/components/DuxtCopyPage.vue +117 -0
  11. package/app/components/DuxtFooter.vue +86 -0
  12. package/app/components/DuxtHeader.vue +496 -0
  13. package/app/components/DuxtJsonEditor.vue +359 -0
  14. package/app/components/DuxtLandingProse.vue +90 -0
  15. package/app/components/DuxtLandingShowcase.vue +295 -0
  16. package/app/components/DuxtLiveWindow.vue +343 -0
  17. package/app/components/DuxtLocale.vue +86 -0
  18. package/app/components/DuxtMetaList.vue +76 -0
  19. package/app/components/DuxtNavigation.vue +94 -0
  20. package/app/components/DuxtNavigationLink.vue +41 -0
  21. package/app/components/DuxtOpenApiCallbacks.vue +92 -0
  22. package/app/components/DuxtOpenApiClient.vue +1339 -0
  23. package/app/components/DuxtOpenApiMedia.vue +108 -0
  24. package/app/components/DuxtOpenApiMethod.vue +59 -0
  25. package/app/components/DuxtOpenApiParameters.vue +114 -0
  26. package/app/components/DuxtOpenApiResponses.vue +109 -0
  27. package/app/components/DuxtOpenApiSchema.vue +365 -0
  28. package/app/components/DuxtOpenApiSecurity.vue +115 -0
  29. package/app/components/DuxtPageFeedback.vue +56 -0
  30. package/app/components/DuxtPageInfo.vue +194 -0
  31. package/app/components/DuxtPageNav.vue +93 -0
  32. package/app/components/DuxtPreviewSkeleton.vue +262 -0
  33. package/app/components/DuxtProgress.vue +45 -0
  34. package/app/components/DuxtSearch.vue +271 -0
  35. package/app/components/DuxtSections.vue +65 -0
  36. package/app/components/DuxtShortcuts.vue +46 -0
  37. package/app/components/DuxtSkipLink.vue +34 -0
  38. package/app/components/DuxtToc.vue +86 -0
  39. package/app/components/DuxtTranslationBanner.vue +64 -0
  40. package/app/components/DuxtVersion.vue +168 -0
  41. package/app/components/DuxtVersionBanner.vue +109 -0
  42. package/app/components/OgImage/Duxt.satori.vue +82 -0
  43. package/app/components/content/Accordion.vue +37 -0
  44. package/app/components/content/AccordionItem.vue +46 -0
  45. package/app/components/content/Callout.vue +58 -0
  46. package/app/components/content/ChangelogGroup.vue +91 -0
  47. package/app/components/content/ChangelogReleases.vue +520 -0
  48. package/app/components/content/CodeGroup.vue +183 -0
  49. package/app/components/content/DevtoolsPanel.vue +107 -0
  50. package/app/components/content/Field.vue +50 -0
  51. package/app/components/content/FieldGroup.vue +8 -0
  52. package/app/components/content/FileTree.vue +92 -0
  53. package/app/components/content/Mermaid.vue +70 -0
  54. package/app/components/content/OpenApiOperation.vue +185 -0
  55. package/app/components/content/OpenApiOperations.vue +72 -0
  56. package/app/components/content/OpenApiOverview.vue +302 -0
  57. package/app/components/content/PackageManagers.vue +187 -0
  58. package/app/components/content/PageCards.vue +48 -0
  59. package/app/components/content/Partial.vue +65 -0
  60. package/app/components/content/Preview.vue +173 -0
  61. package/app/components/content/ProseA.vue +55 -0
  62. package/app/components/content/ProseH2.vue +53 -0
  63. package/app/components/content/ProseH3.vue +53 -0
  64. package/app/components/content/ProseH4.vue +53 -0
  65. package/app/components/content/ProseImg.vue +87 -0
  66. package/app/components/content/ProsePre.vue +58 -0
  67. package/app/components/content/ProseTable.vue +11 -0
  68. package/app/components/content/Since.vue +21 -0
  69. package/app/components/content/Steps.vue +20 -0
  70. package/app/components/content/Tab.vue +23 -0
  71. package/app/components/content/TabGroup.vue +58 -0
  72. package/app/components/ui/alert/Alert.vue +21 -0
  73. package/app/components/ui/alert/AlertDescription.vue +22 -0
  74. package/app/components/ui/alert/AlertTitle.vue +22 -0
  75. package/app/components/ui/alert/index.ts +24 -0
  76. package/app/components/ui/badge/Badge.vue +28 -0
  77. package/app/components/ui/badge/index.ts +52 -0
  78. package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  79. package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
  80. package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  81. package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
  82. package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
  83. package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  84. package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
  85. package/app/components/ui/breadcrumb/index.ts +7 -0
  86. package/app/components/ui/button/Button.vue +31 -0
  87. package/app/components/ui/button/index.ts +39 -0
  88. package/app/components/ui/card/Card.vue +22 -0
  89. package/app/components/ui/card/CardAction.vue +22 -0
  90. package/app/components/ui/card/CardContent.vue +14 -0
  91. package/app/components/ui/card/CardDescription.vue +17 -0
  92. package/app/components/ui/card/CardFooter.vue +17 -0
  93. package/app/components/ui/card/CardHeader.vue +22 -0
  94. package/app/components/ui/card/CardTitle.vue +17 -0
  95. package/app/components/ui/card/index.ts +7 -0
  96. package/app/components/ui/collapsible/Collapsible.vue +19 -0
  97. package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
  98. package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  99. package/app/components/ui/collapsible/index.ts +3 -0
  100. package/app/components/ui/command/Command.vue +98 -0
  101. package/app/components/ui/command/CommandDialog.vue +42 -0
  102. package/app/components/ui/command/CommandEmpty.vue +31 -0
  103. package/app/components/ui/command/CommandGroup.vue +52 -0
  104. package/app/components/ui/command/CommandInput.vue +45 -0
  105. package/app/components/ui/command/CommandItem.vue +85 -0
  106. package/app/components/ui/command/CommandList.vue +32 -0
  107. package/app/components/ui/command/CommandSeparator.vue +23 -0
  108. package/app/components/ui/command/CommandShortcut.vue +19 -0
  109. package/app/components/ui/command/index.ts +29 -0
  110. package/app/components/ui/dialog/Dialog.vue +15 -0
  111. package/app/components/ui/dialog/DialogClose.vue +12 -0
  112. package/app/components/ui/dialog/DialogContent.vue +61 -0
  113. package/app/components/ui/dialog/DialogDescription.vue +25 -0
  114. package/app/components/ui/dialog/DialogFooter.vue +30 -0
  115. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  116. package/app/components/ui/dialog/DialogOverlay.vue +28 -0
  117. package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
  118. package/app/components/ui/dialog/DialogTitle.vue +25 -0
  119. package/app/components/ui/dialog/DialogTrigger.vue +12 -0
  120. package/app/components/ui/dialog/index.ts +10 -0
  121. package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  122. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
  123. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
  124. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  125. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  126. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
  127. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  128. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
  129. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  130. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
  131. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  132. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
  133. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
  134. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  135. package/app/components/ui/dropdown-menu/index.ts +16 -0
  136. package/app/components/ui/input/Input.vue +35 -0
  137. package/app/components/ui/input/index.ts +1 -0
  138. package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
  139. package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
  140. package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
  141. package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
  142. package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
  143. package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
  144. package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
  145. package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
  146. package/app/components/ui/navigation-menu/index.ts +14 -0
  147. package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
  148. package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
  149. package/app/components/ui/scroll-area/index.ts +2 -0
  150. package/app/components/ui/select/Select.vue +15 -0
  151. package/app/components/ui/select/SelectContent.vue +59 -0
  152. package/app/components/ui/select/SelectGroup.vue +19 -0
  153. package/app/components/ui/select/SelectItem.vue +42 -0
  154. package/app/components/ui/select/SelectItemText.vue +12 -0
  155. package/app/components/ui/select/SelectLabel.vue +16 -0
  156. package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
  157. package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
  158. package/app/components/ui/select/SelectSeparator.vue +20 -0
  159. package/app/components/ui/select/SelectTrigger.vue +32 -0
  160. package/app/components/ui/select/SelectValue.vue +12 -0
  161. package/app/components/ui/select/index.ts +11 -0
  162. package/app/components/ui/separator/Separator.vue +30 -0
  163. package/app/components/ui/separator/index.ts +1 -0
  164. package/app/components/ui/sheet/Sheet.vue +15 -0
  165. package/app/components/ui/sheet/SheetClose.vue +12 -0
  166. package/app/components/ui/sheet/SheetContent.vue +68 -0
  167. package/app/components/ui/sheet/SheetDescription.vue +23 -0
  168. package/app/components/ui/sheet/SheetFooter.vue +15 -0
  169. package/app/components/ui/sheet/SheetHeader.vue +15 -0
  170. package/app/components/ui/sheet/SheetOverlay.vue +28 -0
  171. package/app/components/ui/sheet/SheetTitle.vue +23 -0
  172. package/app/components/ui/sheet/SheetTrigger.vue +12 -0
  173. package/app/components/ui/sheet/index.ts +8 -0
  174. package/app/components/ui/sidebar/Sidebar.vue +110 -0
  175. package/app/components/ui/sidebar/SidebarContent.vue +23 -0
  176. package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
  177. package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
  178. package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
  179. package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  180. package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
  181. package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
  182. package/app/components/ui/sidebar/SidebarInput.vue +19 -0
  183. package/app/components/ui/sidebar/SidebarInset.vue +23 -0
  184. package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
  185. package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
  186. package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
  187. package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
  188. package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  189. package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  190. package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  191. package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
  192. package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
  193. package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  194. package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
  195. package/app/components/ui/sidebar/SidebarRail.vue +35 -0
  196. package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
  197. package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
  198. package/app/components/ui/sidebar/index.ts +62 -0
  199. package/app/components/ui/sidebar/utils.ts +19 -0
  200. package/app/components/ui/skeleton/Skeleton.vue +17 -0
  201. package/app/components/ui/skeleton/index.ts +1 -0
  202. package/app/components/ui/sonner/Sonner.vue +67 -0
  203. package/app/components/ui/sonner/index.ts +1 -0
  204. package/app/components/ui/tabs/Tabs.vue +26 -0
  205. package/app/components/ui/tabs/TabsContent.vue +23 -0
  206. package/app/components/ui/tabs/TabsList.vue +28 -0
  207. package/app/components/ui/tabs/TabsTrigger.vue +30 -0
  208. package/app/components/ui/tabs/index.ts +4 -0
  209. package/app/components/ui/textarea/Textarea.vue +32 -0
  210. package/app/components/ui/textarea/index.ts +1 -0
  211. package/app/components/ui/tooltip/Tooltip.vue +15 -0
  212. package/app/components/ui/tooltip/TooltipContent.vue +49 -0
  213. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  214. package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
  215. package/app/components/ui/tooltip/index.ts +4 -0
  216. package/app/composables/useActiveHeading.ts +90 -0
  217. package/app/composables/useDuxtAnimatedHeight.ts +155 -0
  218. package/app/composables/useDuxtBreadcrumb.ts +60 -0
  219. package/app/composables/useDuxtChoice.ts +28 -0
  220. package/app/composables/useDuxtCollection.ts +89 -0
  221. package/app/composables/useDuxtConfig.ts +47 -0
  222. package/app/composables/useDuxtNavigation.ts +87 -0
  223. package/app/composables/useDuxtPageFocus.ts +30 -0
  224. package/app/composables/useDuxtPath.ts +42 -0
  225. package/app/composables/useDuxtSearch.ts +208 -0
  226. package/app/composables/useDuxtSection.ts +44 -0
  227. package/app/composables/useDuxtSectionRow.ts +49 -0
  228. package/app/composables/useDuxtShortcuts.ts +58 -0
  229. package/app/composables/useDuxtSiteUrl.ts +27 -0
  230. package/app/composables/useDuxtToast.ts +25 -0
  231. package/app/composables/useDuxtVersion.ts +138 -0
  232. package/app/composables/useFuzzySearch.ts +83 -0
  233. package/app/composables/usePackageManager.ts +4 -0
  234. package/app/composables/useRecentPages.ts +52 -0
  235. package/app/error.vue +133 -0
  236. package/app/layouts/changelog.vue +85 -0
  237. package/app/layouts/default.vue +23 -0
  238. package/app/layouts/docs.vue +62 -0
  239. package/app/layouts/reference.vue +85 -0
  240. package/app/lib/utils.ts +7 -0
  241. package/app/middleware/duxt-section-layout.global.ts +57 -0
  242. package/app/pages/[...slug].vue +441 -0
  243. package/app/pages/index.vue +479 -0
  244. package/app/types/duxt.d.ts +955 -0
  245. package/app/utils/aside.ts +16 -0
  246. package/app/utils/changelog.ts +182 -0
  247. package/app/utils/code-highlight.ts +116 -0
  248. package/app/utils/duxt-config.ts +185 -0
  249. package/app/utils/duxt-text.ts +135 -0
  250. package/app/utils/file-icons.ts +62 -0
  251. package/app/utils/generated-sections.ts +190 -0
  252. package/app/utils/generated-toc.ts +216 -0
  253. package/app/utils/heading-link.ts +32 -0
  254. package/app/utils/locale-path.ts +45 -0
  255. package/app/utils/navigation-tree.ts +206 -0
  256. package/app/utils/nearest-page.ts +72 -0
  257. package/app/utils/openapi.ts +1028 -0
  258. package/app/utils/package-command.ts +162 -0
  259. package/app/utils/request-samples.ts +635 -0
  260. package/app/utils/section-scope.ts +97 -0
  261. package/app/utils/sheet-navigation.ts +100 -0
  262. package/app/utils/source-file.ts +84 -0
  263. package/app/utils/version-choices.ts +68 -0
  264. package/app/utils/version-paths.ts +65 -0
  265. package/bin/duxt-report.mjs +45 -0
  266. package/components.json +19 -0
  267. package/content-cache.ts +193 -0
  268. package/content.config.ts +38 -0
  269. package/duxt-app-config.ts +124 -0
  270. package/highlight-langs.ts +68 -0
  271. package/i18n/i18n.config.ts +16 -0
  272. package/i18n/locales/de/duxt/changelog.json +17 -0
  273. package/i18n/locales/de/duxt/code.json +13 -0
  274. package/i18n/locales/de/duxt/defaults.json +22 -0
  275. package/i18n/locales/de/duxt/devtools.json +7 -0
  276. package/i18n/locales/de/duxt/error.json +10 -0
  277. package/i18n/locales/de/duxt/footer.json +7 -0
  278. package/i18n/locales/de/duxt/locale.json +7 -0
  279. package/i18n/locales/de/duxt/nav.json +17 -0
  280. package/i18n/locales/de/duxt/openapi.json +89 -0
  281. package/i18n/locales/de/duxt/page.json +34 -0
  282. package/i18n/locales/de/duxt/search.json +12 -0
  283. package/i18n/locales/de/duxt/shortcuts.json +11 -0
  284. package/i18n/locales/de/duxt/theme.json +7 -0
  285. package/i18n/locales/de/duxt/toc.json +7 -0
  286. package/i18n/locales/de/duxt/version.json +25 -0
  287. package/i18n/locales/en/duxt/changelog.json +17 -0
  288. package/i18n/locales/en/duxt/code.json +13 -0
  289. package/i18n/locales/en/duxt/defaults.json +22 -0
  290. package/i18n/locales/en/duxt/devtools.json +7 -0
  291. package/i18n/locales/en/duxt/error.json +10 -0
  292. package/i18n/locales/en/duxt/footer.json +7 -0
  293. package/i18n/locales/en/duxt/locale.json +7 -0
  294. package/i18n/locales/en/duxt/nav.json +17 -0
  295. package/i18n/locales/en/duxt/openapi.json +89 -0
  296. package/i18n/locales/en/duxt/page.json +34 -0
  297. package/i18n/locales/en/duxt/search.json +12 -0
  298. package/i18n/locales/en/duxt/shortcuts.json +11 -0
  299. package/i18n/locales/en/duxt/theme.json +7 -0
  300. package/i18n/locales/en/duxt/toc.json +7 -0
  301. package/i18n/locales/en/duxt/version.json +25 -0
  302. package/i18n/locales/es/duxt/changelog.json +17 -0
  303. package/i18n/locales/es/duxt/code.json +13 -0
  304. package/i18n/locales/es/duxt/defaults.json +22 -0
  305. package/i18n/locales/es/duxt/devtools.json +7 -0
  306. package/i18n/locales/es/duxt/error.json +10 -0
  307. package/i18n/locales/es/duxt/footer.json +7 -0
  308. package/i18n/locales/es/duxt/locale.json +7 -0
  309. package/i18n/locales/es/duxt/nav.json +17 -0
  310. package/i18n/locales/es/duxt/openapi.json +89 -0
  311. package/i18n/locales/es/duxt/page.json +34 -0
  312. package/i18n/locales/es/duxt/search.json +12 -0
  313. package/i18n/locales/es/duxt/shortcuts.json +11 -0
  314. package/i18n/locales/es/duxt/theme.json +7 -0
  315. package/i18n/locales/es/duxt/toc.json +7 -0
  316. package/i18n/locales/es/duxt/version.json +25 -0
  317. package/i18n/locales/fr/duxt/changelog.json +17 -0
  318. package/i18n/locales/fr/duxt/code.json +13 -0
  319. package/i18n/locales/fr/duxt/defaults.json +22 -0
  320. package/i18n/locales/fr/duxt/devtools.json +7 -0
  321. package/i18n/locales/fr/duxt/error.json +10 -0
  322. package/i18n/locales/fr/duxt/footer.json +7 -0
  323. package/i18n/locales/fr/duxt/locale.json +7 -0
  324. package/i18n/locales/fr/duxt/nav.json +17 -0
  325. package/i18n/locales/fr/duxt/openapi.json +89 -0
  326. package/i18n/locales/fr/duxt/page.json +34 -0
  327. package/i18n/locales/fr/duxt/search.json +12 -0
  328. package/i18n/locales/fr/duxt/shortcuts.json +11 -0
  329. package/i18n/locales/fr/duxt/theme.json +7 -0
  330. package/i18n/locales/fr/duxt/toc.json +7 -0
  331. package/i18n/locales/fr/duxt/version.json +25 -0
  332. package/i18n/locales/pt/duxt/changelog.json +17 -0
  333. package/i18n/locales/pt/duxt/code.json +13 -0
  334. package/i18n/locales/pt/duxt/defaults.json +22 -0
  335. package/i18n/locales/pt/duxt/devtools.json +7 -0
  336. package/i18n/locales/pt/duxt/error.json +10 -0
  337. package/i18n/locales/pt/duxt/footer.json +7 -0
  338. package/i18n/locales/pt/duxt/locale.json +7 -0
  339. package/i18n/locales/pt/duxt/nav.json +17 -0
  340. package/i18n/locales/pt/duxt/openapi.json +89 -0
  341. package/i18n/locales/pt/duxt/page.json +34 -0
  342. package/i18n/locales/pt/duxt/search.json +12 -0
  343. package/i18n/locales/pt/duxt/shortcuts.json +11 -0
  344. package/i18n/locales/pt/duxt/theme.json +7 -0
  345. package/i18n/locales/pt/duxt/toc.json +7 -0
  346. package/i18n/locales/pt/duxt/version.json +25 -0
  347. package/i18n/locales/pt-BR/duxt/error.json +8 -0
  348. package/i18n/locales/pt-BR/duxt/nav.json +10 -0
  349. package/i18n/locales/pt-BR/duxt/page.json +10 -0
  350. package/i18n/locales/pt-BR/duxt/search.json +11 -0
  351. package/i18n/locales/pt-BR/duxt/version.json +13 -0
  352. package/mdc.config.ts +28 -0
  353. package/modules/config.ts +440 -0
  354. package/modules/devtools.ts +126 -0
  355. package/modules/git-meta.ts +349 -0
  356. package/modules/redirects.ts +152 -0
  357. package/modules/validate.ts +137 -0
  358. package/nuxt.config.ts +530 -0
  359. package/openapi-model.ts +358 -0
  360. package/openapi-parse.ts +1052 -0
  361. package/package.json +161 -0
  362. package/public/devtools/cache.html +318 -0
  363. package/public/devtools/checks.html +309 -0
  364. package/public/devtools/config.html +461 -0
  365. package/public/devtools/i18n.html +309 -0
  366. package/public/devtools/pages.html +348 -0
  367. package/public/devtools/paths.html +315 -0
  368. package/public/devtools/redirects.html +309 -0
  369. package/public/devtools/search.html +324 -0
  370. package/public/devtools/sources.html +309 -0
  371. package/public/devtools/versions.html +318 -0
  372. package/report.ts +303 -0
  373. package/repository-root.ts +26 -0
  374. package/section-reports.ts +55 -0
  375. package/sections-changelog.ts +644 -0
  376. package/sections-openapi.ts +450 -0
  377. package/sections-resolve.ts +711 -0
  378. package/sections.ts +157 -0
  379. package/server/devtools/content.ts +120 -0
  380. package/server/devtools/context.ts +38 -0
  381. package/server/devtools/entry-path.ts +30 -0
  382. package/server/devtools/handler.ts +104 -0
  383. package/server/devtools/preview.ts +461 -0
  384. package/server/devtools/render/content.ts +233 -0
  385. package/server/devtools/render/sources.ts +414 -0
  386. package/server/devtools/render/system.ts +352 -0
  387. package/server/devtools/shell.ts +611 -0
  388. package/server/devtools/sources.ts +122 -0
  389. package/server/devtools/system.ts +178 -0
  390. package/server/mcp/tools/list-pages.ts +52 -0
  391. package/server/mcp/tools/read-page.ts +57 -0
  392. package/server/mcp/tools/search-docs.ts +74 -0
  393. package/server/middleware/raw-markdown.ts +86 -0
  394. package/server/routes/llms-full.txt.get.ts +75 -0
  395. package/server/routes/llms.txt.get.ts +63 -0
  396. package/server/routes/rss.xml.get.ts +125 -0
  397. package/server/utils/duxt-server-text.ts +46 -0
  398. package/sources-git.ts +113 -0
  399. package/sources-resolve.ts +908 -0
  400. package/sources.ts +287 -0
  401. package/validate-report.ts +494 -0
@@ -0,0 +1,208 @@
1
+ import type { DuxtSearchSection } from '@duxt/composables/useFuzzySearch';
2
+
3
+ /** A hit, with the source it came out of. */
4
+ export interface DuxtSearchHit extends DuxtSearchSection {
5
+ /** Undefined on a site with one source: there is nothing to distinguish. */
6
+ source?: { label: string; collection: string };
7
+ }
8
+
9
+ /**
10
+ * Search across every source, not just the one being read.
11
+ *
12
+ * A site pulling three repositories had three separate searches, and which one
13
+ * a reader got depended on which page they happened to be standing on. This is
14
+ * the thing the `sources` model makes possible and a single-repository theme
15
+ * cannot do at all.
16
+ *
17
+ * RANKED TOGETHER, NOT GROUPED PER REPOSITORY. The choice matters and it is not
18
+ * obvious. Grouping per repository asks the reader to know which project their
19
+ * answer is in — but if they knew that, the search would be doing less work
20
+ * than the sidebar. So the lists are interleaved by rank and each hit carries a
21
+ * badge saying where it came from: the best answers first, provenance kept.
22
+ * The source being read goes first in each round, because a reader searching
23
+ * inside a project usually means that project.
24
+ *
25
+ * ONE VERSION PER REPOSITORY. Searching every version returns each page as many
26
+ * times as there are versions, which buries the answer under its own history.
27
+ * So each repository contributes the version the reader is in, or its default.
28
+ */
29
+ export function useDuxtSearch() {
30
+ const duxt = useDuxtConfig();
31
+ const { source } = useDuxtCollection();
32
+
33
+ const sources = computed(() => duxt.resolvedSources ?? []);
34
+
35
+ /**
36
+ * The collections to search, one per repository.
37
+ *
38
+ * Fixed at setup, deliberately: each one needs its own `useSearchCollection`,
39
+ * and a composable list that grows with the route is not a thing. The
40
+ * manifest is build-time config and does not change while the app is running,
41
+ * so the set is complete from the start; which of them is FIRST still follows
42
+ * the route.
43
+ */
44
+ const searchable = sources.value.length
45
+ ? [
46
+ ...new Map(
47
+ sources.value.map((entry) => [entry.collection, entry])
48
+ ).values()
49
+ ]
50
+ : [];
51
+
52
+ const searches = searchable.map((entry) => ({
53
+ entry,
54
+ ...useSearchCollection(
55
+ entry.collection as unknown as Parameters<typeof useSearchCollection>[0],
56
+ {
57
+ // A table is flattened into one string, so its cells run together —
58
+ // `KeyWhat it controlstitleThe name in the navbar…`. Its content is
59
+ // still reachable through the page that holds it.
60
+ ignoredTags: ['table'],
61
+ immediate: false
62
+ }
63
+ )
64
+ }));
65
+
66
+ /**
67
+ * The approximate pass, one index per source.
68
+ *
69
+ * It used to run over the collection being read alone, which made a typo
70
+ * quietly narrow the search from every source back to one — the reader who
71
+ * most needed the other repositories searched was the reader who had
72
+ * mistyped. Each index is still built on first use and only on the path FTS
73
+ * could not answer, so a reader who types accurately downloads none of them.
74
+ */
75
+ const approximate = searchable.map((entry) => ({
76
+ entry,
77
+ search: useFuzzySearch(entry.collection as DuxtCollectionName, {
78
+ ignoredTags: ['table']
79
+ }).search
80
+ }));
81
+
82
+ /** One entry per repository: the version being read, else that repo's default. */
83
+ const active = computed(() => {
84
+ const current = source.value;
85
+ const byRepo = new Map<string, (typeof searchable)[number]>();
86
+
87
+ for (const entry of searchable) {
88
+ const key = entry.repo ?? '';
89
+ const chosen = byRepo.get(key);
90
+
91
+ if (entry.collection === current?.collection) {
92
+ byRepo.set(key, entry);
93
+ continue;
94
+ }
95
+
96
+ if (chosen?.collection === current?.collection) continue;
97
+ if (!chosen || (entry.isDefault && !chosen.isDefault)) {
98
+ byRepo.set(key, entry);
99
+ }
100
+ }
101
+
102
+ // The source being read leads; the rest keep the config's own order.
103
+ return [...byRepo.values()].sort((a, b) =>
104
+ a.collection === current?.collection
105
+ ? -1
106
+ : b.collection === current?.collection
107
+ ? 1
108
+ : 0
109
+ );
110
+ });
111
+
112
+ /** Only worth a badge when there is more than one thing to tell apart. */
113
+ const labelled = computed(() => active.value.length > 1);
114
+
115
+ const labelOf = (entry: (typeof searchable)[number]) =>
116
+ [entry.repo, entry.version].filter(Boolean).join(' ') ||
117
+ entry.prefix ||
118
+ '/';
119
+
120
+ async function init() {
121
+ await Promise.all(
122
+ searches
123
+ .filter((search) => active.value.includes(search.entry))
124
+ .filter((search) => search.status.value === 'idle')
125
+ .map((search) => search.init())
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Interleave by rank.
131
+ *
132
+ * Without scores to compare across collections — each database ranks inside
133
+ * itself — position is the only comparable thing there is. Round-robin over
134
+ * the lists gives every source its best hit before any source gets its
135
+ * second, which is the closest honest approximation of "ranked together".
136
+ */
137
+ function interleave(lists: DuxtSearchHit[][], limit: number) {
138
+ const merged: DuxtSearchHit[] = [];
139
+ const depth = Math.max(0, ...lists.map((list) => list.length));
140
+
141
+ for (let rank = 0; rank < depth && merged.length < limit; rank++) {
142
+ for (const list of lists) {
143
+ const hit = list[rank];
144
+ if (hit) merged.push(hit);
145
+ if (merged.length >= limit) break;
146
+ }
147
+ }
148
+
149
+ return merged;
150
+ }
151
+
152
+ async function search(
153
+ term: string,
154
+ limit = 20
155
+ ): Promise<{ hits: DuxtSearchHit[]; approximate: boolean }> {
156
+ if (!term.trim()) return { hits: [], approximate: false };
157
+
158
+ const wanted = active.value;
159
+
160
+ const lists = await Promise.all(
161
+ searches
162
+ .filter((entry) => wanted.includes(entry.entry))
163
+ .map(async (entry) => {
164
+ const hits = (await entry.search(term, {
165
+ limit
166
+ })) as DuxtSearchSection[];
167
+
168
+ return hits.map((hit) => ({
169
+ ...hit,
170
+ source: labelled.value
171
+ ? {
172
+ label: labelOf(entry.entry),
173
+ collection: entry.entry.collection
174
+ }
175
+ : undefined
176
+ }));
177
+ })
178
+ );
179
+
180
+ const merged = interleave(lists, limit);
181
+ if (merged.length) return { hits: merged, approximate: false };
182
+
183
+ // FTS matches terms and prefixes, not near-misses, so one wrong letter
184
+ // leaves the reader with an empty box. Fuse gets a second look, over the
185
+ // same sources and merged the same way.
186
+ const fuzzy = await Promise.all(
187
+ approximate
188
+ .filter((entry) => wanted.includes(entry.entry))
189
+ .map(async (entry) => {
190
+ const hits = await entry.search(term, limit);
191
+
192
+ return hits.map((hit) => ({
193
+ ...hit,
194
+ source: labelled.value
195
+ ? {
196
+ label: labelOf(entry.entry),
197
+ collection: entry.entry.collection
198
+ }
199
+ : undefined
200
+ }));
201
+ })
202
+ );
203
+
204
+ return { hits: interleave(fuzzy, limit), approximate: true };
205
+ }
206
+
207
+ return { search, init, labelled };
208
+ }
@@ -0,0 +1,44 @@
1
+ import type { ContentNavigationItem } from '@nuxt/content';
2
+ import type { Ref } from 'vue';
3
+
4
+ /**
5
+ * The documentation is split into sections, each owning a branch of the tree —
6
+ * so the sidebar shows one branch, not everything. Which branch follows from
7
+ * the path, falling back to the whole tree so a page outside any section still
8
+ * has a sidebar.
9
+ */
10
+ export function useDuxtSection(
11
+ navigation: Ref<ContentNavigationItem[] | null | undefined>
12
+ ) {
13
+ const path = useDuxtPath();
14
+ const duxt = useDuxtConfig();
15
+
16
+ /**
17
+ * BOTH ROWS, sections first.
18
+ *
19
+ * A generated section can declare that its entry belongs in the top navbar
20
+ * rather than in the section row (`navigation: 'navigation'`), and a site can
21
+ * put an ordinary page there by hand. Neither stops being the branch its
22
+ * pages sit in — but reading only `sections` said it did: the sidebar fell
23
+ * back to the whole tree, and the breadcrumb lost its head, so a release page
24
+ * printed its own version and nothing above it.
25
+ */
26
+ /**
27
+ * LONGEST MATCH, not the first one written. `/demo` and `/demo/api` both
28
+ * prefix a page under the reference, and taking the first said the reader was
29
+ * in the prose beside it — so the sidebar drew that tree instead of the
30
+ * endpoints. Same rule, and the same bug, as `sourceForPath`.
31
+ */
32
+ const section = computed(() =>
33
+ currentSection(
34
+ [...(duxt.sections ?? []), ...(duxt.navigation ?? [])],
35
+ path.value
36
+ )
37
+ );
38
+
39
+ const items = computed(() =>
40
+ sectionItems(navigation.value ?? [], section.value?.to, path.value)
41
+ );
42
+
43
+ return { section, items };
44
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Is the second navbar row showing?
3
+ *
4
+ * Two things need the answer and must not disagree about it: `DuxtSections`,
5
+ * which draws the row only where one of its sections is current — a page
6
+ * outside every section, or a generated section whose entry the site put in the
7
+ * top row instead, would otherwise get the whole row with nothing marked — and
8
+ * the LAYOUTS, whose sticky columns are offset by exactly the header above
9
+ * them. Answered in one place because a mismatch is silent: the row vanishes
10
+ * and the sidebars keep the gap where it used to be.
11
+ */
12
+ export function useDuxtSectionRow() {
13
+ const duxt = useDuxtConfig();
14
+ const path = useDuxtPath();
15
+
16
+ /**
17
+ * THIS AREA'S ENTRIES, not every entry the site declared — see
18
+ * `sectionsForPath`. A second source has top-level parts of its own, and the
19
+ * row is "the parts of what you are reading" rather than a site index.
20
+ */
21
+ const sections = computed(() =>
22
+ sectionsForPath(duxt.sections ?? [], duxt.resolvedSources ?? [], path.value)
23
+ );
24
+
25
+ const visible = computed(() =>
26
+ sections.value.some(
27
+ (section) => section.to && path.value.startsWith(section.to)
28
+ )
29
+ );
30
+
31
+ /**
32
+ * How far down the page the header reaches — the navbar, plus the section row
33
+ * where there is one — as a length the layouts hand down in
34
+ * `--duxt-header-offset`.
35
+ *
36
+ * NOT `chrome`, which is what this was called. The word is right in the sense
37
+ * an interface means it and wrong in the sense most readers reach for first,
38
+ * and a name that has to be explained is a name chosen badly.
39
+ * `DuxtLiveWindow`'s `chrome` prop keeps it, because there it really is a
40
+ * drawn browser bar.
41
+ *
42
+ * A CSS variable rather than a class per case: the sticky columns live in
43
+ * three layouts and in the page's own contents column, and four copies of a
44
+ * pair of arbitrary values is four places to forget one.
45
+ */
46
+ const headerOffset = computed(() => (visible.value ? '6.5rem' : '3.75rem'));
47
+
48
+ return { sections, visible, headerOffset };
49
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * One place that knows which keys the theme listens to.
3
+ *
4
+ * The overlay on `?` and the handlers themselves have to agree, and the only
5
+ * way they do is by being the same list — a shortcut sheet that lists a key
6
+ * nothing binds is worse than no sheet.
7
+ *
8
+ * A shortcut never fires while the reader is typing. Without that guard, `?`
9
+ * in the search box opens the overlay instead of being searched for, which is
10
+ * the single most common bug in this kind of feature.
11
+ */
12
+ export interface DuxtShortcut {
13
+ /** The key, as `KeyboardEvent.key` reports it. */
14
+ key: string;
15
+ /** Requires ⌘ on a Mac, Ctrl elsewhere. */
16
+ meta?: boolean;
17
+ /** i18n key for what it does. */
18
+ label: string;
19
+ /** How to draw it. */
20
+ keys: string[];
21
+ }
22
+
23
+ export const duxtShortcuts: DuxtShortcut[] = [
24
+ { key: 'k', meta: true, label: 'duxt.shortcuts.search', keys: ['⌘', 'K'] },
25
+ { key: '?', label: 'duxt.shortcuts.help', keys: ['?'] },
26
+ { key: '[', label: 'duxt.shortcuts.previous', keys: ['['] },
27
+ { key: ']', label: 'duxt.shortcuts.next', keys: [']'] }
28
+ ];
29
+
30
+ /** Is the reader typing? Then the key belongs to whatever they are typing in. */
31
+ export function isTyping(target: EventTarget | null): boolean {
32
+ const element = target as HTMLElement | null;
33
+ if (!element) return false;
34
+
35
+ return (
36
+ element.isContentEditable ||
37
+ ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)
38
+ );
39
+ }
40
+
41
+ /** Bind one key, with the guards every one of them needs. */
42
+ export function onDuxtShortcut(
43
+ match: (event: KeyboardEvent) => boolean,
44
+ run: (event: KeyboardEvent) => void
45
+ ) {
46
+ onMounted(() => {
47
+ const handler = (event: KeyboardEvent) => {
48
+ if (isTyping(event.target)) return;
49
+ if (!match(event)) return;
50
+
51
+ event.preventDefault();
52
+ run(event);
53
+ };
54
+
55
+ window.addEventListener('keydown', handler);
56
+ onBeforeUnmount(() => window.removeEventListener('keydown', handler));
57
+ });
58
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The site's own origin, and paths made absolute against it.
3
+ *
4
+ * duxt cannot guess this — a layer has no idea what domain it will be served
5
+ * from — so it reads `i18n.baseUrl`, the one place a Nuxt site already has to
6
+ * state it for hreflang. Where it is unset, `absolute()` hands back the path
7
+ * unchanged: a relative `canonical` is valid and resolves correctly, whereas a
8
+ * guessed origin in a `canonical` is actively wrong. Same stance `app.vue`
9
+ * takes on the alternate links, for the same reason.
10
+ *
11
+ * Set `NUXT_PUBLIC_I18N_BASE_URL` in the deployment, or `i18n.baseUrl` in the
12
+ * consumer's nuxt.config.
13
+ */
14
+ export function useDuxtSiteUrl() {
15
+ const runtime = useRuntimeConfig().public as {
16
+ i18n?: { baseUrl?: string };
17
+ };
18
+
19
+ const origin = computed(
20
+ () => runtime.i18n?.baseUrl?.replace(/\/+$/, '') ?? ''
21
+ );
22
+
23
+ const absolute = (path: string) =>
24
+ origin.value ? `${origin.value}${path}` : path;
25
+
26
+ return { origin, absolute };
27
+ }
@@ -0,0 +1,25 @@
1
+ import { toast } from 'vue-sonner';
2
+
3
+ /**
4
+ * Toasts, wrapped so components never import vue-sonner directly.
5
+ *
6
+ * The wrapper is the seam: a consumer that wants a different notification
7
+ * system replaces this one composable instead of every call site, and the
8
+ * layer's own components keep working. Each level maps to Sonner's own type,
9
+ * which picks the icon and its status colour.
10
+ */
11
+ export function useDuxtToast() {
12
+ return {
13
+ success: (message: string, description?: string) =>
14
+ toast.success(message, { description }),
15
+ info: (message: string, description?: string) =>
16
+ toast.info(message, { description }),
17
+ warning: (message: string, description?: string) =>
18
+ toast.warning(message, { description }),
19
+ error: (message: string, description?: string) =>
20
+ toast.error(message, { description }),
21
+ /** A toast that stays until the promise settles. */
22
+ loading: (message: string) => toast.loading(message),
23
+ dismiss: (id?: number | string) => toast.dismiss(id)
24
+ };
25
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * What the reader needs to be told about the version they are in.
3
+ *
4
+ * Three questions, one answer, because they are the same question asked by
5
+ * different parts of the page: the banner needs to know whether this version is
6
+ * still the one to read, the head needs to know whether a crawler should index
7
+ * it, and the switcher needs a badge per entry. All three follow from the
8
+ * resolved source manifest — which version serves this path, which one serves
9
+ * it without a prefix, and what the config says each is worth.
10
+ */
11
+ export function useDuxtVersion() {
12
+ const duxt = useDuxtConfig();
13
+ const path = useDuxtPath();
14
+
15
+ const sources = computed(() => duxt.resolvedSources ?? []);
16
+
17
+ /** The source serving this route. */
18
+ const current = computed(() => sourceForPath(path.value, sources.value));
19
+
20
+ /**
21
+ * The version to send the reader to instead — the default of the SAME
22
+ * repository. Another project's default says nothing about this one.
23
+ *
24
+ * A VERSION-NEUTRAL GENERATED SECTION is its own answer. A changelog is one
25
+ * global history served at a URL with no version in it, so there is no other
26
+ * version of it to prefer: left to the search below it would have matched the
27
+ * documentation's default instead, and the page would then have carried
28
+ * `noindex` and a canonical pointing at a URL that does not exist.
29
+ */
30
+ const preferred = computed(() =>
31
+ current.value?.generated?.versioning === 'global'
32
+ ? current.value
33
+ : sources.value.find(
34
+ (source) =>
35
+ source.isDefault &&
36
+ source.generated?.versioning !== 'global' &&
37
+ source.repo === current.value?.repo &&
38
+ // The default of the same ARTEFACT — see `sameArtefact`. Without
39
+ // it a reference page's canonical pointed at the documentation's
40
+ // default, which is a different document at a different URL.
41
+ sameArtefact(source, current.value)
42
+ )
43
+ );
44
+
45
+ /** Is this the version a first-time reader should be on? */
46
+ const isPreferred = computed(
47
+ () => !current.value || current.value === preferred.value
48
+ );
49
+
50
+ const status = computed(() => current.value?.status ?? 'current');
51
+
52
+ /**
53
+ * The versions the switcher may offer — see `versionChoices`.
54
+ *
55
+ * Here rather than in `DuxtVersion` because the mobile sheet asks the same
56
+ * question to decide WHERE the version goes: a full-width select in its own
57
+ * strip when there is a choice, a badge beside the brand when there is not.
58
+ */
59
+ const choices = computed(() =>
60
+ versionChoices(sources.value, current.value, duxt.versions)
61
+ );
62
+
63
+ /** Is there anything to switch to? */
64
+ const switchable = computed(() => choices.value.length > 1);
65
+
66
+ /**
67
+ * WHICH KIND of "not the current version" this is.
68
+ *
69
+ * `isDefault` says only "not the one served by default", and that covers two
70
+ * readers who need opposite things told: someone on a release from two years
71
+ * ago should be told to upgrade, and someone on the branch documenting next
72
+ * month's should be told that what they are reading may still change. Telling
73
+ * the second one to upgrade is exactly backwards, which is what the banner
74
+ * did before this.
75
+ *
76
+ * The config wins where it speaks — `status: 'upcoming'` on a branch called
77
+ * `next` is a fact only the maintainer has. Where it does not, semver decides,
78
+ * and where semver cannot (a branch beside a tag) the version is treated as
79
+ * old: a tag next to a default branch is almost always a past release, and
80
+ * the maintainer can say otherwise in one word.
81
+ */
82
+ const kind = computed<'current' | 'upcoming' | 'old' | 'deprecated' | 'eol'>(
83
+ () => {
84
+ if (status.value === 'eol') return 'eol';
85
+ if (status.value === 'deprecated') return 'deprecated';
86
+ if (status.value === 'upcoming') return 'upcoming';
87
+ if (isPreferred.value) return 'current';
88
+
89
+ return versionRelation(
90
+ current.value?.version,
91
+ preferred.value?.version
92
+ ) === 'newer'
93
+ ? 'upcoming'
94
+ : 'old';
95
+ }
96
+ );
97
+
98
+ /**
99
+ * A crawler is as likely to serve v0.7.0 as the current docs, and nothing on
100
+ * the page says which it is. So an older or dead version points at the
101
+ * current one and asks not to be indexed itself — `follow`, because the links
102
+ * out of it are still worth crawling.
103
+ */
104
+ const shouldIndex = computed(
105
+ () => isPreferred.value && status.value !== 'eol'
106
+ );
107
+
108
+ /** The same page in the preferred version, for the banner and the canonical. */
109
+ const preferredPath = computed(() =>
110
+ versionPath(
111
+ path.value,
112
+ current.value?.prefix,
113
+ preferred.value?.prefix || '/'
114
+ )
115
+ );
116
+
117
+ const shouldWarn = computed(
118
+ () =>
119
+ Boolean(current.value?.version) &&
120
+ (!isPreferred.value ||
121
+ status.value === 'deprecated' ||
122
+ status.value === 'eol')
123
+ );
124
+
125
+ return {
126
+ sources,
127
+ choices,
128
+ switchable,
129
+ current,
130
+ preferred,
131
+ isPreferred,
132
+ status,
133
+ kind,
134
+ shouldIndex,
135
+ shouldWarn,
136
+ preferredPath
137
+ };
138
+ }
@@ -0,0 +1,83 @@
1
+ import type Fuse from 'fuse.js';
2
+ import type { MaybeRefOrGetter } from 'vue';
3
+
4
+ /** One entry of Content's search index: a page, or a heading inside one. */
5
+ export interface DuxtSearchSection {
6
+ id: string;
7
+ title: string;
8
+ titles: string[];
9
+ level: number;
10
+ content: string;
11
+ }
12
+
13
+ /**
14
+ * Typo tolerance beside Content's FTS index.
15
+ *
16
+ * FTS5 matches terms and prefixes, never near-misses, so a single wrong letter
17
+ * returns nothing at all. Fuse matches approximately over the same sections the
18
+ * database indexes.
19
+ *
20
+ * Both the library and the sections are loaded on the first query the database
21
+ * cannot answer, not when the dialog opens: a reader who types accurately never
22
+ * downloads either. The sections come through Content's client database, which
23
+ * the FTS index has already loaded by then, so the fallback costs no second
24
+ * payload — only Fuse itself.
25
+ */
26
+ export function useFuzzySearch(
27
+ /**
28
+ * Which collection to fall back over. A versioned site names its collections
29
+ * after the prefix they serve, so this follows `useDuxtCollection()` once the
30
+ * dialog does — it is passed rather than resolved here to keep the exact and
31
+ * the approximate pass reading the same one.
32
+ */
33
+ collection: MaybeRefOrGetter<DuxtCollectionName> = 'docs' as DuxtCollectionName,
34
+ options?: { ignoredTags?: string[] }
35
+ ) {
36
+ let index: Promise<Fuse<DuxtSearchSection>> | undefined;
37
+ let indexedFor: string | undefined;
38
+
39
+ async function build(name: DuxtCollectionName) {
40
+ const [{ default: Fuse }, sections] = await Promise.all([
41
+ import('fuse.js'),
42
+ queryCollectionSearchSections(name as DuxtCollectionArg, options)
43
+ ]);
44
+
45
+ return new Fuse(sections as DuxtSearchSection[], {
46
+ // A hit in the heading outranks one buried in a paragraph, and the
47
+ // headings above a section still say more than its body text.
48
+ keys: [
49
+ { name: 'title', weight: 3 },
50
+ { name: 'titles', weight: 2 },
51
+ { name: 'content', weight: 1 }
52
+ ],
53
+
54
+ // By default Fuse scores a match by how near the start of the field it
55
+ // sits, which says nothing about a paragraph — only the quality of the
56
+ // match should count here.
57
+ ignoreLocation: true,
58
+
59
+ // Loose enough to survive a transposed letter, tight enough that a short
60
+ // term does not drag in half the documentation.
61
+ threshold: 0.35,
62
+ minMatchCharLength: 3
63
+ });
64
+ }
65
+
66
+ async function search(term: string, limit = 20) {
67
+ const name = toValue(collection);
68
+
69
+ // Built on first use, and again when the route moves to another collection
70
+ // — otherwise a versioned site would keep answering out of the version the
71
+ // reader happened to arrive from.
72
+ if (!index || name !== indexedFor) {
73
+ indexedFor = name;
74
+ index = build(name);
75
+ }
76
+
77
+ const fuse = await index;
78
+
79
+ return fuse.search(term, { limit }).map((hit) => hit.item);
80
+ }
81
+
82
+ return { search };
83
+ }
@@ -0,0 +1,4 @@
1
+ /** The package manager the reader picked — see `useDuxtChoice`. */
2
+ export function usePackageManager() {
3
+ return useDuxtChoice('package-manager');
4
+ }