@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,187 @@
1
+ <script setup lang="ts">
2
+ // `::package-managers{command="add -D @kirchdev/duxt"}` — one command, every
3
+ // manager, in a single box: tabs and copy button sit in the code block's own
4
+ // header rather than floating above a separate card.
5
+ const props = defineProps<{
6
+ command: string;
7
+ managers?: DuxtPackageManager[];
8
+ }>();
9
+
10
+ const duxt = useDuxtConfig();
11
+
12
+ // Per block, then the site's configured order, then all four.
13
+ const offered = computed(
14
+ () => props.managers ?? duxt.packageManagers ?? duxtPackageManagers
15
+ );
16
+
17
+ /**
18
+ * A manager that cannot express this command gets NO TAB.
19
+ *
20
+ * Berry has no `outdated` and npm no `patch`, and the nearest relative in each
21
+ * case does a different job — so a tab there would be a control that lies.
22
+ * Dropping it reuses what the block could already do: it may list fewer
23
+ * managers than the reader's cookie covers, and falls back to its own first tab.
24
+ * `modules/validate.ts` reports which page asked, so the omission is not silent.
25
+ */
26
+ const managers = computed(() =>
27
+ offered.value.filter(
28
+ (manager) => packageCommand(manager, props.command) !== undefined
29
+ )
30
+ );
31
+
32
+ /**
33
+ * The mark each manager is drawn with.
34
+ *
35
+ * vscode-icons, because it carries the real colours in the file itself — the
36
+ * same collection every file name and every fence language on this site is
37
+ * drawn from. What stood here before was simple-icons, which is monochrome by
38
+ * design, plus eight hand-set hex values: two per manager, because one cannot
39
+ * serve both themes. That table is gone with it.
40
+ */
41
+ const managerIcons: Record<DuxtPackageManager, string> = {
42
+ npm: 'vscode-icons:file-type-npm',
43
+ pnpm: 'vscode-icons:file-type-pnpm',
44
+ yarn: 'vscode-icons:file-type-yarn',
45
+ bun: 'vscode-icons:file-type-bun'
46
+ };
47
+
48
+ /**
49
+ * pnpm, and pnpm alone, needs a second spelling.
50
+ *
51
+ * Its mark is orange squares AND WHITE ONES, so half the grid disappears on a
52
+ * white page. vscode-icons ships a `light` variant for exactly that — the white
53
+ * cells drawn `#4e4e4e` instead — and it ships one for no other manager here,
54
+ * which is the collection reaching the same conclusion. Two elements swapped by
55
+ * the theme class, the way the preview poster serves its two screenshots.
56
+ */
57
+ const managerIconsLight: Partial<Record<DuxtPackageManager, string>> = {
58
+ pnpm: 'vscode-icons:file-type-light-pnpm'
59
+ };
60
+
61
+ const commands = computed(() =>
62
+ Object.fromEntries(
63
+ managers.value.map((manager) => [
64
+ manager,
65
+ packageCommand(manager, props.command)!
66
+ ])
67
+ )
68
+ );
69
+
70
+ const { data: highlighted } = await useAsyncData(
71
+ `package-managers-${props.command}`,
72
+ async () => {
73
+ const entries = await Promise.all(
74
+ Object.entries(commands.value).map(
75
+ async ([manager, command]) =>
76
+ [manager, await highlightCode(command, 'bash')] as const
77
+ )
78
+ );
79
+ return Object.fromEntries(entries);
80
+ }
81
+ );
82
+
83
+ // Shared across every block on the page and remembered between pages, so a
84
+ // reader picks their manager once for the whole site. The value arrives with
85
+ // the request, so the server renders the right tab and nothing jumps.
86
+ const stored = usePackageManager();
87
+
88
+ // A block can list fewer managers than the reader's choice covers; fall back to
89
+ // its first rather than showing nothing.
90
+ const active = computed<DuxtPackageManager>({
91
+ get: () =>
92
+ managers.value.find((manager) => manager === stored.value) ??
93
+ managers.value[0]!,
94
+ set: (value) => {
95
+ stored.value = value;
96
+ }
97
+ });
98
+ const copied = ref(false);
99
+ const notify = useDuxtToast();
100
+ const { t } = useI18n();
101
+
102
+ async function copy() {
103
+ try {
104
+ await navigator.clipboard.writeText(commands.value[active.value]!);
105
+ copied.value = true;
106
+ notify.success(t('duxt.code.copiedToast'));
107
+ setTimeout(() => (copied.value = false), 2000);
108
+ } catch {
109
+ notify.error(
110
+ 'Could not copy',
111
+ 'The clipboard is unavailable in this context.'
112
+ );
113
+ // Clipboard is unavailable over plain HTTP; a failed copy stays silent.
114
+ }
115
+ }
116
+ </script>
117
+
118
+ <template>
119
+ <!-- Nothing to show where NO manager can express the command: `active` has
120
+ no first tab to fall back on, and a card with an empty header reads as a
121
+ broken component rather than as a command nobody can run. The build
122
+ validator names the page. -->
123
+ <div
124
+ v-if="managers.length"
125
+ class="duxt-code my-6 overflow-hidden rounded-lg border bg-card"
126
+ >
127
+ <div
128
+ class="flex min-h-11 items-center gap-1 border-b bg-muted/40 px-2 py-1.5"
129
+ >
130
+ <button
131
+ v-for="manager in managers"
132
+ :key="manager"
133
+ type="button"
134
+ class="flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors"
135
+ :class="
136
+ active === manager
137
+ ? 'bg-background text-foreground shadow-sm'
138
+ : 'text-muted-foreground hover:bg-accent hover:text-foreground'
139
+ "
140
+ @click="active = manager"
141
+ >
142
+ <Icon
143
+ :name="
144
+ managerIconsLight[manager] ??
145
+ managerIcons[manager] ??
146
+ 'lucide:terminal'
147
+ "
148
+ class="size-3.5"
149
+ :class="managerIconsLight[manager] ? 'dark:hidden' : ''"
150
+ />
151
+ <Icon
152
+ v-if="managerIconsLight[manager]"
153
+ :name="managerIcons[manager]"
154
+ class="hidden size-3.5 dark:block"
155
+ />
156
+ {{ manager }}
157
+ </button>
158
+
159
+ <UiButton
160
+ variant="ghost"
161
+ size="icon"
162
+ class="ml-auto size-7"
163
+ :aria-label="
164
+ copied ? $t('duxt.code.copied') : $t('duxt.code.copyCommand')
165
+ "
166
+ @click="copy"
167
+ >
168
+ <Icon
169
+ :name="copied ? 'lucide:check' : 'lucide:copy'"
170
+ class="size-3.5"
171
+ />
172
+ </UiButton>
173
+ </div>
174
+
175
+ <!-- eslint-disable-next-line vue/no-v-html -- Shiki output, built on the
176
+ server from this component's own prop, never from page content. -->
177
+ <div
178
+ v-if="highlighted?.[active]"
179
+ class="duxt-shell overflow-x-auto px-4 py-3 font-mono text-sm"
180
+ v-html="highlighted[active]"
181
+ />
182
+ <pre
183
+ v-else
184
+ class="overflow-x-auto px-4 py-3 font-mono text-sm"
185
+ ><code>{{ commands[active] }}</code></pre>
186
+ </div>
187
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import type { ContentNavigationItem } from '@nuxt/content';
3
+ // `::page-cards` on a section's index page: one card per child, taken from the
4
+ // navigation, so a new page appears here without the index being edited.
5
+ const props = defineProps<{ path?: string }>();
6
+
7
+ const path = useDuxtPath();
8
+ const localeLink = useDuxtLink();
9
+
10
+ const { data: navigation } = await useDuxtNavigation();
11
+
12
+ const base = computed(() => props.path ?? path.value);
13
+
14
+ const items = computed(() => {
15
+ const branch = navigation.value?.find((item) => item.path === base.value);
16
+ return branch?.children?.filter((child) => child.path !== base.value) ?? [];
17
+ });
18
+
19
+ /** `icon` comes from frontmatter, which Content types as unknown. */
20
+ const iconOf = (item: ContentNavigationItem) =>
21
+ typeof item.icon === 'string' ? item.icon : undefined;
22
+ </script>
23
+
24
+ <template>
25
+ <div v-if="items.length" class="not-typeset my-8 grid gap-4 sm:grid-cols-2">
26
+ <NuxtLink
27
+ v-for="item in items"
28
+ :key="item.path"
29
+ :to="localeLink(item.path)"
30
+ >
31
+ <UiCard
32
+ class="h-full transition-colors hover:border-foreground/20 hover:bg-accent/30"
33
+ >
34
+ <UiCardHeader>
35
+ <Icon
36
+ v-if="iconOf(item)"
37
+ :name="iconOf(item)!"
38
+ class="size-5 text-muted-foreground"
39
+ />
40
+ <UiCardTitle class="text-base">{{ item.title }}</UiCardTitle>
41
+ <UiCardDescription v-if="item.description">{{
42
+ item.description
43
+ }}</UiCardDescription>
44
+ </UiCardHeader>
45
+ </UiCard>
46
+ </NuxtLink>
47
+ </div>
48
+ </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import { partialsCollection } from '../../../sources-resolve';
3
+
4
+ /**
5
+ * `:partial{name="install"}` — a block written once and rendered in every
6
+ * source that needs it.
7
+ *
8
+ * Content ships no include directive, and across repositories that is a gap
9
+ * with no workaround: an install note that must read the same in three projects
10
+ * is copied into three projects and drifts. The partials collection is built
11
+ * from every source's `_partials/` folder, so a block defined in one repository
12
+ * is available to the pages of another.
13
+ *
14
+ * TRANSLATED like the pages around it, and through the same chain: a partials
15
+ * collection per language, tried best first, falling back to the untranslated
16
+ * original. Without that a German page including `:partial{name="install"}`
17
+ * renders an English block in the middle of German prose — which is worse than
18
+ * an untranslated page, because nothing on the page says it happened.
19
+ *
20
+ * A missing partial renders nothing rather than an error. A page whose install
21
+ * note failed to resolve is still a readable page; the build says so instead —
22
+ * see `modules/validate.ts`.
23
+ */
24
+ const props = defineProps<{ name?: string }>();
25
+
26
+ const { source, fallbacks } = useDuxtCollection();
27
+
28
+ /**
29
+ * The collections to try, best first — the page's own chain, mapped onto the
30
+ * partials collections. The same order, deliberately: a page and the blocks it
31
+ * includes falling back to different languages is how a page ends up half
32
+ * translated.
33
+ */
34
+ const chain = computed(() =>
35
+ [
36
+ ...new Set(
37
+ [source.value, ...fallbacks.value]
38
+ .filter(Boolean)
39
+ .map((entry) => partialsCollection(entry!))
40
+ )
41
+ ].map((name) => name as DuxtCollectionArg)
42
+ );
43
+
44
+ const { data: partial } = await useAsyncData(
45
+ () => `duxt-partial-${props.name}`,
46
+ async () => {
47
+ if (!props.name) return null;
48
+
49
+ for (const collection of chain.value) {
50
+ const found = await queryCollection(collection)
51
+ .where('stem', 'LIKE', `%_partials/${props.name}`)
52
+ .first();
53
+
54
+ if (found) return found;
55
+ }
56
+
57
+ return null;
58
+ },
59
+ { watch: [() => props.name, chain] }
60
+ );
61
+ </script>
62
+
63
+ <template>
64
+ <ContentRenderer v-if="partial" :value="partial" />
65
+ </template>
@@ -0,0 +1,173 @@
1
+ <script setup lang="ts">
2
+ import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
3
+
4
+ /**
5
+ * `::preview` — a component rendered as the page renders it, with the MDC that
6
+ * produced it one tab away.
7
+ *
8
+ * The documentation of a theme has one problem no prose solves: a page can
9
+ * describe `::callout{type="warning"}`, and the reader still has to build the
10
+ * site to find out what it looks like. This block renders the real component in
11
+ * the real theme — same tokens, same dark mode, same page — so the example is
12
+ * the thing itself rather than a picture of it.
13
+ *
14
+ * ::preview
15
+ * :::callout{type="tip"}
16
+ * Rendered by the page you are reading.
17
+ * :::
18
+ *
19
+ * #code
20
+ * ```mdc
21
+ * ::callout{type="tip"}
22
+ * Rendered by the page you are reading.
23
+ * ::
24
+ * ```
25
+ * ::
26
+ *
27
+ * The source is WRITTEN OUT in the `#code` slot rather than derived from the
28
+ * default slot, and that is a deliberate limit, not a shortcut: by the time this
29
+ * component exists its children are rendered VNodes, and the Markdown that
30
+ * produced them is gone — reconstructing it would mean re-serialising an AST
31
+ * into a syntax it never came from, and getting it subtly wrong on every block
32
+ * that nests. Two tabs and no `#code` is a valid block: the example renders and
33
+ * no source tab appears.
34
+ */
35
+ const slots = useSlots();
36
+
37
+ const hasCode = computed(() => Boolean(slots.code));
38
+ const active = ref('preview');
39
+
40
+ const { t } = useI18n();
41
+
42
+ /**
43
+ * The fence in `#code`, read off its VNode — the same trick `::code-group`
44
+ * uses on its children. What it yields is the filename or the language, which
45
+ * is what the tab should say and which icon it should carry: a reader picking
46
+ * between an example and its source is picking between a rendered thing and a
47
+ * `.vue`, a `.md` or an `.mdc` file, and the icon says which without a word.
48
+ */
49
+ const codeMeta = computed(() => {
50
+ const nodes = (slots.code?.() ?? []).flatMap((node) =>
51
+ Array.isArray(node.children) && typeof node.type === 'symbol'
52
+ ? (node.children as VNode[])
53
+ : [node]
54
+ );
55
+
56
+ const props = (nodes.find((node) => typeof node.type === 'object')?.props ??
57
+ {}) as { filename?: string; language?: string; code?: string };
58
+
59
+ return {
60
+ label: props.filename ?? props.language ?? t('duxt.code.source'),
61
+ icon: fileIcon(props.filename ?? props.language ?? 'md'),
62
+ code: props.code
63
+ };
64
+ });
65
+
66
+ /**
67
+ * The copy button lives up here rather than in the fence's own header, because
68
+ * the fence's header is hidden: it would say `mdc` directly under a tab that
69
+ * already says `mdc`, and one card does not need naming twice.
70
+ */
71
+ const notify = useDuxtToast();
72
+ const copied = ref(false);
73
+
74
+ async function copy() {
75
+ const code = codeMeta.value.code;
76
+ if (!code) return;
77
+
78
+ try {
79
+ await navigator.clipboard.writeText(code);
80
+ copied.value = true;
81
+ notify.success(t('duxt.code.copiedToast'));
82
+ setTimeout(() => (copied.value = false), 2000);
83
+ } catch {
84
+ notify.error(t('duxt.page.copyFailed'));
85
+ }
86
+ }
87
+
88
+ const tabs = computed(() => [
89
+ { value: 'preview', label: t('duxt.code.preview'), icon: 'lucide:eye' },
90
+ { value: 'code', label: codeMeta.value.label, icon: codeMeta.value.icon }
91
+ ]);
92
+ </script>
93
+
94
+ <template>
95
+ <!-- No tabs at all when there is no source: a tab strip with one tab is a
96
+ control that cannot be operated, and reka would still put it in the
97
+ accessibility tree as one. -->
98
+ <div v-if="!hasCode" class="my-6">
99
+ <slot />
100
+ </div>
101
+
102
+ <!-- One card, built like the package-manager block and the code block beside
103
+ it: a header of pills, then the panel. Three blocks on a page that all
104
+ switch between things should not switch in three different shapes.
105
+
106
+ One surface for the whole card, example and source alike: switching
107
+ tabs should change what is in the box, not what the box is made of. -->
108
+ <TabsRoot
109
+ v-else
110
+ v-model="active"
111
+ class="my-6 overflow-hidden rounded-lg border bg-card"
112
+ >
113
+ <!-- The header row is the flex container, and the strip is one child of
114
+ it: a `tablist` may hold tabs and nothing else, and a button inside one
115
+ is an `aria-required-children` failure — latent here only because it
116
+ renders on the source tab, which is not the tab a page loads on. -->
117
+ <div
118
+ class="flex min-h-11 items-center gap-1 border-b bg-muted/40 px-2 py-1.5"
119
+ >
120
+ <TabsList
121
+ class="flex items-center gap-1"
122
+ :aria-label="$t('duxt.code.previewTabs') as string"
123
+ >
124
+ <TabsTrigger
125
+ v-for="tab in tabs"
126
+ :key="tab.value"
127
+ :value="tab.value"
128
+ class="flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
129
+ >
130
+ <Icon :name="tab.icon" class="size-3.5" />
131
+ {{ tab.label }}
132
+ </TabsTrigger>
133
+ </TabsList>
134
+
135
+ <!-- Right of the tabs, where every other card on the site puts it. Only
136
+ on the source tab: a copy button beside a rendered example copies
137
+ something the reader cannot see. -->
138
+ <UiButton
139
+ v-if="active === 'code' && codeMeta.code"
140
+ variant="ghost"
141
+ size="icon"
142
+ class="ml-auto size-7"
143
+ :aria-label="copied ? $t('duxt.code.copied') : $t('duxt.code.copy')"
144
+ @click="copy"
145
+ >
146
+ <Icon
147
+ :name="copied ? 'lucide:check' : 'lucide:copy'"
148
+ class="size-3.5"
149
+ />
150
+ </UiButton>
151
+ </div>
152
+
153
+ <TabsContent value="preview" class="outline-none">
154
+ <!-- The example on the card's own surface, its outer margins taken off:
155
+ a callout carries `my-6`, and inside a padded panel that margin is
156
+ the padding a second time. -->
157
+ <div class="px-4 py-6 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
158
+ <slot />
159
+ </div>
160
+ </TabsContent>
161
+
162
+ <!-- The fence brought its own card into a card, so its frame, its rounding
163
+ and its own header come off — the tab above already names the language,
164
+ and the copy button moved up there with it. Its surface stays, and it is
165
+ the card's own — so the panel does not change colour between tabs. -->
166
+ <TabsContent
167
+ value="code"
168
+ class="outline-none [&_.duxt-code]:my-0 [&_.duxt-code]:rounded-none [&_.duxt-code]:border-0 [&_.duxt-code-header]:hidden [&_.duxt-code-copy]:hidden"
169
+ >
170
+ <slot name="code" />
171
+ </TabsContent>
172
+ </TabsRoot>
173
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * A link inside a page, resolved against the source it was written in.
4
+ *
5
+ * Markdown has no idea it is being served under a prefix. `/getting-started`
6
+ * written in this repository's `docs/` is correct on a site with one source and
7
+ * a 404 on a site with two, where the same page lives at
8
+ * `/duxt/getting-started` — and the author of the Markdown, who may be another
9
+ * repository entirely, cannot know which. Worse across versions: a link written
10
+ * in v0.7.0 must stay inside v0.7.0, or the reader silently changes version
11
+ * mid-sentence.
12
+ *
13
+ * So an absolute internal path is read as relative TO ITS OWN SOURCE, and the
14
+ * prefix is put back here. A path that already carries the prefix is left
15
+ * alone, so a link written the long way round still works.
16
+ */
17
+ const props = defineProps<{
18
+ href?: string;
19
+ target?: string;
20
+ }>();
21
+
22
+ const { source } = useDuxtCollection();
23
+ const localeLink = useDuxtLink();
24
+
25
+ const external = computed(() =>
26
+ Boolean(props.href && /^(?:[a-z]+:|\/\/)/i.test(props.href))
27
+ );
28
+
29
+ const to = computed(() => {
30
+ const href = props.href ?? '';
31
+ if (external.value || !href.startsWith('/')) return href;
32
+
33
+ const prefix = source.value?.prefix ?? '';
34
+ const [path, hash] = href.split('#');
35
+ const resolved =
36
+ prefix && !isInside(path!, prefix) ? `${prefix}${path}` : path!;
37
+
38
+ return `${localeLink(resolved)}${hash ? `#${hash}` : ''}`;
39
+ });
40
+ </script>
41
+
42
+ <template>
43
+ <a
44
+ v-if="external || !href?.startsWith('/')"
45
+ :href="href"
46
+ :target="target ?? (external ? '_blank' : undefined)"
47
+ :rel="external ? 'noopener' : undefined"
48
+ >
49
+ <slot />
50
+ </a>
51
+
52
+ <NuxtLink v-else :to="to" :target="target">
53
+ <slot />
54
+ </NuxtLink>
55
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * THE WHOLE HEADING IS THE LINK, not just the marker beside it.
4
+ *
5
+ * A '#' that appears at -left-5 on hover is a target most readers never find,
6
+ * and no touch device can hover onto it at all. The words are what someone aims
7
+ * at when they want to link to a section.
8
+ *
9
+ * The marker stays, now decorative: it says the heading is anchored. It is
10
+ * aria-hidden so the link is announced by the heading text itself, which also
11
+ * drops the generic "anchor" label a screen reader used to read in its place.
12
+ *
13
+ * `scroll-mt-28` clears the whole sticky stack — the 57px header AND the
14
+ * section row under it — because 20 (80px) only cleared the header and left an
15
+ * anchored heading hiding behind the sections.
16
+ *
17
+ * The explanation sits here rather than in the template because a Prose heading
18
+ * is instantiated once per heading: Vue keeps template comments in the rendered
19
+ * HTML, so a comment there ships to the browser as many times as the page has
20
+ * headings.
21
+ */
22
+ defineProps<{ id?: string }>();
23
+
24
+ const slots = useSlots();
25
+
26
+ /**
27
+ * A heading that IS a link cannot be wrapped in one — see `headingHasLink`.
28
+ * The marker beside it becomes the anchor instead, and stops being decorative:
29
+ * it is the only thing left to announce, so it takes a name.
30
+ */
31
+ const nested = computed(() => headingHasLink(slots.default?.()));
32
+ </script>
33
+
34
+ <template>
35
+ <h2 :id="id" class="group scroll-mt-28">
36
+ <a v-if="id && !nested" :href="`#${id}`" class="duxt-heading-link">
37
+ <span class="duxt-anchor" aria-hidden="true">#</span>
38
+ <slot />
39
+ </a>
40
+
41
+ <template v-else-if="id">
42
+ <a
43
+ :href="`#${id}`"
44
+ class="duxt-anchor duxt-anchor-only"
45
+ :aria-label="$t('duxt.page.anchor')"
46
+ >#</a
47
+ >
48
+ <slot />
49
+ </template>
50
+
51
+ <slot v-else />
52
+ </h2>
53
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * THE WHOLE HEADING IS THE LINK, not just the marker beside it.
4
+ *
5
+ * A '#' that appears at -left-5 on hover is a target most readers never find,
6
+ * and no touch device can hover onto it at all. The words are what someone aims
7
+ * at when they want to link to a section.
8
+ *
9
+ * The marker stays, now decorative: it says the heading is anchored. It is
10
+ * aria-hidden so the link is announced by the heading text itself, which also
11
+ * drops the generic "anchor" label a screen reader used to read in its place.
12
+ *
13
+ * `scroll-mt-28` clears the whole sticky stack — the 57px header AND the
14
+ * section row under it — because 20 (80px) only cleared the header and left an
15
+ * anchored heading hiding behind the sections.
16
+ *
17
+ * The explanation sits here rather than in the template because a Prose heading
18
+ * is instantiated once per heading: Vue keeps template comments in the rendered
19
+ * HTML, so a comment there ships to the browser as many times as the page has
20
+ * headings.
21
+ */
22
+ defineProps<{ id?: string }>();
23
+
24
+ const slots = useSlots();
25
+
26
+ /**
27
+ * A heading that IS a link cannot be wrapped in one — see `headingHasLink`.
28
+ * The marker beside it becomes the anchor instead, and stops being decorative:
29
+ * it is the only thing left to announce, so it takes a name.
30
+ */
31
+ const nested = computed(() => headingHasLink(slots.default?.()));
32
+ </script>
33
+
34
+ <template>
35
+ <h3 :id="id" class="group scroll-mt-28">
36
+ <a v-if="id && !nested" :href="`#${id}`" class="duxt-heading-link">
37
+ <span class="duxt-anchor" aria-hidden="true">#</span>
38
+ <slot />
39
+ </a>
40
+
41
+ <template v-else-if="id">
42
+ <a
43
+ :href="`#${id}`"
44
+ class="duxt-anchor duxt-anchor-only"
45
+ :aria-label="$t('duxt.page.anchor')"
46
+ >#</a
47
+ >
48
+ <slot />
49
+ </template>
50
+
51
+ <slot v-else />
52
+ </h3>
53
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * THE WHOLE HEADING IS THE LINK, not just the marker beside it.
4
+ *
5
+ * A '#' that appears at -left-5 on hover is a target most readers never find,
6
+ * and no touch device can hover onto it at all. The words are what someone aims
7
+ * at when they want to link to a section.
8
+ *
9
+ * The marker stays, now decorative: it says the heading is anchored. It is
10
+ * aria-hidden so the link is announced by the heading text itself, which also
11
+ * drops the generic "anchor" label a screen reader used to read in its place.
12
+ *
13
+ * `scroll-mt-28` clears the whole sticky stack — the 57px header AND the
14
+ * section row under it — because 20 (80px) only cleared the header and left an
15
+ * anchored heading hiding behind the sections.
16
+ *
17
+ * The explanation sits here rather than in the template because a Prose heading
18
+ * is instantiated once per heading: Vue keeps template comments in the rendered
19
+ * HTML, so a comment there ships to the browser as many times as the page has
20
+ * headings.
21
+ */
22
+ defineProps<{ id?: string }>();
23
+
24
+ const slots = useSlots();
25
+
26
+ /**
27
+ * A heading that IS a link cannot be wrapped in one — see `headingHasLink`.
28
+ * The marker beside it becomes the anchor instead, and stops being decorative:
29
+ * it is the only thing left to announce, so it takes a name.
30
+ */
31
+ const nested = computed(() => headingHasLink(slots.default?.()));
32
+ </script>
33
+
34
+ <template>
35
+ <h4 :id="id" class="group scroll-mt-28">
36
+ <a v-if="id && !nested" :href="`#${id}`" class="duxt-heading-link">
37
+ <span class="duxt-anchor" aria-hidden="true">#</span>
38
+ <slot />
39
+ </a>
40
+
41
+ <template v-else-if="id">
42
+ <a
43
+ :href="`#${id}`"
44
+ class="duxt-anchor duxt-anchor-only"
45
+ :aria-label="$t('duxt.page.anchor')"
46
+ >#</a
47
+ >
48
+ <slot />
49
+ </template>
50
+
51
+ <slot v-else />
52
+ </h4>
53
+ </template>