@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,611 @@
1
+ /**
2
+ * The frame every devtools panel is drawn in.
3
+ *
4
+ * Plain HTML with inline styles, not a Vue island: the panel renders inside an
5
+ * iframe in the devtools window, which shares nothing with the site — no
6
+ * stylesheet, no component registry, no theme. One page per panel, linked by a
7
+ * row of tabs, because a single template string stops being editable at about
8
+ * the second panel and there are ten.
9
+ *
10
+ * Nothing here is bundled into a production build: `modules/devtools.ts`
11
+ * returns early outside dev, so the route these files serve is never
12
+ * registered.
13
+ */
14
+
15
+ /** Paths the module knew at build time and a request handler cannot discover. */
16
+ export interface DevtoolsContext {
17
+ rootDir: string;
18
+ layerDir: string;
19
+ dataDir: string;
20
+ appConfigFile?: string;
21
+ locales: string[];
22
+ defaultLocale?: string;
23
+ /** vue-i18n's own fallback, so the path debugger falls back where pages do. */
24
+ fallbackLocale?: string | string[];
25
+ }
26
+
27
+ export const escape = (value: unknown) =>
28
+ String(value ?? '').replace(
29
+ /[&<>"]/g,
30
+ (character) =>
31
+ ({
32
+ '&': '&amp;',
33
+ '<': '&lt;',
34
+ '>': '&gt;',
35
+ '"': '&quot;'
36
+ })[character] ?? character
37
+ );
38
+
39
+ export const code = (value: unknown) => `<code>${escape(value)}</code>`;
40
+ export const dim = (value: unknown) =>
41
+ `<span class="dim">${escape(value)}</span>`;
42
+ // The kind is appended rather than interpolated: `tag('default')` is the
43
+ // commonest call, and a template with a hole in it emits `class="tag "` for it.
44
+ export const tag = (value: unknown, kind = '') =>
45
+ `<span class="tag${kind ? ` ${kind}` : ''}">${escape(value)}</span>`;
46
+
47
+ /**
48
+ * A link that opens the file in the editor.
49
+ *
50
+ * Nuxt Devtools serves this endpoint on the site's own origin, and the panel is
51
+ * an iframe on that origin, so a plain anchor is the whole integration. It only
52
+ * works while devtools is running, which is exactly when this page is reachable.
53
+ *
54
+ * The root directory is passed in rather than read from the context: this file
55
+ * holds the renderers, and a renderer that reaches for a build-time global
56
+ * cannot be handed fixture data — which is what the documentation previews are.
57
+ */
58
+ export function editor(
59
+ file: string | undefined,
60
+ label?: string,
61
+ rootDir = ''
62
+ ): string {
63
+ if (!file) return dim('—');
64
+
65
+ const absolute = file.startsWith('/') ? file : `${rootDir}/${file}`;
66
+
67
+ return `<a class="file" href="/__nuxt_devtools__/open-in-editor?file=${encodeURIComponent(absolute)}" target="_blank" title="${escape(absolute)}">${escape(label ?? file)}</a>`;
68
+ }
69
+
70
+ /**
71
+ * A table, or an empty state that says what would have filled it.
72
+ *
73
+ * "Nothing to show" is the wrong answer in a debugging view: an empty table is
74
+ * itself a finding, and the reader needs to know which. Every caller that can
75
+ * be empty for an interesting reason passes its own sentence.
76
+ */
77
+ export function table(
78
+ headers: string[],
79
+ rows: string[],
80
+ empty = 'Nothing to show.',
81
+ className = ''
82
+ ): string {
83
+ return `<div class="scroll"><table${className ? ` class="${className}"` : ''}>
84
+ <thead><tr>${headers.map((header) => `<th scope="col">${header}</th>`).join('')}</tr></thead>
85
+ <tbody>${rows.join('') || `<tr><td colspan="${headers.length}" class="empty">${empty}</td></tr>`}</tbody>
86
+ </table></div>`;
87
+ }
88
+
89
+ /**
90
+ * The one-line summary above a panel: counts and verdicts, before the detail.
91
+ *
92
+ * A panel that opens with two hundred rows makes the reader do the counting,
93
+ * and the count is usually the whole answer — five collections, seventy-six
94
+ * pages, no errors.
95
+ */
96
+ export function stats(items: (string | undefined)[]): string {
97
+ const kept = items.filter(Boolean);
98
+ if (!kept.length) return '';
99
+
100
+ return `<div class="stats">${kept.join('')}</div>`;
101
+ }
102
+
103
+ /** One figure in that row: the number large, what it counts underneath. */
104
+ export const stat = (value: unknown, label: string, kind = '') =>
105
+ `<div class="stat ${kind}"><b>${escape(value)}</b><span>${escape(label)}</span></div>`;
106
+
107
+ /**
108
+ * A text box that hides every row of a table not matching what is typed.
109
+ *
110
+ * Wrapped in a bar rather than left loose: between an open `summary` and its
111
+ * table, a bare input broke the one box a `details` is meant to be — the
112
+ * heading kept its squared-off bottom edge and the input floated in the gap.
113
+ * The bar is that middle band, and it carries the live match count, because a
114
+ * filter that hides rows without saying how many it kept makes the reader
115
+ * count them.
116
+ */
117
+ export const filter = (placeholder: string) =>
118
+ `<div class="filterbar"><input type="search" class="filter" data-filter placeholder="${escape(placeholder)}" aria-label="${escape(placeholder)}"><span class="count" data-filter-count role="status" aria-live="polite"></span></div>`;
119
+
120
+ /**
121
+ * A value as JSON: short ones inline, structured ones as a disclosure.
122
+ *
123
+ * The Config panel's whole subject is what a key BECAME, and half those keys
124
+ * hold a translated object or a list of links. Rendered as one truncated line
125
+ * of `JSON.stringify` they were unreadable in the only case that matters —
126
+ * every locale of a label sat past the cut, so the row proved a value exists
127
+ * and nothing else. Short values still print inline, because a disclosure
128
+ * around `["pnpm","npm"]` hides nothing and costs a click.
129
+ *
130
+ * Kept collapsed, and kept in the DOM: the filter box matches on row text, so
131
+ * a folded object is still searchable by any string inside it.
132
+ */
133
+ export function json(value: unknown, inline = 72): string {
134
+ if (!value || typeof value !== 'object') return code(String(value));
135
+
136
+ const flat = JSON.stringify(value);
137
+ const label = Array.isArray(value)
138
+ ? `${value.length} entries`
139
+ : `${Object.keys(value).length} keys`;
140
+
141
+ if (flat.length <= inline) return `${dim(label)} ${code(flat)}`;
142
+
143
+ return `<details class="json"><summary>${dim(label)} <code class="peek">${escape(truncate(flat, 96))}</code></summary><pre class="json">${highlight(JSON.stringify(value, null, 2))}</pre></details>`;
144
+ }
145
+
146
+ const truncate = (value: string, length: number) =>
147
+ value.length > length ? `${value.slice(0, length)}…` : value;
148
+
149
+ /** String, key, number, keyword — the four colours JSON has to say anything with. */
150
+ const TOKEN =
151
+ /("(?:\\.|[^"\\])*")(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/gi;
152
+
153
+ /**
154
+ * Escaped as it is tokenised, never after.
155
+ *
156
+ * Highlighting the escaped string instead would mean matching `&quot;` as a
157
+ * string delimiter — one entity in a value and the colours slide off by a
158
+ * character.
159
+ */
160
+ function highlight(source: string): string {
161
+ let out = '';
162
+ let last = 0;
163
+
164
+ for (const match of source.matchAll(TOKEN)) {
165
+ const text = match[0];
166
+ out += escape(source.slice(last, match.index));
167
+ last = match.index + text.length;
168
+
169
+ if (match[1] && match[2]) {
170
+ out += `<span class="j-key">${escape(match[1])}</span>${escape(match[2])}`;
171
+ continue;
172
+ }
173
+
174
+ const kind = match[1] ? 'str' : /^[a-z]/.test(text) ? 'word' : 'num';
175
+ out += `<span class="j-${kind}">${escape(text)}</span>`;
176
+ }
177
+
178
+ return out + escape(source.slice(last));
179
+ }
180
+
181
+ export const row = (cells: string[]) =>
182
+ `<tr>${cells.map((cell) => `<td>${cell}</td>`).join('')}</tr>`;
183
+
184
+ export interface Panel {
185
+ slug: string;
186
+ title: string;
187
+ hint: string;
188
+ }
189
+
190
+ /**
191
+ * The tabs, in the order a problem is usually chased: what the config became,
192
+ * then what a URL does with it, then what the content behind it looks like,
193
+ * then the machinery around it.
194
+ */
195
+ export const PANELS: Panel[] = [
196
+ {
197
+ slug: '',
198
+ title: 'Sources',
199
+ hint: 'What `duxt.sources` became: one collection per source and version, and the URL prefix each serves.'
200
+ },
201
+ {
202
+ slug: 'paths',
203
+ title: 'Paths',
204
+ hint: 'Resolve a URL the way the theme does, step by step — and say which step failed.'
205
+ },
206
+ {
207
+ slug: 'pages',
208
+ title: 'Pages',
209
+ hint: 'Every page in every collection, with the frontmatter and git metadata attached to it.'
210
+ },
211
+ {
212
+ slug: 'versions',
213
+ title: 'Versions',
214
+ hint: 'Which versions carry which page. The view the fallback is debugged from.'
215
+ },
216
+ {
217
+ slug: 'checks',
218
+ title: 'Checks',
219
+ hint: 'The build validator, run against what the site is serving now.'
220
+ },
221
+ {
222
+ slug: 'search',
223
+ title: 'Search',
224
+ hint: 'Ask the search index what it actually holds.'
225
+ },
226
+ {
227
+ slug: 'config',
228
+ title: 'Config',
229
+ hint: 'The merged `duxt` config, and which side of the merge each value came from.'
230
+ },
231
+ {
232
+ slug: 'i18n',
233
+ title: 'i18n',
234
+ hint: 'Message keys per locale, against the base language.'
235
+ },
236
+ {
237
+ slug: 'cache',
238
+ title: 'Cache',
239
+ hint: "Content's download cache under `.data/content`, and a way to drop an entry."
240
+ },
241
+ {
242
+ slug: 'redirects',
243
+ title: 'Redirects',
244
+ hint: 'Every redirect the server ships, with the rules `redirectFrom` generated marked as the layer\u2019s own.'
245
+ }
246
+ ];
247
+
248
+ const href = (slug: string) => `/_duxt/devtools${slug ? `/${slug}` : ''}`;
249
+
250
+ /**
251
+ * How a preview differs from the panel: where its tabs point, and that nothing
252
+ * else in it navigates.
253
+ *
254
+ * A preview is the same document rendered from fixtures and saved as a file, so
255
+ * its tab row has to link to sibling files rather than to a route that exists
256
+ * only in a dev server. Everything else in the page — an editor link, the drop
257
+ * button, the search form — would leave the page or POST somewhere, and a
258
+ * reader clicking one in the documentation is a reader chasing a broken link.
259
+ * They stay visible, because they are part of what the panel IS; they are made
260
+ * inert instead of removed.
261
+ */
262
+
263
+ const INERT = `
264
+ // EVERY link and button, with no exception for the tab row — a preview has
265
+ // no tab row. Embedded in a documentation page, a reader who clicked one
266
+ // would land on another panel inside the frame while the prose around it went
267
+ // on explaining this one.
268
+ document.addEventListener('click', (event) => {
269
+ if (event.target.closest('a, button')) event.preventDefault();
270
+ });
271
+ document.addEventListener('submit', (event) => event.preventDefault());
272
+
273
+ // The page that framed this one owns the theme and the height: it knows
274
+ // which mode the reader picked, and only this document knows how tall it
275
+ // turned out. Two messages, one each way.
276
+ const theme = new URLSearchParams(location.search).get('theme');
277
+ if (theme) document.documentElement.dataset.theme = theme;
278
+
279
+ // Only from the frame that embedded this one, and only from its own origin.
280
+ // Both halves are needed: the origin alone would still accept a message from
281
+ // a sibling frame on the same site, and \`event.source\` alone would accept one
282
+ // from a parent on another origin. This document is served from \`public/\`, so
283
+ // the page framing it is always same-origin — a message from anywhere else is
284
+ // not a theme change, it is somebody else talking.
285
+ addEventListener('message', (event) => {
286
+ if (event.origin !== location.origin || event.source !== parent) return;
287
+
288
+ if (event.data && typeof event.data.duxtTheme === 'string') {
289
+ document.documentElement.dataset.theme = event.data.duxtTheme;
290
+ }
291
+ });
292
+
293
+ // Addressed rather than broadcast, for the same reason. \`'*'\` hands the
294
+ // height to whatever ends up framing this page, which on a consumer's site is
295
+ // not necessarily the page that asked for it.
296
+ const measure = () => parent.postMessage(
297
+ { duxtHeight: document.documentElement.scrollHeight },
298
+ location.origin
299
+ );
300
+
301
+ addEventListener('load', measure);
302
+ addEventListener('resize', measure);
303
+ document.addEventListener('toggle', measure, true);
304
+ document.addEventListener('input', () => setTimeout(measure, 0));
305
+ `;
306
+
307
+ /**
308
+ * The palette, borrowed rather than invented.
309
+ *
310
+ * The panel is an iframe inside the devtools window, so it cannot read that
311
+ * window's theme — `prefers-color-scheme` is the only signal it gets, and a
312
+ * page that ignores it is a white rectangle in a dark tool. The accent is
313
+ * Nuxt's own green, which is what makes the tab read as part of devtools
314
+ * instead of as a document someone embedded in it.
315
+ */
316
+ /**
317
+ * The dark half of the palette, written once and applied twice.
318
+ *
319
+ * Inside devtools `prefers-color-scheme` is the only signal a panel gets. A
320
+ * preview embedded in the documentation is a different case: it sits on a page
321
+ * whose theme is a class the reader toggles, so the parent tells it which one
322
+ * to wear, and an explicit choice has to beat the media query in BOTH
323
+ * directions — hence the `:not([data-theme='light'])` guard on the query.
324
+ */
325
+ const DARK = `
326
+ --bg: #0f1115;
327
+ --fg: #e8e8ea;
328
+ --muted: #9099a8;
329
+ --faint: #5b6472;
330
+ --line: #23262d;
331
+ --raised: #171a20;
332
+ --accent: #00dc82;
333
+ --accent-soft: #10291f;
334
+ --warn: #f59e0b;
335
+ --error: #f87171;
336
+ --ok: #4ade80;
337
+ `;
338
+
339
+ const STYLE = `
340
+ :root {
341
+ color-scheme: light dark;
342
+ --bg: #fff;
343
+ --fg: #12141a;
344
+ --muted: #6b7280;
345
+ --faint: #9ca3af;
346
+ --line: #e8e8ea;
347
+ --raised: #f6f6f7;
348
+ --accent: #00a86b;
349
+ --accent-soft: #e6f7f0;
350
+ --warn: #b45309;
351
+ --error: #b91c1c;
352
+ --ok: #15803d;
353
+ --radius: 7px;
354
+ }
355
+ @media (prefers-color-scheme: dark) {
356
+ :root:not([data-theme='light']) { ${DARK} }
357
+ }
358
+ :root[data-theme='dark'] { ${DARK} }
359
+
360
+ * { box-sizing: border-box; }
361
+ body {
362
+ margin: 0;
363
+ padding: 0 0 40px;
364
+ font: 13px/1.55 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
365
+ color: var(--fg);
366
+ background: var(--bg);
367
+ -webkit-font-smoothing: antialiased;
368
+ }
369
+ a { color: inherit; }
370
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
371
+
372
+ header.bar {
373
+ position: sticky; top: 0; z-index: 2;
374
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
375
+ backdrop-filter: blur(8px);
376
+ border-bottom: 1px solid var(--line);
377
+ padding: 10px 16px 0;
378
+ }
379
+ nav { display: flex; gap: 1px; flex-wrap: wrap; }
380
+ nav a {
381
+ padding: 5px 10px; border-radius: var(--radius) var(--radius) 0 0;
382
+ text-decoration: none; color: var(--muted); font-weight: 500;
383
+ border-bottom: 2px solid transparent; line-height: 1.4;
384
+ }
385
+ nav a:hover { color: var(--fg); background: var(--raised); }
386
+ nav a[aria-current] { color: var(--fg); border-bottom-color: var(--accent); font-weight: 600; }
387
+
388
+ main { padding: 18px 16px 0; max-width: 1400px; }
389
+ h1 { margin: 0 0 3px; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
390
+ h2 {
391
+ margin: 26px 0 8px; font-size: 12px; font-weight: 600;
392
+ text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
393
+ }
394
+ p.hint { margin: 0 0 16px; color: var(--muted); max-width: 78ch; }
395
+
396
+ .stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
397
+ .stat {
398
+ display: flex; flex-direction: column; gap: 1px;
399
+ padding: 7px 12px; min-width: 78px;
400
+ border: 1px solid var(--line); border-radius: var(--radius); background: var(--raised);
401
+ }
402
+ .stat b { font-size: 16px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
403
+ .stat span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
404
+ .stat.ok b { color: var(--ok); }
405
+ .stat.warn b { color: var(--warn); }
406
+ .stat.error b { color: var(--error); }
407
+
408
+ .scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; }
409
+ table { width: 100%; border-collapse: collapse; }
410
+ th, td { padding: 7px 11px; text-align: left; vertical-align: top; }
411
+ /* Offset zero, not the height of the tab bar. .scroll sets overflow-x, which
412
+ computes overflow-y to auto as well, so the box — not the page — is what a
413
+ sticky heading offsets against. At 41px the heading sat 41px down from the
414
+ box's top edge, leaving a blank band above it AND covering the first row:
415
+ the Redirects panel counted eight rules and showed seven. */
416
+ thead th {
417
+ position: sticky; top: 0; z-index: 1;
418
+ background: var(--raised);
419
+ font-weight: 600; color: var(--muted);
420
+ font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
421
+ white-space: nowrap; border-bottom: 1px solid var(--line);
422
+ }
423
+ tbody tr { border-top: 1px solid var(--line); }
424
+ tbody tr:first-child { border-top: 0; }
425
+ tbody tr:hover { background: var(--raised); }
426
+ td.empty { color: var(--muted); padding: 16px 11px; text-align: center; }
427
+
428
+ code {
429
+ font: 11.5px/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
430
+ background: var(--raised); border: 1px solid var(--line);
431
+ padding: 0 4px; border-radius: 4px; white-space: nowrap;
432
+ }
433
+ a.file { text-decoration: none; border-bottom: 1px dashed var(--faint); }
434
+ a.file:hover { border-bottom-style: solid; color: var(--accent); }
435
+ .dim { color: var(--faint); }
436
+ .trunc {
437
+ display: inline-block; max-width: 34ch; overflow: hidden;
438
+ text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
439
+ }
440
+ td.wide { min-width: 32ch; }
441
+
442
+ .tag {
443
+ display: inline-block; border-radius: 999px; padding: 1px 7px;
444
+ font-size: 10px; font-weight: 600; white-space: nowrap;
445
+ background: var(--accent-soft); color: var(--accent);
446
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
447
+ }
448
+ .tag.warn { background: transparent; color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
449
+ .tag.error { background: transparent; color: var(--error); border-color: color-mix(in srgb, var(--error) 40%, transparent); }
450
+ .tag.ok { background: transparent; color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
451
+ .tag.muted { background: var(--raised); color: var(--muted); border-color: var(--line); }
452
+
453
+ form { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; align-items: center; }
454
+ input[type=text], input[type=search] {
455
+ flex: 1; min-width: 240px; padding: 6px 11px;
456
+ border: 1px solid var(--line); border-radius: var(--radius);
457
+ font: inherit; background: var(--bg); color: inherit;
458
+ }
459
+ .filterbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
460
+ .filterbar .count {
461
+ color: var(--muted); font-size: 11.5px;
462
+ font-variant-numeric: tabular-nums; white-space: nowrap;
463
+ }
464
+ input.filter {
465
+ flex: 0 1 340px; min-width: 0; margin: 0; padding-left: 31px;
466
+ background: var(--bg) no-repeat 10px 50% / 14px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239099a8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.6-3.6'/%3E%3C/svg%3E");
467
+ }
468
+ input.filter:focus { border-color: var(--accent); }
469
+ input.filter::-webkit-search-cancel-button { cursor: pointer; }
470
+ input::placeholder { color: var(--faint); }
471
+ button {
472
+ padding: 6px 14px; border: 1px solid var(--line); border-radius: var(--radius);
473
+ background: var(--raised); font: inherit; font-weight: 500; cursor: pointer; color: inherit;
474
+ }
475
+ button:hover { border-color: var(--accent); color: var(--accent); }
476
+
477
+ ol.steps { margin: 0 0 18px; padding: 0; list-style: none; counter-reset: step; }
478
+ ol.steps li {
479
+ position: relative; counter-increment: step;
480
+ padding: 8px 0 8px 30px; border-left: 2px solid var(--line); margin-left: 9px;
481
+ }
482
+ ol.steps li::before {
483
+ content: counter(step);
484
+ position: absolute; left: -10px; top: 8px;
485
+ width: 18px; height: 18px; border-radius: 999px;
486
+ background: var(--raised); border: 1px solid var(--line);
487
+ font-size: 10px; font-weight: 600; color: var(--muted);
488
+ display: flex; align-items: center; justify-content: center;
489
+ }
490
+ ol.steps li:last-child { border-left-color: transparent; }
491
+
492
+ .note {
493
+ padding: 10px 13px; border-radius: var(--radius);
494
+ background: var(--raised); border: 1px solid var(--line);
495
+ color: var(--muted); margin-bottom: 16px; max-width: 90ch;
496
+ }
497
+ .note code { background: var(--bg); }
498
+
499
+ details { margin-bottom: 10px; }
500
+ details > summary {
501
+ cursor: pointer; padding: 7px 11px; border-radius: var(--radius);
502
+ background: var(--raised); border: 1px solid var(--line);
503
+ font-weight: 600; list-style-position: inside;
504
+ }
505
+ details > summary:hover { border-color: var(--accent); }
506
+ details[open] > summary { border-radius: var(--radius) var(--radius) 0 0; margin-bottom: -1px; }
507
+ details[open] > summary + .scroll { border-radius: 0 0 var(--radius) var(--radius); }
508
+ details[open] > summary + .filterbar {
509
+ margin: 0; padding: 8px 11px;
510
+ background: var(--raised); border: 1px solid var(--line); border-top: 0;
511
+ }
512
+ details[open] > .filterbar + .scroll {
513
+ border-radius: 0 0 var(--radius) var(--radius); margin-top: -1px;
514
+ }
515
+
516
+ /* A disclosure inside a table cell is not the panel-sized one above: no bar,
517
+ no border, no weight — just a triangle in front of the preview. */
518
+ details.json { margin: 0; }
519
+ details.json > summary {
520
+ padding: 0; background: none; border: 0; border-radius: 0; font-weight: inherit;
521
+ list-style-position: outside; margin-left: 12px;
522
+ }
523
+ details.json > summary::marker { color: var(--faint); }
524
+ details.json > summary:hover { border: 0; }
525
+ details.json[open] > summary { margin-bottom: 6px; border-radius: 0; }
526
+ details.json[open] > summary .peek { display: none; }
527
+ pre.json {
528
+ margin: 0 0 2px; padding: 9px 11px; max-height: 320px; overflow: auto;
529
+ background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
530
+ font: 11.5px/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
531
+ white-space: pre; tab-size: 2;
532
+ }
533
+ .j-key { color: var(--accent); }
534
+ .j-num { color: var(--warn); }
535
+ .j-word { color: var(--muted); font-style: italic; }
536
+
537
+ .matrix th:first-child, .matrix td:first-child {
538
+ position: sticky; left: 0; background: var(--bg);
539
+ border-right: 1px solid var(--line);
540
+ }
541
+ .matrix thead th:first-child { background: var(--raised); }
542
+ .matrix tbody tr:hover td:first-child { background: var(--raised); }
543
+ .matrix td.mark { text-align: center; }
544
+ .matrix td.yes { color: var(--ok); font-weight: 700; }
545
+ .matrix td.no { color: var(--faint); }
546
+ `;
547
+
548
+ /**
549
+ * The filter box, in six lines rather than a framework.
550
+ *
551
+ * A collection of two hundred pages is a table nobody scrolls. Hiding rows
552
+ * client-side keeps every panel a plain server-rendered document — the reader
553
+ * types, the rows go, and a reload still shows everything.
554
+ */
555
+ const SCRIPT = `
556
+ document.addEventListener('input', (event) => {
557
+ const input = event.target;
558
+ if (!input.matches('[data-filter]')) return;
559
+
560
+ const term = input.value.trim().toLowerCase();
561
+ const scope = input.closest('details') ?? document;
562
+ const rows = scope.querySelectorAll('tbody tr');
563
+ let shown = 0;
564
+
565
+ for (const row of rows) {
566
+ row.hidden = Boolean(term) && !row.textContent.toLowerCase().includes(term);
567
+ if (!row.hidden) shown += 1;
568
+ }
569
+
570
+ const count = input.parentElement.querySelector('[data-filter-count]');
571
+ if (count) {
572
+ count.textContent = term
573
+ ? shown ? shown + ' of ' + rows.length : 'no matches'
574
+ : '';
575
+ }
576
+ });
577
+ `;
578
+
579
+ /** The full document: tab row, panel heading, body. */
580
+ export function page(active: string, body: string, preview = false): string {
581
+ const panel = PANELS.find((entry) => entry.slug === active) ?? PANELS[0]!;
582
+
583
+ /**
584
+ * NO TAB ROW IN A PREVIEW.
585
+ *
586
+ * The tab row is how the devtools panel is navigated, and it is exactly wrong
587
+ * inside a documentation page: the frame sits under prose describing ONE
588
+ * panel, and a reader who switched tabs in it would be reading about Sources
589
+ * while looking at Paths. The row is also the panel's own chrome — the
590
+ * documentation already carries a heading, a description and a sidebar entry
591
+ * saying which panel this is.
592
+ */
593
+ const tabs = preview
594
+ ? ''
595
+ : `<header class="bar"><nav aria-label="Panels">${PANELS.map(
596
+ (entry) =>
597
+ `<a href="${href(entry.slug)}"${entry.slug === active ? ' aria-current="page"' : ''}>${escape(entry.title)}</a>`
598
+ ).join('')}</nav></header>`;
599
+
600
+ return `<!doctype html>
601
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
602
+ <title>duxt — ${escape(panel.title)}</title><style>${STYLE}</style></head><body>
603
+ ${tabs}
604
+ <main>
605
+ <h1>${escape(panel.title)}</h1>
606
+ <p class="hint">${panel.hint.replace(/`([^`]+)`/g, (_, inner: string) => code(inner))}</p>
607
+ ${body}
608
+ </main>
609
+ <script>${SCRIPT}${preview ? INERT : ''}</script>
610
+ </body></html>`;
611
+ }