@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,194 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Where the page came from, under the table of contents.
4
+ *
5
+ * Not at the foot of the ARTICLE, which is where it sat first and where it read
6
+ * as an afterthought below the prev/next links. "Edit this page", when it last
7
+ * changed and who wrote it are one question — provenance — and the right-hand
8
+ * column is already the one answering what a page IS rather than what it says.
9
+ *
10
+ * Everything here is derived. The `sources` entry carries the repository, the
11
+ * ref and the folder; the page carries its own file name and, where git could
12
+ * be asked, its history.
13
+ */
14
+ const props = defineProps<{
15
+ /**
16
+ * Lay it out as a ROW rather than a stack.
17
+ *
18
+ * The column form is the one this was written for; the row is what it needs
19
+ * where there is no column to sit in — under the article of a page whose
20
+ * layout fills the right-hand side itself, where a stack of three lines
21
+ * hanging off the left edge under a full-width rule reads as leftovers.
22
+ */
23
+ row?: boolean;
24
+ page:
25
+ | {
26
+ stem?: string;
27
+ /**
28
+ * The day a dated page names — a release, and nothing else so far.
29
+ * It REPLACES the last commit rather than joining it: every release of
30
+ * a changelog shares one file, so their last commit is the same date on
31
+ * all of them and says nothing about any.
32
+ */
33
+ date?: string;
34
+ /** The diff a release was cut from, where its heading linked one. */
35
+ compare?: string;
36
+ lastUpdated?: string;
37
+ contributors?: { name: string; commits: number; username?: string }[];
38
+ }
39
+ | null
40
+ | undefined;
41
+ }>();
42
+
43
+ const page = computed(() => props.page);
44
+
45
+ const { source } = useDuxtCollection();
46
+ const { locale, t } = useI18n();
47
+
48
+ const file = computed(() => {
49
+ // A GENERATED SECTION has no file per page: every page in it was split out of
50
+ // one artefact, and `path` is that artefact. Sending the reader to a file
51
+ // named after the URL would be a 404 with an edit form on it.
52
+ if (source.value?.generated) return source.value.path;
53
+
54
+ return page.value?.stem
55
+ ? sourceFilePath(
56
+ page.value.stem,
57
+ source.value?.prefix ?? '',
58
+ source.value?.path ?? 'docs'
59
+ )
60
+ : undefined;
61
+ });
62
+
63
+ const link = computed(() =>
64
+ file.value
65
+ ? sourceLink(
66
+ source.value?.repositoryUrl,
67
+ source.value?.ref,
68
+ source.value?.refKind,
69
+ file.value
70
+ )
71
+ : undefined
72
+ );
73
+
74
+ /**
75
+ * The day this page names, where it names one.
76
+ *
77
+ * `timeZone: 'UTC'`, unlike the commit date below it: a release date is a
78
+ * calendar day and not a moment, so read as local time it is the day before for
79
+ * every reader west of Greenwich — and a different day on the server than in
80
+ * the browser, which is a hydration mismatch as well as a wrong date. The
81
+ * commit date IS a moment and is left alone.
82
+ */
83
+ const released = computed(() => changelogDate(page.value?.date, locale.value));
84
+
85
+ const updated = computed(() => {
86
+ // The page's own date wins. Both lines would be true and only one is useful:
87
+ // a changelog's releases all live in one file, so the commit that last
88
+ // touched it dates every release the same day, which is at best the newest
89
+ // one's and at worst years off.
90
+ if (page.value?.date) return undefined;
91
+
92
+ const value = page.value?.lastUpdated;
93
+ if (!value) return undefined;
94
+
95
+ const date = new Date(value);
96
+ if (Number.isNaN(date.getTime())) return undefined;
97
+
98
+ return new Intl.DateTimeFormat(locale.value, { dateStyle: 'medium' }).format(
99
+ date
100
+ );
101
+ });
102
+
103
+ const contributors = computed(() => page.value?.contributors ?? []);
104
+
105
+ /**
106
+ * What this page lets a reader DO with where it came from.
107
+ *
108
+ * The compare link goes above the edit link, because the two are the same kind
109
+ * of thing pointing at the same repository — where the page came FROM, rather
110
+ * than what it says — and it is the one a reader of a release wants first: the
111
+ * notes are the summary, the diff is what actually shipped.
112
+ */
113
+ const actions = computed(() => {
114
+ const entries: { icon: string; label: string; to: string; external: true }[] =
115
+ [];
116
+
117
+ // The diff a release links, where its heading linked an absolute one — see
118
+ // the parser. `git-compare` rather than the crossed arrows: at 14 px beside a
119
+ // pencil the arrows read as a knot rather than as two branches.
120
+ if (page.value?.compare) {
121
+ entries.push({
122
+ icon: 'lucide:git-compare',
123
+ label: t('duxt.page.compare'),
124
+ to: page.value.compare,
125
+ external: true
126
+ });
127
+ }
128
+
129
+ // A tag has no edit form — see `sourceLink`. The label follows the kind
130
+ // rather than promising a form that answers with a 404.
131
+ if (link.value) {
132
+ entries.push({
133
+ icon: link.value.kind === 'edit' ? 'lucide:pencil' : 'lucide:file-code-2',
134
+ label: t(
135
+ link.value.kind === 'edit' ? 'duxt.page.edit' : 'duxt.page.view'
136
+ ),
137
+ to: link.value.url,
138
+ external: true
139
+ });
140
+ }
141
+
142
+ return entries;
143
+ });
144
+
145
+ /** The dated facts, in the order they are read. */
146
+ const notes = computed(() => {
147
+ const lines: string[] = [];
148
+
149
+ if (released.value) {
150
+ lines.push(t('duxt.page.released', { date: released.value }));
151
+ } else if (updated.value) {
152
+ lines.push(t('duxt.page.lastUpdated', { date: updated.value }));
153
+ }
154
+
155
+ return lines;
156
+ });
157
+ </script>
158
+
159
+ <template>
160
+ <!-- One block, one spacing rule — see `DuxtMetaList`. The contributors are
161
+ the one part that is neither an action nor a line of text, so they go in
162
+ the slot and are spaced as one more item. -->
163
+ <DuxtMetaList :row="row" :actions="actions" :notes="notes">
164
+ <div v-if="contributors.length" :class="row ? '' : 'space-y-1.5'">
165
+ <p>{{ $t('duxt.page.contributors') }}</p>
166
+
167
+ <ul class="flex flex-wrap items-center gap-x-2 gap-y-1.5">
168
+ <li
169
+ v-for="person in contributors"
170
+ :key="person.name"
171
+ class="flex items-center gap-1.5"
172
+ >
173
+ <img
174
+ v-if="person.username"
175
+ :src="`https://github.com/${person.username}.png?size=40`"
176
+ alt=""
177
+ width="18"
178
+ height="18"
179
+ loading="lazy"
180
+ class="size-[18px] rounded-full border"
181
+ />
182
+ <span
183
+ v-else
184
+ aria-hidden="true"
185
+ class="flex size-[18px] items-center justify-center rounded-full border bg-muted text-[9px] font-medium"
186
+ >
187
+ {{ person.name.slice(0, 1).toUpperCase() }}
188
+ </span>
189
+ <span>{{ person.name }}</span>
190
+ </li>
191
+ </ul>
192
+ </div>
193
+ </DuxtMetaList>
194
+ </template>
@@ -0,0 +1,93 @@
1
+ <script setup lang="ts">
2
+ import type { ContentNavigationItem } from '@nuxt/content';
3
+ // Previous and next within the current section only. Content's own
4
+ // surroundings query walks the whole collection in one flat order, so the last
5
+ // page of one section would offer a "next" that lands in another — crossing a
6
+ // section is the section row's job, not a link reading "the next page".
7
+ const props = defineProps<{ path: string }>();
8
+
9
+ const { data: navigation } = await useDuxtNavigation();
10
+
11
+ const { items } = useDuxtSection(navigation);
12
+ const localeLink = useDuxtLink();
13
+
14
+ /** The section's pages in reading order, groups flattened into their children. */
15
+ const pages = computed(() => {
16
+ const flat: ContentNavigationItem[] = [];
17
+
18
+ const walk = (entries: ContentNavigationItem[]) => {
19
+ for (const entry of entries) {
20
+ if (entry.page !== false) flat.push(entry);
21
+ if (entry.children?.length) walk(entry.children);
22
+ }
23
+ };
24
+
25
+ walk(items.value);
26
+ return flat;
27
+ });
28
+
29
+ const index = computed(() =>
30
+ pages.value.findIndex((page) => page.path === props.path)
31
+ );
32
+ const previous = computed(() =>
33
+ index.value > 0 ? pages.value[index.value - 1] : undefined
34
+ );
35
+ const next = computed(() =>
36
+ index.value >= 0 ? pages.value[index.value + 1] : undefined
37
+ );
38
+
39
+ /**
40
+ * `[` and `]` move through the section the same way the two links do.
41
+ *
42
+ * Bound here rather than in the layout because this component already knows
43
+ * which pages are adjacent — a second computation of that would be a second
44
+ * chance to disagree with what the links say.
45
+ */
46
+ const router = useRouter();
47
+
48
+ onDuxtShortcut(
49
+ (event) =>
50
+ (event.key === '[' || event.key === ']') &&
51
+ !event.metaKey &&
52
+ !event.ctrlKey,
53
+ (event) => {
54
+ const target = event.key === '[' ? previous.value : next.value;
55
+ if (target?.path) void router.push(localeLink(target.path)!);
56
+ }
57
+ );
58
+ </script>
59
+
60
+ <template>
61
+ <nav
62
+ v-if="previous || next"
63
+ class="mt-16 grid gap-4 border-t pt-8 sm:grid-cols-2"
64
+ :aria-label="$t('duxt.nav.pagination')"
65
+ >
66
+ <NuxtLink
67
+ v-if="previous"
68
+ :to="localeLink(previous.path)"
69
+ class="flex flex-col gap-1 rounded-lg border p-4 transition-colors hover:bg-accent"
70
+ >
71
+ <span class="flex items-center gap-1 text-xs text-muted-foreground">
72
+ <Icon name="lucide:arrow-left" class="size-3.5" />
73
+ {{ $t('duxt.nav.previous') }}
74
+ </span>
75
+ <span class="font-medium">{{ previous.title }}</span>
76
+ </NuxtLink>
77
+ <span v-else class="hidden sm:block" />
78
+
79
+ <NuxtLink
80
+ v-if="next"
81
+ :to="localeLink(next.path)"
82
+ class="flex flex-col gap-1 rounded-lg border p-4 text-right transition-colors hover:bg-accent"
83
+ >
84
+ <span
85
+ class="flex items-center justify-end gap-1 text-xs text-muted-foreground"
86
+ >
87
+ {{ $t('duxt.nav.next') }}
88
+ <Icon name="lucide:arrow-right" class="size-3.5" />
89
+ </span>
90
+ <span class="font-medium">{{ next.title }}</span>
91
+ </NuxtLink>
92
+ </nav>
93
+ </template>
@@ -0,0 +1,262 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The SHAPE of a documentation page, for the moment before the real one is up.
4
+ *
5
+ * The landing page's window frames a second copy of the application, and
6
+ * between the band arriving and that copy booting there was an empty box the
7
+ * height of a page. A spinner would have said "wait"; this says what is coming.
8
+ *
9
+ * DRAWN RATHER THAN PHOTOGRAPHED, and that is the whole point of it. A
10
+ * screenshot has to exist, has to be produced twice for the two themes, and is
11
+ * wrong the day the theme changes — none of which a layer can ask of every site
12
+ * that extends it. This is the theme's own tokens in the theme's own measures,
13
+ * so it is correct in light and dark by construction and needs no asset. A site
14
+ * that would rather show a real picture sets `duxt.landing.preview.src`.
15
+ *
16
+ * SIZED BY ITS BOX, NOT BY THE WINDOW. Every breakpoint here is a container
17
+ * query — `@2xl` where the page says `sm`, `@5xl` where it says `lg`, `@7xl`
18
+ * where it says `xl`, all at the same widths. It has to be: this poster is
19
+ * drawn in the LANDING PAGE's document, so a viewport breakpoint asks how wide
20
+ * the browser is when the question is how wide the window is. Half-width, in a
21
+ * showcase band on a wide screen, that drew the full desktop page — two
22
+ * sidebars, a section row — squeezed into a 30 rem box. The frame underneath
23
+ * has no such problem: an iframe carries a viewport of its own.
24
+ *
25
+ * THE MEASURES ARE THE REAL ONES, not a sketch: `h-14` for the header, `w-56`
26
+ * for each of the two columns, the section row at `lg` where `DuxtSections`
27
+ * appears, the same `gap-8` and `max-w-[90rem]` the docs layout sets. A poster
28
+ * whose columns sit somewhere else than the page's is a poster the reader
29
+ * watches jump when the frame takes over — which is the one thing it exists to
30
+ * prevent.
31
+ *
32
+ * Nothing here is content: no words, no links, no headings. It is `aria-hidden`
33
+ * where it stands, and every bar is a rounded rectangle.
34
+ *
35
+ * THREE SHAPES, because the window points at three kinds of page and one shape
36
+ * fitted only the first. A poster whose layout is not the layout that arrives
37
+ * is worse than a plain box: it says a page is coming and then a different one
38
+ * does, which reads as the frame having loaded the wrong thing.
39
+ */
40
+ withDefaults(
41
+ defineProps<{
42
+ /**
43
+ * `docs` — a Markdown page: two sidebars, prose, a code card.
44
+ * `api` — an operation list: a run of method-and-path rows.
45
+ * `text` — a plain-text artefact such as `llms.txt`: no chrome at all,
46
+ * because the browser renders one in a bare monospace document.
47
+ */
48
+ variant?: DuxtSkeletonVariant;
49
+ }>(),
50
+ { variant: 'docs' }
51
+ );
52
+ </script>
53
+
54
+ <template>
55
+ <!-- A PLAIN-TEXT ARTEFACT. No header, no sidebars, no article measures: the
56
+ browser renders `text/plain` as a bare monospace document, and drawing a
57
+ documentation page in front of one is a promise the frame then breaks.
58
+ Ragged line lengths, because that is what a file looks like. -->
59
+ <div
60
+ v-if="variant === 'text'"
61
+ class="flex h-full w-full flex-col gap-2.5 overflow-hidden bg-background p-4 font-mono"
62
+ >
63
+ <div
64
+ v-for="(width, index) in [
65
+ 22, 0, 74, 68, 0, 30, 0, 88, 82, 90, 76, 84, 70, 86, 62, 0, 26, 0, 80,
66
+ 72, 88, 66, 84, 78, 90, 58
67
+ ]"
68
+ :key="index"
69
+ class="h-2.5 shrink-0 rounded"
70
+ :class="width ? 'bg-muted/50' : ''"
71
+ :style="width ? { width: `${width}%` } : { height: '0.25rem' }"
72
+ />
73
+ </div>
74
+
75
+ <div v-else class="flex h-full w-full flex-col bg-background">
76
+ <!-- The header: the wordmark, the navbar row, the version pill and search.
77
+ `h-14` plus its border, as `DuxtHeader` sets it. -->
78
+ <div class="flex h-14 shrink-0 items-center gap-4 border-b px-4 @5xl:px-8">
79
+ <div class="h-4 w-16 rounded bg-muted" />
80
+
81
+ <!-- The navbar row, then a clear gap, then the version pill, the search
82
+ box at its real `w-56` and the three icons. Measured off the page
83
+ this stands in for, not sketched. -->
84
+ <div class="ml-auto hidden items-center gap-6 @2xl:flex">
85
+ <div
86
+ v-for="width in [32, 62, 50, 42]"
87
+ :key="width"
88
+ class="h-3 rounded bg-muted/60"
89
+ :style="{ width: `${width}px` }"
90
+ />
91
+ </div>
92
+
93
+ <div class="ml-6 flex items-center gap-6">
94
+ <div class="h-5 w-12 rounded-full bg-muted/60" />
95
+ <div class="h-8 w-56 rounded-md border bg-muted/30 @max-2xl:w-24" />
96
+ <div class="hidden gap-6 @2xl:flex">
97
+ <div
98
+ v-for="icon in 3"
99
+ :key="icon"
100
+ class="size-4 rounded bg-muted/60"
101
+ />
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- The section row. Hidden below `lg`, exactly where the real one is. -->
107
+ <div
108
+ class="hidden h-11 shrink-0 items-center gap-1 border-b px-4 @5xl:flex @5xl:px-8"
109
+ >
110
+ <div
111
+ v-for="(width, index) in [58, 54, 40, 58, 72, 108, 24]"
112
+ :key="width"
113
+ class="flex items-center gap-2 rounded-md px-3 py-1.5"
114
+ :class="index === 0 ? 'bg-accent' : ''"
115
+ >
116
+ <div class="size-3.5 rounded bg-muted/60" />
117
+ <div class="h-3 rounded bg-muted/60" :style="{ width: `${width}px` }" />
118
+ </div>
119
+ </div>
120
+
121
+ <div
122
+ class="mx-auto flex min-h-0 w-full max-w-[90rem] flex-1 gap-8 px-4 @5xl:px-8"
123
+ >
124
+ <!-- The section's own pages, one line each — a docs sidebar lists a
125
+ branch, not a table of contents. -->
126
+ <div class="hidden w-56 shrink-0 flex-col gap-1 py-8 @5xl:flex">
127
+ <div
128
+ v-for="(width, index) in [64, 56, 76]"
129
+ :key="width"
130
+ class="flex items-center gap-2 rounded-md px-2 py-1.5"
131
+ :class="index === 0 ? 'bg-accent' : ''"
132
+ >
133
+ <div class="size-3.5 rounded bg-muted/60" />
134
+ <div
135
+ class="h-3 rounded bg-muted/60"
136
+ :style="{ width: `${width}px` }"
137
+ />
138
+ </div>
139
+ </div>
140
+
141
+ <!-- The article: breadcrumb, title with the copy control beside it, the
142
+ lead, the rule under the header, then prose and a code card. -->
143
+ <div class="flex min-w-0 max-w-3xl flex-1 flex-col py-8">
144
+ <div class="h-2.5 w-16 rounded bg-muted/60" />
145
+
146
+ <div class="mt-3 flex items-start justify-between gap-4">
147
+ <div class="h-9 w-56 rounded bg-muted" />
148
+ <div class="h-8 w-36 rounded-md border bg-muted/30" />
149
+ </div>
150
+
151
+ <div class="mt-4 h-3.5 w-4/5 rounded bg-muted/60" />
152
+
153
+ <div class="my-8 h-px w-full bg-border" />
154
+
155
+ <!-- AN OPERATION LIST, for a tag page built from an OpenAPI document:
156
+ one lead line and then a run of bordered rows, each a method pill,
157
+ a path and a summary pushed to the end. Nothing like the prose and
158
+ the code card below it, which is exactly why it is drawn
159
+ separately — the two pages look nothing alike. -->
160
+ <template v-if="variant === 'api'">
161
+ <div class="h-2.5 w-4/5 rounded bg-muted/60" />
162
+
163
+ <div class="mt-6 flex flex-col gap-2">
164
+ <div
165
+ v-for="(row, index) in [
166
+ { method: 34, path: 52, summary: 62 },
167
+ { method: 38, path: 52, summary: 68 },
168
+ { method: 34, path: 88, summary: 60 },
169
+ { method: 44, path: 88, summary: 66 },
170
+ { method: 46, path: 88, summary: 64 }
171
+ ]"
172
+ :key="index"
173
+ class="flex items-center gap-3 rounded-lg border px-3 py-2.5"
174
+ >
175
+ <div
176
+ class="h-5 shrink-0 rounded bg-muted"
177
+ :style="{ width: `${row.method}px` }"
178
+ />
179
+ <div
180
+ class="h-2.5 rounded bg-muted/60"
181
+ :style="{ width: `${row.path}px` }"
182
+ />
183
+ <div
184
+ class="ml-auto h-2.5 rounded bg-muted/40"
185
+ :style="{ width: `${row.summary}px` }"
186
+ />
187
+ </div>
188
+ </div>
189
+ </template>
190
+
191
+ <template v-else>
192
+ <div class="flex flex-col gap-2.5">
193
+ <div
194
+ v-for="line in ['100%', '88%']"
195
+ :key="line"
196
+ class="h-2.5 rounded bg-muted/60"
197
+ :style="{ width: line }"
198
+ />
199
+ </div>
200
+
201
+ <div class="my-6 overflow-hidden rounded-lg border">
202
+ <div class="flex h-11 items-center gap-2 border-b bg-muted/40 px-3">
203
+ <div class="size-3.5 rounded bg-muted/60" />
204
+ <div class="h-2.5 w-20 rounded bg-muted/60" />
205
+ </div>
206
+ <div class="flex flex-col gap-2 p-4">
207
+ <div
208
+ v-for="line in ['58%', '44%', '20%']"
209
+ :key="line"
210
+ class="h-2.5 rounded bg-muted/40"
211
+ :style="{ width: line }"
212
+ />
213
+ </div>
214
+ </div>
215
+
216
+ <div class="h-2.5 w-3/5 rounded bg-muted/60" />
217
+ <div class="mt-8 h-5 w-40 rounded bg-muted" />
218
+ </template>
219
+ </div>
220
+
221
+ <!-- The contents column: the page's own outline, then the aside's fixed
222
+ links, then the provenance under a rule. -->
223
+ <div class="hidden w-56 shrink-0 flex-col gap-6 py-8 @7xl:flex">
224
+ <div class="flex flex-col gap-2">
225
+ <div class="h-3 w-24 rounded bg-muted" />
226
+ <div class="h-2.5 w-20 rounded bg-muted/60" />
227
+ <div class="h-2.5 w-28 rounded bg-muted/60" />
228
+ </div>
229
+
230
+ <div class="flex flex-col gap-2">
231
+ <div class="h-3 w-20 rounded bg-muted" />
232
+ <div
233
+ v-for="width in [76, 68, 88, 72]"
234
+ :key="width"
235
+ class="flex items-center gap-2"
236
+ >
237
+ <div class="size-3 rounded bg-muted/60" />
238
+ <div
239
+ class="h-2.5 rounded bg-muted/60"
240
+ :style="{ width: `${width}px` }"
241
+ />
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Provenance under its rule: the edit link, when it last changed,
246
+ and who wrote it beside their avatar. -->
247
+ <div class="flex flex-col gap-3 border-t pt-4">
248
+ <div class="flex items-center gap-2">
249
+ <div class="size-3 rounded bg-muted/60" />
250
+ <div class="h-2.5 w-20 rounded bg-muted/60" />
251
+ </div>
252
+ <div class="h-2.5 w-32 rounded bg-muted/40" />
253
+ <div class="h-2.5 w-16 rounded bg-muted/40" />
254
+ <div class="flex items-center gap-2">
255
+ <div class="size-4 rounded-full bg-muted/60" />
256
+ <div class="h-2.5 w-16 rounded bg-muted/40" />
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </template>
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * How far down the page the reader is.
4
+ *
5
+ * A two-pixel rule under the header rather than a number: on a long reference
6
+ * page the useful question is "am I near the end", and a percentage answers it
7
+ * worse than a line does. `aria-hidden`, because it says nothing a screen
8
+ * reader cannot already ask the document.
9
+ *
10
+ * Measured against the document, not against a scroll container, so it is
11
+ * correct on the docs layout and on the landing page alike. Passive listener:
12
+ * this must never be the thing that makes scrolling stutter.
13
+ */
14
+ const progress = ref(0);
15
+
16
+ function measure() {
17
+ const doc = document.documentElement;
18
+ const scrollable = doc.scrollHeight - doc.clientHeight;
19
+
20
+ progress.value = scrollable > 0 ? (doc.scrollTop / scrollable) * 100 : 0;
21
+ }
22
+
23
+ onMounted(() => {
24
+ measure();
25
+ window.addEventListener('scroll', measure, { passive: true });
26
+ window.addEventListener('resize', measure, { passive: true });
27
+
28
+ onBeforeUnmount(() => {
29
+ window.removeEventListener('scroll', measure);
30
+ window.removeEventListener('resize', measure);
31
+ });
32
+ });
33
+ </script>
34
+
35
+ <template>
36
+ <div
37
+ aria-hidden="true"
38
+ class="pointer-events-none fixed inset-x-0 top-0 z-50 h-0.5"
39
+ >
40
+ <div
41
+ class="h-full bg-primary transition-[width] duration-75"
42
+ :style="{ width: `${progress}%` }"
43
+ />
44
+ </div>
45
+ </template>