@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,295 @@
1
+ <script setup lang="ts">
2
+ import { sourcesForRoute } from '../../sources-resolve';
3
+
4
+ /**
5
+ * One feature, given a whole band: its prose on one side and the thing itself
6
+ * on the other.
7
+ *
8
+ * The feature GRID says what a site can do in four words per card. This says it
9
+ * in a paragraph and a running example, which is the difference between a list
10
+ * of claims and a demonstration — and the reason a landing page needs both
11
+ * rather than a longer grid.
12
+ *
13
+ * The sides alternate, and the alternation is decided by the CALLER's index
14
+ * rather than counted here: a component that counted its own instances would
15
+ * flip when a band is added above it, and a `reverse` written per band would be
16
+ * a layout decision in every entry of a config that should only carry content.
17
+ */
18
+ const props = defineProps<{
19
+ showcase: DuxtResolved<DuxtShowcase>;
20
+ /** Position in the list. Even bands put the demo on the right. */
21
+ index: number;
22
+ }>();
23
+
24
+ const localeLink = useDuxtLink();
25
+
26
+ /** A band's own `reverse` wins; otherwise the position decides. */
27
+ const reversed = computed(
28
+ () => props.showcase.reverse ?? props.index % 2 === 1
29
+ );
30
+
31
+ const demo = computed(() => props.showcase.demo);
32
+
33
+ /**
34
+ * Whether the CLIENT lays the band out rather than this component.
35
+ *
36
+ * A split client is two rows with prose beside each, and only it knows where
37
+ * those rows are — so it takes the words through its slots and this component
38
+ * draws none of its own. Anything else keeps the ordinary two columns.
39
+ */
40
+ const splitClient = computed(
41
+ () =>
42
+ demo.value.type === 'operation' &&
43
+ (demo.value.layout ?? (props.showcase.full ? 'split' : 'panel')) === 'split'
44
+ );
45
+
46
+ /**
47
+ * The heading each band contributes to the document outline.
48
+ *
49
+ * An id per band rather than one shared one: `aria-labelledby` on the section
50
+ * needs a target that is THIS band's title, and a landing page with five bands
51
+ * pointing at the same node is five sections claiming one name.
52
+ */
53
+ const headingId = computed(() => `duxt-showcase-${props.index}`);
54
+
55
+ /** The files of a code demo. One draws a plain block, several draw a group. */
56
+ const files = computed(() =>
57
+ demo.value.type === 'code' ? demo.value.files : []
58
+ );
59
+
60
+ /**
61
+ * The operation behind an `operation` demo — the try-it client on its own.
62
+ *
63
+ * Read out of the page Content already built for that endpoint, so nothing here
64
+ * parses OpenAPI a second time and a band and its page can never disagree. The
65
+ * collection is resolved from the TARGET's path rather than from the route this
66
+ * component happens to stand on, which is the landing page and belongs to no
67
+ * source.
68
+ */
69
+ const duxt = useDuxtConfig();
70
+ const { locale, fallbackLocale } = useI18n();
71
+
72
+ const operationPath = computed(() =>
73
+ demo.value.type === 'operation' ? demo.value.to : undefined
74
+ );
75
+
76
+ const { data: operationPage } = await useAsyncData(
77
+ () => `duxt-showcase-operation-${operationPath.value ?? props.index}`,
78
+ () => {
79
+ const path = operationPath.value;
80
+ if (!path) return Promise.resolve(null);
81
+
82
+ const chain = sourcesForRoute(
83
+ path,
84
+ locale.value,
85
+ duxt.resolvedSources ?? [],
86
+ fallbackLocale.value as string | string[] | undefined
87
+ );
88
+
89
+ const collection = (chain[0]?.collection ?? 'docs') as DuxtCollectionArg;
90
+
91
+ return queryCollection(collection).path(path).first();
92
+ },
93
+ { watch: [operationPath, locale] }
94
+ );
95
+
96
+ /**
97
+ * Its props, in the shape `OpenApiOperation` hands the client.
98
+ *
99
+ * Undefined where the page carries no operation — a mistyped path, a section
100
+ * that is not an API reference — and the band then draws no demo rather than
101
+ * throwing on the landing page.
102
+ */
103
+ const operation = computed(() =>
104
+ openApiOperationProps(
105
+ (operationPage.value as { body?: unknown } | null)?.body
106
+ )
107
+ );
108
+ </script>
109
+
110
+ <template>
111
+ <section
112
+ class="mx-auto max-w-[90rem] px-4 py-12 sm:py-16 lg:px-8"
113
+ :aria-labelledby="headingId"
114
+ >
115
+ <!-- TWO COLUMNS, OR ONE. A demo that is a PICTURE of something — a framed
116
+ page, a screenshot, a block of config — reads fine beside its
117
+ paragraph. A demo that IS the thing needs the width the thing was built
118
+ for, and then the prose goes above it rather than beside it. -->
119
+ <div
120
+ :class="
121
+ splitClient
122
+ ? ''
123
+ : showcase.full
124
+ ? 'flex flex-col gap-8'
125
+ : 'grid items-center gap-8 lg:grid-cols-2 lg:gap-16'
126
+ "
127
+ >
128
+ <!-- THE PROSE. `lg:order-2` rather than a reordered DOM: the reading
129
+ order is title, text, list, link in every band, and a source order
130
+ that alternates would hand a screen reader a demo before the sentence
131
+ that says what it is.
132
+
133
+ A SPLIT CLIENT DRAWS ITS OWN. It is two rows, each with prose beside
134
+ it, and both come from this same component through its slots — see
135
+ the demo below. -->
136
+ <DuxtLandingProse
137
+ v-if="!splitClient"
138
+ :prose="showcase"
139
+ :heading-id="headingId"
140
+ :class="[
141
+ reversed && !showcase.full ? 'lg:order-2' : '',
142
+ showcase.full ? 'max-w-3xl' : ''
143
+ ]"
144
+ />
145
+
146
+ <!-- THE DEMO. `min-w-0` because a highlighted line of config is wider
147
+ than its column and a grid track sized to its content would push the
148
+ prose off the page rather than scroll the code. -->
149
+ <!-- `self-start`: a demo that changes height — a code group whose tabs
150
+ hold files of different lengths — grows DOWNWARDS from a fixed top
151
+ edge. Centred it grew in both directions, so the tab strip the reader
152
+ just clicked moved up under their cursor. The prose beside it stays
153
+ centred, which is what the row's `items-center` does. -->
154
+ <div
155
+ class="min-w-0 lg:self-start"
156
+ :class="reversed && !showcase.full ? 'lg:order-1' : ''"
157
+ >
158
+ <DuxtLiveWindow
159
+ v-if="demo.type === 'frame'"
160
+ :tabs="[
161
+ { label: showcase.title, to: demo.to, skeleton: demo.skeleton }
162
+ ]"
163
+ :height="demo.height"
164
+ :chrome="demo.chrome ?? true"
165
+ />
166
+
167
+ <!-- THE CLIENT, ON ITS OWN. Not the page it lives on: the panel sits
168
+ beside the endpoint only above 80rem and under the whole
169
+ description everywhere narrower, so a band-sized frame of that page
170
+ showed prose and hid the control the band is named for. This is the
171
+ control, with nothing around it — and less to download than the
172
+ second copy of the application a frame would have booted. -->
173
+ <!-- AND IT SCROLLS ITSELF, the way the operation page bounds the same
174
+ panel at `xl`. A full client is server, authentication, parameters,
175
+ a body editor, the send button and the samples under it — running
176
+ to twice the height of every other band, with its own paragraph
177
+ sitting in the middle of a column of nothing.
178
+
179
+ `max-h`, not `h`: a GET with two parameters is a short panel, and
180
+ padding it out to a POST's height is a box mostly empty. No border
181
+ and no padding here either — the client brings its own, and a
182
+ second one around it was a box drawn around a box.
183
+
184
+ NONE OF THAT IN A `full` BAND. There the client has the width it
185
+ was built for and splits itself into two cards, so there is no
186
+ column of nothing to rescue and nothing to cap. -->
187
+ <div
188
+ v-else-if="demo.type === 'operation'"
189
+ class="not-typeset"
190
+ :class="
191
+ showcase.full
192
+ ? ''
193
+ : 'max-h-[38rem] overflow-y-auto max-lg:max-h-[32rem]'
194
+ "
195
+ :style="
196
+ !showcase.full && demo.height
197
+ ? { maxHeight: demo.height }
198
+ : undefined
199
+ "
200
+ >
201
+ <!-- `v-bind` on a typed component: the props come out of a page's
202
+ body, so they are a record here and the component's own contract
203
+ is what checks them at the other end. -->
204
+ <DuxtOpenApiClient
205
+ v-if="operation"
206
+ :key="demo.to"
207
+ v-bind="operation as any"
208
+ :layout="demo.layout ?? (showcase.full ? 'split' : 'panel')"
209
+ :reverse="reversed"
210
+ >
211
+ <!-- The band's own words, handed to the row they belong beside.
212
+ The `h2` the section is labelled by goes with the form; the
213
+ samples row gets an `h3`, because it is a part of this section
214
+ rather than a section of its own. -->
215
+ <template #beside-form>
216
+ <DuxtLandingProse :prose="showcase" :heading-id="headingId" />
217
+ </template>
218
+
219
+ <!-- THE `v-if` IS ON THE CONTENT, NOT ON THE SLOT. A
220
+ `<template v-if>` around a named slot means the slot is not
221
+ PASSED at all when the condition is false — and the server and
222
+ the client disagree about what stands in its place, which is
223
+ the hydration mismatch this produced:
224
+
225
+ rendered on server: (start of fragment)
226
+ expected on client: Symbol(v-cmt)
227
+
228
+ Passed always, with the condition inside, both render the same
229
+ comment node when there is nothing to show. -->
230
+ <template #beside-samples>
231
+ <DuxtLandingProse
232
+ v-if="demo.samples"
233
+ :prose="demo.samples"
234
+ :level="3"
235
+ />
236
+ </template>
237
+ </DuxtOpenApiClient>
238
+ </div>
239
+
240
+ <!-- ONE FILE: the block, with its own header naming it.
241
+
242
+ SEVERAL: `CodeGroup` — the same component `::code-group` draws on a
243
+ docs page, handed blocks built from config instead of from
244
+ Markdown. It reads the label off each child's `filename` and
245
+ switches that child's own header off, so a reader meets one control
246
+ in both places and this page grows no tab bar of its own. -->
247
+ <CodeGroup
248
+ v-else-if="demo.type === 'code' && files.length > 1"
249
+ class="my-0!"
250
+ >
251
+ <DuxtCodeBlock
252
+ v-for="(file, position) in files"
253
+ :key="file.name ?? position"
254
+ :code="file.code"
255
+ :language="file.language"
256
+ :filename="file.name"
257
+ />
258
+ </CodeGroup>
259
+
260
+ <DuxtCodeBlock
261
+ v-else-if="demo.type === 'code' && files[0]"
262
+ :code="files[0].code"
263
+ :language="files[0].language"
264
+ :filename="files[0].name"
265
+ class="my-0!"
266
+ />
267
+
268
+ <!-- Two <img>s rather than one: a screenshot of a light interface on a
269
+ dark page is a torch, and `srcDark` is how a site hands over the
270
+ other file. Without one, `src` serves both. -->
271
+ <div
272
+ v-else-if="demo.type === 'image'"
273
+ class="overflow-hidden rounded-xl border bg-background shadow-sm"
274
+ >
275
+ <img
276
+ :src="demo.src"
277
+ :alt="demo.alt ?? ''"
278
+ loading="lazy"
279
+ decoding="async"
280
+ class="w-full"
281
+ :class="demo.srcDark ? 'dark:hidden' : ''"
282
+ />
283
+ <img
284
+ v-if="demo.srcDark"
285
+ :src="demo.srcDark"
286
+ alt=""
287
+ loading="lazy"
288
+ decoding="async"
289
+ class="hidden w-full dark:block"
290
+ />
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </section>
295
+ </template>
@@ -0,0 +1,343 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * A page of this site, framed as a browser window and operable — the reader
4
+ * scrolls it, opens its navigation and flips its theme without leaving the
5
+ * page it stands on.
6
+ *
7
+ * The landing page had one of these written into it. It now needs four: the big
8
+ * one under the hero, and one per showcase band that would rather show the real
9
+ * thing than a screenshot of it. So the window is a component, and everything
10
+ * that made the original one expensive to get right lives here once:
11
+ *
12
+ * - it is mounted ON APPROACH, never on the server. The frame is a second copy
13
+ * of the application, and an iframe in the initial HTML is a second full page
14
+ * load competing with the first paint;
15
+ * - a POSTER covers the box until that copy has booted, so the band has its
16
+ * height from the first byte rather than jumping to a page's worth;
17
+ * - the address bar is polled rather than listened for. The frame is
18
+ * same-origin, so its location is readable — but the site inside it is a Nuxt
19
+ * app, and a client-side route change fires no `load` on the element.
20
+ *
21
+ * TABS ARE PAGES, NOT PANES. One frame, one page loaded at a time: four frames
22
+ * behind a tab bar would be four copies of the application booting for a band
23
+ * the reader sees one quarter of.
24
+ */
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ /** The pages behind the tab bar. One entry draws no tab bar at all. */
28
+ tabs?: DuxtResolved<DuxtDemoTab>[];
29
+ /** Window height, any CSS length. */
30
+ height?: string;
31
+ /** The browser chrome. `false` leaves the page in a plain bordered box. */
32
+ chrome?: boolean;
33
+ /** Prefetch the first tab's document while the browser is idle. */
34
+ prefetch?: boolean;
35
+ /**
36
+ * A screenshot shown until the live page is up — or instead of it, where
37
+ * `live` is false. Without one the window draws the SHAPE of a page from
38
+ * the theme's own tokens, which needs no asset and is right in both modes
39
+ * by construction.
40
+ */
41
+ poster?: string;
42
+ /** The dark-mode file. Without one, `poster` serves both — a torch. */
43
+ posterDark?: string;
44
+ /** The poster's alt text. Only read where the poster is the content. */
45
+ posterAlt?: string;
46
+ /**
47
+ * Load the page at all. `false` shows only the poster, which is what a site
48
+ * picks when a second copy of the application is a cost it will not pay.
49
+ */
50
+ live?: boolean;
51
+ }>(),
52
+ { chrome: true, prefetch: false, live: true }
53
+ );
54
+
55
+ const localeLink = useDuxtLink();
56
+ const { t } = useI18n();
57
+
58
+ const active = ref(0);
59
+ const tabs = computed(() => props.tabs ?? []);
60
+ const current = computed(() => tabs.value[active.value] ?? tabs.value[0]);
61
+
62
+ /** Where the frame is pointed, in the reader's own language. */
63
+ const target = computed(() => localeLink(current.value?.to ?? '/'));
64
+
65
+ const root = useTemplateRef<HTMLElement>('root');
66
+ const frame = useTemplateRef<HTMLIFrameElement>('frame');
67
+
68
+ /** Mounted only once the band is on its way in — see the note above. */
69
+ const visible = ref(false);
70
+
71
+ /**
72
+ * True until the framed page has loaded once. Reset on a tab change, because
73
+ * that IS a first load — a different page, fetched from scratch.
74
+ *
75
+ * Only the first load of each: a route change the reader makes INSIDE the frame
76
+ * is the app's own navigation, which draws its own progress bar, and a second
77
+ * spinner over it would report the same thing twice.
78
+ */
79
+ const loading = ref(true);
80
+
81
+ const location = ref<{ href: string; path: string }>();
82
+
83
+ function readFrameLocation() {
84
+ try {
85
+ const view = frame.value?.contentWindow;
86
+ if (!view) return;
87
+
88
+ const { href, pathname, search, hash } = view.location;
89
+ // The bar shows the whole URL the way a browser's does — origin included,
90
+ // which is only knowable in the browser. The link keeps the path, because
91
+ // that is what the router routes on.
92
+ location.value = { href, path: `${pathname}${search}${hash}` };
93
+ } catch {
94
+ // A cross-origin document: nothing to read, and nothing to report.
95
+ }
96
+ }
97
+
98
+ function select(index: number) {
99
+ if (index === active.value) return;
100
+ active.value = index;
101
+ loading.value = true;
102
+ // The bar would otherwise keep printing the page we just navigated away
103
+ // from, for as long as the new one takes to answer.
104
+ location.value = undefined;
105
+ }
106
+
107
+ /**
108
+ * The document, fetched into the cache while the browser is idle.
109
+ *
110
+ * The frame still mounts on approach — a prefetch is not an execution. What it
111
+ * removes is the SLOW half: a document, its entry chunk, its payload. By the
112
+ * time the observer says "now" there is nothing left to download.
113
+ * `as="document"` is what makes the browser reuse it for an iframe rather than
114
+ * fetch it a second time.
115
+ *
116
+ * Off by default, and set only on the window a reader is certain to reach. Four
117
+ * prefetched applications is not a saving, it is the first paint again.
118
+ */
119
+ useHead(() => ({
120
+ link:
121
+ props.prefetch && props.live
122
+ ? [{ rel: 'prefetch', as: 'document', href: target.value }]
123
+ : []
124
+ }));
125
+
126
+ onMounted(() => {
127
+ if (!props.live) return;
128
+
129
+ const element = root.value;
130
+ if (!element) return;
131
+
132
+ // No IntersectionObserver (an old browser, a test environment): show it
133
+ // rather than leave an empty box on the page.
134
+ if (!('IntersectionObserver' in window)) {
135
+ visible.value = true;
136
+ } else {
137
+ const observer = new IntersectionObserver(
138
+ (entries) => {
139
+ if (!entries.some((entry) => entry.isIntersecting)) return;
140
+ visible.value = true;
141
+ observer.disconnect();
142
+ },
143
+ // Far enough ahead that the frame has begun booting by the time the band
144
+ // is on screen: it is a second copy of the application, so the gap
145
+ // between "in view" and "usable" is a page load rather than a paint.
146
+ { rootMargin: '600px' }
147
+ );
148
+
149
+ observer.observe(element);
150
+ onBeforeUnmount(() => observer.disconnect());
151
+ }
152
+
153
+ const timer = setInterval(readFrameLocation, 500);
154
+ onBeforeUnmount(() => clearInterval(timer));
155
+ });
156
+
157
+ /**
158
+ * The origin comes from the REQUEST, which both halves can read: `window` on
159
+ * the client, the incoming headers on the server. Every other source swaps the
160
+ * printed string once — at the frame's first load, or at hydration — and that
161
+ * swap is a flicker between two spellings of one page.
162
+ */
163
+ const requestUrl = useRequestURL({
164
+ xForwardedHost: true,
165
+ xForwardedProto: true
166
+ });
167
+
168
+ const href = computed(
169
+ () => location.value?.href ?? `${requestUrl.origin}${target.value}`
170
+ );
171
+
172
+ /** The frame's current page, falling back to the one it was pointed at. */
173
+ const path = computed(() => location.value?.path ?? target.value);
174
+
175
+ const title = computed(
176
+ () => current.value?.label ?? t('duxt.defaults.landing.preview')
177
+ );
178
+ </script>
179
+
180
+ <template>
181
+ <div
182
+ ref="root"
183
+ class="overflow-hidden rounded-xl border bg-background shadow-sm"
184
+ >
185
+ <!-- The window's own frame. The address bar is a <div>, not an input: it
186
+ says which page is inside, and a text field a reader can type into but
187
+ not submit is a control that lies. -->
188
+ <div
189
+ v-if="chrome"
190
+ class="flex items-center gap-3 border-b bg-muted/60 px-4 py-2.5"
191
+ >
192
+ <div aria-hidden="true" class="flex shrink-0 items-center gap-1.5">
193
+ <span class="size-2.5 rounded-full bg-muted-foreground/30" />
194
+ <span class="size-2.5 rounded-full bg-muted-foreground/30" />
195
+ <span class="size-2.5 rounded-full bg-muted-foreground/30" />
196
+ </div>
197
+
198
+ <!-- One fixed width, centred: a bar sized to its text grows and shrinks
199
+ on every navigation inside the frame, which reads as the frame
200
+ jittering rather than as a URL changing. The status icon sits where a
201
+ browser puts its padlock — leftmost, always there, so the URL never
202
+ shifts when it changes. -->
203
+ <div class="flex min-w-0 flex-1 justify-center">
204
+ <div
205
+ class="flex w-full max-w-md items-center gap-2 rounded-md bg-background/70 px-3 py-1 font-mono text-xs text-muted-foreground"
206
+ >
207
+ <Icon
208
+ :name="loading && live ? 'lucide:loader-circle' : 'lucide:globe'"
209
+ class="size-3 shrink-0"
210
+ :class="loading && live ? 'animate-spin' : 'opacity-60'"
211
+ role="status"
212
+ :aria-label="
213
+ loading && live ? $t('duxt.defaults.landing.preview') : undefined
214
+ "
215
+ />
216
+ <span class="truncate">{{ href }}</span>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- The way out of the frame. A page read inside a 30 rem window is a
221
+ demonstration; at some point the reader wants the real one. -->
222
+ <UiButton as-child size="sm" variant="ghost" class="shrink-0">
223
+ <NuxtLink :to="path">
224
+ <Icon name="lucide:external-link" class="size-3.5" />
225
+ <span class="sr-only sm:not-sr-only">
226
+ {{ $t('duxt.defaults.landing.previewOpen') }}
227
+ </span>
228
+ </NuxtLink>
229
+ </UiButton>
230
+ </div>
231
+
232
+ <!-- The tab bar, where there is more than one page to show. Real buttons
233
+ with `aria-selected` rather than a styled div: this is a tablist, the
234
+ arrow keys are expected to work, and they do because the buttons are
235
+ focusable in order. -->
236
+ <div
237
+ v-if="tabs.length > 1"
238
+ role="tablist"
239
+ :aria-label="$t('duxt.defaults.landing.demoTitle')"
240
+ class="flex gap-1 overflow-x-auto border-b bg-muted/30 px-2 py-1.5"
241
+ >
242
+ <button
243
+ v-for="(tab, index) in tabs"
244
+ :key="tab.to"
245
+ type="button"
246
+ role="tab"
247
+ :aria-selected="index === active"
248
+ class="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
249
+ :class="
250
+ index === active
251
+ ? 'bg-background text-foreground shadow-sm'
252
+ : 'text-muted-foreground hover:bg-background/60 hover:text-foreground'
253
+ "
254
+ @click="select(index)"
255
+ >
256
+ <Icon v-if="tab.icon" :name="tab.icon" class="size-4" />
257
+ {{ tab.label }}
258
+ </button>
259
+ </div>
260
+
261
+ <!-- THE BOX OWNS THE HEIGHT, not whatever happens to be inside it. The
262
+ frame is mounted late and the poster is absolutely positioned, so with
263
+ the height on the frame the band would have none at all until the frame
264
+ arrived, and then jump to a page's worth. -->
265
+ <!-- `@container`, because what is inside is sized by THIS BOX and not by
266
+ the browser window. The frame gets that for free — an iframe carries a
267
+ viewport of its own — but the poster is drawn in this document, and
268
+ with viewport breakpoints a half-width window on a wide screen drew the
269
+ full desktop page squeezed into it. -->
270
+ <div
271
+ class="relative @container h-[44rem] max-lg:h-[36rem] max-sm:h-[28rem]"
272
+ :style="height ? { height } : undefined"
273
+ >
274
+ <ClientOnly v-if="live">
275
+ <!-- `:key` on the target, so a tab change REPLACES the element rather
276
+ than repointing it: repointing an iframe pushes an entry onto the
277
+ browser's own history, and the reader's back button then walks the
278
+ tab bar instead of leaving the page.
279
+
280
+ NO `loading="lazy"`. The observer above has already decided this
281
+ frame should load, and the attribute puts the browser's heuristic
282
+ in front of that decision. -->
283
+ <iframe
284
+ v-if="visible"
285
+ :key="target"
286
+ ref="frame"
287
+ :src="target"
288
+ :title="title"
289
+ class="h-full w-full"
290
+ @load="
291
+ loading = false;
292
+ readFrameLocation();
293
+ "
294
+ />
295
+ </ClientOnly>
296
+
297
+ <!-- Over the frame until it has loaded once, then gone. Not `v-if`-ed
298
+ away on the same tick it stops being needed: a poster that vanishes
299
+ the instant the frame reports `load` swaps one picture for another
300
+ mid-paint, and a fade covers exactly that. `pointer-events-none` so a
301
+ poster on its way out cannot eat the click that lands underneath. -->
302
+ <Transition
303
+ enter-from-class="opacity-0"
304
+ leave-to-class="opacity-0"
305
+ enter-active-class="transition-opacity"
306
+ leave-active-class="transition-opacity duration-500"
307
+ >
308
+ <div
309
+ v-if="loading || !live"
310
+ class="absolute inset-0 overflow-hidden bg-background"
311
+ :class="live ? 'pointer-events-none' : ''"
312
+ :aria-hidden="live ? 'true' : undefined"
313
+ >
314
+ <!-- Two <img>s rather than one: a screenshot of a light theme on a
315
+ dark page is a torch, and `posterDark` is how a site hands over
316
+ the other file. Without one, `poster` serves both. -->
317
+ <template v-if="poster">
318
+ <img
319
+ :src="poster"
320
+ :alt="posterAlt ?? ''"
321
+ decoding="async"
322
+ class="h-full w-full object-cover object-top"
323
+ :class="posterDark ? 'dark:hidden' : ''"
324
+ />
325
+ <img
326
+ v-if="posterDark"
327
+ :src="posterDark"
328
+ alt=""
329
+ decoding="async"
330
+ class="hidden h-full w-full object-cover object-top dark:block"
331
+ />
332
+ </template>
333
+
334
+ <!-- No screenshot: the SHAPE of a documentation page, drawn from the
335
+ theme's own tokens. It needs no asset, it is right in both modes
336
+ by construction, and it says "a page is coming" where a blank box
337
+ said nothing. -->
338
+ <DuxtPreviewSkeleton v-else />
339
+ </div>
340
+ </Transition>
341
+ </div>
342
+ </div>
343
+ </template>