@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,16 @@
1
+ import type { InjectionKey } from 'vue';
2
+
3
+ /**
4
+ * A layout's answer to "is there room on the right?".
5
+ *
6
+ * `pages/[...slug].vue` draws the contents column, the aside's links and the
7
+ * page's provenance in one sticky column, and for almost every page that is
8
+ * right. A layout that fills the same side itself — `reference`, whose
9
+ * operations put the request client there — provides `false` and the page
10
+ * leaves the space alone.
11
+ *
12
+ * Provided rather than matched on a layout name: the layout is the only thing
13
+ * that knows what it draws, and a name in a list in the page is a rule that
14
+ * goes stale the first time a consumer ships a layout of their own.
15
+ */
16
+ export const DUXT_ASIDE: InjectionKey<boolean> = Symbol('duxt-aside');
@@ -0,0 +1,182 @@
1
+ /**
2
+ * What a release history needs beyond the pages the parser writes: the anchor a
3
+ * group is linked at, the colour it is drawn in, and the day it was cut.
4
+ *
5
+ * Here rather than in the components because TWO PLACES have to agree about an
6
+ * id — the heading `ChangelogGroup` draws, and the contents column built from
7
+ * the page's own AST (`generated-toc.ts`), which never sees that component
8
+ * render. A slug computed twice is a contents column whose links go nowhere.
9
+ *
10
+ * Named exports rather than auto-imports read from a test: the same reason
11
+ * `generated-sections.ts` gives, and the same tests.
12
+ */
13
+
14
+ /**
15
+ * The anchor a group heading gets, or nothing when the name has no ASCII in it.
16
+ *
17
+ * Undefined rather than a fallback: a heading a component draws is not in the
18
+ * outline Content slugifies, so there is nothing upstream to fall back TO, and
19
+ * pointing every group of every release at `#` is worse than not linking one.
20
+ */
21
+ export function changelogAnchor(name: string): string | undefined {
22
+ return (
23
+ name
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, '-')
26
+ .replace(/^-+|-+$/g, '') || undefined
27
+ );
28
+ }
29
+
30
+ /**
31
+ * The mark a release tool puts in FRONT of a heading, dropped.
32
+ *
33
+ * `\p{S}` is symbols — the `⚠` release-please writes on its breaking block, an
34
+ * emoji a hand-kept changelog opens a section with. LEADING ONLY: a name that
35
+ * uses a symbol anywhere else keeps it, so `C++ Support` survives, and a
36
+ * heading that is nothing BUT a mark comes back untouched rather than emptied.
37
+ *
38
+ * The stored name stays whole either way. This is what a label SHOWS — never
39
+ * what the filter matches, what the anchor is built from, or what travels as a
40
+ * prop — which is also why the mark is drawn away rather than parsed away: the
41
+ * file said it, and a site that wants it back overrides the component instead
42
+ * of the changelog.
43
+ */
44
+ export function changelogLabel(name: string): string {
45
+ return name.replace(/^[\p{S}\s]+/u, '').trim() || name;
46
+ }
47
+
48
+ /** The classes one group is drawn with: a dot, a run of text, a filter chip. */
49
+ export interface DuxtChangelogTone {
50
+ /** A filled dot — the group's identity wherever the name is already there. */
51
+ dot: string;
52
+ /**
53
+ * The NAME set in the tone, for the label a release page heads its group
54
+ * with.
55
+ *
56
+ * A separate value rather than the dot's colour reused, because a filled
57
+ * shape and a run of text do not clear contrast at the same lightness: the
58
+ * `500` a 6-pixel dot is drawn in is well under the floor for words.
59
+ *
60
+ * `700` / `400` is the most saturated pair the ramp offers that still clears
61
+ * AA on both grounds — measured, not picked: on white, `600` fails for four
62
+ * of the six hues (amber 3.2, emerald 3.7, cyan 3.7, sky 4.1), so the light
63
+ * side stops at `700`. The dark side has the room, which is why it takes
64
+ * `400` rather than the `300` the chip uses: on the page background it
65
+ * measures 7.0–11.5 and on a card 6.3–10.4, and it is the half a reader of a
66
+ * dark site actually sees.
67
+ */
68
+ text: string;
69
+ /** A tinted surface with a matching ring, for the filter chip. */
70
+ chip: string;
71
+ }
72
+
73
+ /**
74
+ * The palette, as `DuxtOpenApiMethod` builds one: Tailwind's ramp with a
75
+ * lighter text in dark mode, so every pair clears the contrast floor in both
76
+ * themes without adding six token pairs a consumer would have to override.
77
+ */
78
+ const TONES: DuxtChangelogTone[] = [
79
+ {
80
+ dot: 'bg-emerald-500',
81
+ text: 'text-emerald-700 dark:text-emerald-400',
82
+ chip: 'bg-emerald-500/10 text-emerald-700 ring-emerald-500/30 dark:text-emerald-300'
83
+ },
84
+ {
85
+ dot: 'bg-sky-500',
86
+ text: 'text-sky-700 dark:text-sky-400',
87
+ chip: 'bg-sky-500/10 text-sky-700 ring-sky-500/30 dark:text-sky-300'
88
+ },
89
+ {
90
+ dot: 'bg-amber-500',
91
+ text: 'text-amber-700 dark:text-amber-400',
92
+ chip: 'bg-amber-500/10 text-amber-700 ring-amber-500/30 dark:text-amber-300'
93
+ },
94
+ {
95
+ dot: 'bg-violet-500',
96
+ text: 'text-violet-700 dark:text-violet-400',
97
+ chip: 'bg-violet-500/10 text-violet-700 ring-violet-500/30 dark:text-violet-300'
98
+ },
99
+ {
100
+ dot: 'bg-rose-500',
101
+ text: 'text-rose-700 dark:text-rose-400',
102
+ chip: 'bg-rose-500/10 text-rose-700 ring-rose-500/30 dark:text-rose-300'
103
+ },
104
+ {
105
+ dot: 'bg-cyan-500',
106
+ text: 'text-cyan-700 dark:text-cyan-400',
107
+ chip: 'bg-cyan-500/10 text-cyan-700 ring-cyan-500/30 dark:text-cyan-300'
108
+ }
109
+ ];
110
+
111
+ /**
112
+ * The names that get the colour a reader expects, keyed by an English word the
113
+ * heading CONTAINS.
114
+ *
115
+ * A HINT, never a taxonomy — which is the whole difference from a mapping the
116
+ * parser would have needed. `sections-changelog.ts` takes group names verbatim
117
+ * because a fixed set fails silently on the first heading it does not know;
118
+ * here the failure is that a group is drawn cyan instead of green, and the
119
+ * fallback below still gives it a colour of its own. So the list stays short
120
+ * and covers what release-please and Keep a Changelog write, and a changelog
121
+ * kept in another language simply falls through to the hash.
122
+ */
123
+ const BREAKING = /break|inkompat|incompat/i;
124
+
125
+ const HINTS: [RegExp, number][] = [
126
+ [BREAKING, 4],
127
+ [/feat|added|neu/i, 0],
128
+ [/fix|bug|behoben/i, 2],
129
+ [/perf/i, 3],
130
+ [/doc/i, 1],
131
+ [/deprecat|remov|revert/i, 4],
132
+ [/depend|chore|build|ci|refactor|misc/i, 5]
133
+ ];
134
+
135
+ /**
136
+ * A stable colour for a group name.
137
+ *
138
+ * The fallback is a hash rather than one grey for everything, because the point
139
+ * of the colour is that a reader scanning forty releases finds the same kind of
140
+ * change in the same colour every time — which a hash gives in any language,
141
+ * for any heading, with nothing configured.
142
+ */
143
+ export function changelogTone(name: string): DuxtChangelogTone {
144
+ for (const [pattern, index] of HINTS) {
145
+ if (pattern.test(name)) return TONES[index]!;
146
+ }
147
+
148
+ let hash = 0;
149
+ for (const character of name) {
150
+ hash = (hash * 31 + character.codePointAt(0)!) % 0xffffffff;
151
+ }
152
+
153
+ return TONES[hash % TONES.length]!;
154
+ }
155
+
156
+ /**
157
+ * A release day, in the reader's language.
158
+ *
159
+ * `timeZone: 'UTC'` because the value is a calendar date and not a moment: read
160
+ * as local time, `2026-09-08` is the 7th for every reader west of Greenwich —
161
+ * and a different day on the server than in the browser, which is a hydration
162
+ * mismatch as well as a wrong date.
163
+ *
164
+ * Here rather than beside one of its callers because TWO of them draw it — the
165
+ * timeline on the overview and the meta row on the release page — and a date
166
+ * formatted in two places is two chances to print two different days for one
167
+ * release.
168
+ */
169
+ export function changelogDate(
170
+ value: string | undefined,
171
+ locale: string
172
+ ): string | undefined {
173
+ if (!value) return undefined;
174
+
175
+ const date = new Date(value);
176
+ if (Number.isNaN(date.getTime())) return undefined;
177
+
178
+ return new Intl.DateTimeFormat(locale, {
179
+ dateStyle: 'medium',
180
+ timeZone: 'UTC'
181
+ }).format(date);
182
+ }
@@ -0,0 +1,116 @@
1
+ import { grammars as generated } from '#build/duxt-grammars.mjs';
2
+
3
+ /**
4
+ * The generated map, annotated here rather than trusted from the declaration.
5
+ *
6
+ * `writeGrammars` writes both the module and its `.d.ts`, and the file cannot
7
+ * import Shiki's own types to describe itself with — so the shape is stated at
8
+ * the one place that uses it.
9
+ */
10
+ const grammars = generated as Record<string, () => Promise<unknown>>;
11
+
12
+ /**
13
+ * Highlight code the site builds at RUNTIME, with the themes the fences use.
14
+ *
15
+ * Content highlights every Markdown fence at build time, and three things on
16
+ * this site never pass through it: the package-manager block's command, which
17
+ * is assembled from a prop; the example bodies and responses the API reference
18
+ * derives from a schema; and the request samples the try-it client rewrites on
19
+ * every keystroke. All of them came out as flat grey text beside fences that
20
+ * were coloured, which reads as the highlighter being switched off.
21
+ *
22
+ * BUILT ON `shiki/core`, not on the bundled entry, because this one runs in the
23
+ * BROWSER too — the sample changes as the reader types, and the server cannot
24
+ * re-render it. The bundled entry carries every grammar Shiki ships; the core
25
+ * takes only what this site actually generates, plus the JavaScript regex
26
+ * engine, which is what keeps the WASM out of the chunk. Loaded on demand either
27
+ * way, and cached for the life of the process.
28
+ *
29
+ * THE GRAMMAR SET IS GENERATED, from the samples a site configured — see
30
+ * `writeGrammars` in `modules/config.ts`. It used to be three names hard-coded
31
+ * here, which was right while the samples were `curl` and `fetch` and wrong the
32
+ * moment `duxt.requestSamples` became a list a consumer writes: a Python sample
33
+ * needs a Python grammar, and nobody wants to ship one to a site that has no
34
+ * Python sample. Every language in the map is bytes a reader downloads, which is
35
+ * exactly why the map is not simply all of Shiki.
36
+ *
37
+ * `defaultColor: false` emits both themes as custom properties per token,
38
+ * matching how Content's own output is styled.
39
+ */
40
+ type Highlighter = {
41
+ codeToHtml: (code: string, options: object) => string;
42
+ };
43
+
44
+ /**
45
+ * Spellings that are not grammar names.
46
+ *
47
+ * Shiki resolves its own aliases once a grammar is loaded, but the map above is
48
+ * keyed by the names the samples declare — so a `ts` fence has to be pointed at
49
+ * `typescript` before the lookup, not after it.
50
+ */
51
+ const aliases: Record<string, string> = {
52
+ curl: 'bash',
53
+ js: 'typescript',
54
+ javascript: 'typescript',
55
+ sh: 'bash',
56
+ shell: 'bash',
57
+ ts: 'typescript'
58
+ };
59
+
60
+ let highlighter: Promise<Highlighter> | undefined;
61
+
62
+ async function load(): Promise<Highlighter> {
63
+ const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] =
64
+ await Promise.all([
65
+ import('shiki/core'),
66
+ import('shiki/engine/javascript')
67
+ ]);
68
+
69
+ return createHighlighterCore({
70
+ themes: [
71
+ import('shiki/themes/github-light.mjs'),
72
+ import('shiki/themes/github-dark.mjs')
73
+ ],
74
+ // Cast because the generated module is written by `writeGrammars` and
75
+ // cannot import Shiki's `LanguageRegistration` to type itself with — the
76
+ // values ARE those modules, which is what the loaders resolve to.
77
+ langs: Object.values(grammars).map((grammar) => grammar()) as Parameters<
78
+ typeof createHighlighterCore
79
+ >[0]['langs'],
80
+ engine: createJavaScriptRegexEngine()
81
+ }) as Promise<Highlighter>;
82
+ }
83
+
84
+ /**
85
+ * The language, when it is one this site generates and highlights.
86
+ *
87
+ * Undefined for everything else, which is how a block asks for plain text
88
+ * rather than for a grammar the core highlighter was never given: an unknown
89
+ * `lang` makes Shiki throw, and a response body labelled `xml` must not take
90
+ * the page with it.
91
+ */
92
+ export function duxtCodeLang(lang?: string): string | undefined {
93
+ if (!lang) return undefined;
94
+
95
+ const value = lang.toLowerCase();
96
+ const resolved = aliases[value] ?? value;
97
+
98
+ return resolved in grammars ? resolved : undefined;
99
+ }
100
+
101
+ export async function highlightCode(
102
+ code: string,
103
+ lang = 'bash'
104
+ ): Promise<string> {
105
+ // One highlighter for the process: loading the grammars per call would repeat
106
+ // the most expensive part of this for every command on the page.
107
+ highlighter ??= load();
108
+
109
+ const shiki = await highlighter;
110
+
111
+ return shiki.codeToHtml(code, {
112
+ lang,
113
+ themes: { light: 'github-light', dark: 'github-dark' },
114
+ defaultColor: false
115
+ });
116
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The layer's defaults, and how a consumer's config is merged over them.
3
+ *
4
+ * Nuxt merges app.config with defu, which CONCATENATES arrays: a consumer
5
+ * setting `navigation: [...]` would get its own entries plus the layer's, in
6
+ * that order, with no way to remove ours. Every list here would be unusable.
7
+ *
8
+ * So the layer ships no lists in app.config at all. It keeps them here and
9
+ * merges them itself, replacing arrays instead of appending to them — a
10
+ * consumer's list is the list. Objects still merge key by key, so overriding
11
+ * `footer.note` leaves `footer.columns` alone.
12
+ */
13
+ export const duxtDefaults: DuxtConfig = {
14
+ // A placeholder, not a name. The layer cannot know what a site is called,
15
+ // and `title` is read by thirteen call sites — the title template, the 404,
16
+ // the brand, the OG card, schema.org, llms.txt and the feed — several of
17
+ // which interpolate it into a template literal and would print the word
18
+ // `undefined` rather than degrade. So a neutral, translated word stands in
19
+ // until a consumer sets its own. It names nobody, which is the whole test.
20
+ title: 'duxt.defaults.title',
21
+
22
+ // No `version`. The layer knows nothing about the state of somebody else's
23
+ // project, and a default made every site that extended it wear a number it
24
+ // had never set — in the hero pill and, for a site with at most one source,
25
+ // in the header badge beside the search. A site that has a version says so
26
+ // in its own `app.config.ts`; `DuxtVersion` draws nothing until it does.
27
+
28
+ navigation: [
29
+ // No `to`: the header resolves it to the first section, so the entry works
30
+ // whether or not the consumer's URLs carry a prefix.
31
+ //
32
+ // One entry where `sections` and `links` ship empty, because "Docs" names
33
+ // the INTERFACE and not a tree or a repository. The dropdown that used to
34
+ // sit beside it pointed at duxt's own tech stack — five links belonging to
35
+ // this project and to no site that extends it. It lives in
36
+ // `www/app/app.config.ts` now.
37
+ { label: 'duxt.defaults.navigation.docs', icon: 'lucide:book-open-text' }
38
+ ],
39
+
40
+ /**
41
+ * Empty, like `links` below — and for a reason one step further out.
42
+ *
43
+ * The section row names the top-level parts of a documentation TREE, and the
44
+ * tree belongs to the consumer. Shipping "Get started / Concepts / Guides /
45
+ * Reference" pointing at `/getting-started`, `/concepts`, … was the layer
46
+ * guessing at somebody else's folder names: a site whose docs are shaped
47
+ * differently got four tabs leading nowhere, and every one of them looked
48
+ * like a bug in duxt rather than a default it had never been told to change.
49
+ *
50
+ * Those labels were duxt's OWN documentation showing through the layer, so
51
+ * their translations left with them — `duxt.defaults.*` translates the interface
52
+ * layer draws, never content a site writes.
53
+ *
54
+ * Left empty, the row does not render and the sidebar falls back to the whole
55
+ * tree, which is the right shape for a site that has not split its docs into
56
+ * sections at all. duxt's own sections live in `www/app/app.config.ts`.
57
+ */
58
+ sections: [],
59
+
60
+ /**
61
+ * Empty, like `footer.legal`.
62
+ *
63
+ * These are the icon links on the right of the navbar, and they belong to
64
+ * whoever runs the site. Shipping duxt's own repository here gave a stranger
65
+ * extending the layer a GitHub icon pointing at somebody else's project —
66
+ * the same mistake the legal row already settled. kirchDev's own links live
67
+ * in `www/app/app.config.ts`, where they are an example rather than a
68
+ * default.
69
+ *
70
+ * Their i18n keys went with them. `duxt.defaults.*` translates the defaults
71
+ * the layer itself ships and nothing else — it is not a vocabulary for
72
+ * consumers to reach into. A consumer that did would depend on a key it
73
+ * cannot see being renamed, and i18n answers a missing key by printing the
74
+ * key, so the break would be silent. Consumers write a literal, their own
75
+ * key, or the record form; `www` is the worked example of the third.
76
+ */
77
+ links: [],
78
+
79
+ /** Which package managers a command block offers, in the order it shows them. */
80
+ packageManagers: ['pnpm', 'npm', 'yarn', 'bun'],
81
+
82
+ /** A flat docs tree gets a trail that only repeats its own section name. */
83
+ breadcrumb: true,
84
+
85
+ landing: {
86
+ // No badge, no headline, no description. A pill above the headline says
87
+ // something about the state of a project — "beta", "v2 is out" — and the
88
+ // headline and the paragraph under it say what the project IS. The layer
89
+ // knows none of the three. Those two were duxt's OWN marketing showing
90
+ // through every site that extended it; they live in `www/app/app.config.ts`
91
+ // now, in the record form, because the Nitro routes that read them have no
92
+ // i18n — see `server/utils/duxt-server-text.ts`.
93
+ //
94
+ // Unset, the h1 falls back to `title` and the paragraph does not render at
95
+ // all. Set `landing.badge`, `landing.headline` or `landing.description`
96
+ // and each appears.
97
+
98
+ // One action, and a generic one: "read the docs" is true of every site
99
+ // built on this layer. A second button pointing at duxt's own repository
100
+ // was not — see `links` above.
101
+ // No `to`, for the same reason the section row above ships empty: the
102
+ // layer knows that a site HAS documentation, never what its first page is
103
+ // called. The landing resolves it to the first section, or to `/`.
104
+ actions: [
105
+ {
106
+ label: 'duxt.defaults.landing.actions.docs',
107
+ icon: 'lucide:arrow-right'
108
+ }
109
+ ],
110
+ /**
111
+ * Empty, like `sections` and `links` above.
112
+ *
113
+ * The six cards that used to sit here described duxt — its source list, its
114
+ * git-native sourcing, its shadcn-vue base. That is duxt's own marketing
115
+ * copy, not a default another site would keep: a stranger extending the
116
+ * layer got a landing page selling somebody else's project.
117
+ *
118
+ * Their translations left with them — `duxt.defaults.*` translates the
119
+ * interface the layer draws, never content a site writes. duxt's own cards
120
+ * live in `www/app/app.config.ts`, where each one POINTS at the page that
121
+ * explains it, which the layer could never know.
122
+ *
123
+ * Left empty, the band does not render: the landing gates the whole section
124
+ * on `duxt.landing?.features?.length`.
125
+ */
126
+ features: []
127
+ },
128
+
129
+ /**
130
+ * Title only, no links — same reasoning as `links` above.
131
+ *
132
+ * "Star on GitHub" that stars duxt, an issue tracker that is not yours and a
133
+ * Discord that is somebody else's community are worse than an empty column.
134
+ * The block draws nothing until a consumer fills it.
135
+ *
136
+ * The title is the exception that stays: "Community" is interface text the layer
137
+ * draws itself, in every language it ships, and it is not anybody's link.
138
+ */
139
+ aside: {
140
+ title: 'duxt.defaults.aside.title'
141
+ }
142
+ };
143
+
144
+ type Plain = Record<string, unknown>;
145
+
146
+ const isPlainObject = (value: unknown): value is Plain =>
147
+ typeof value === 'object' && value !== null && !Array.isArray(value);
148
+
149
+ /** Merge objects key by key; an array on the left replaces the one on the right. */
150
+ export function mergeDuxtConfig<T>(over: unknown, base: T): T {
151
+ if (Array.isArray(over)) return over as T;
152
+ if (over === undefined) return base;
153
+ if (!isPlainObject(over) || !isPlainObject(base)) return over as T;
154
+
155
+ const result: Plain = { ...base };
156
+ for (const [key, value] of Object.entries(over)) {
157
+ result[key] = mergeDuxtConfig(value, (base as Plain)[key]);
158
+ }
159
+
160
+ return result as T;
161
+ }
162
+
163
+ /**
164
+ * How many ROWS a band draws, which is not always one.
165
+ *
166
+ * The bands alternate — prose left, prose right, prose left — and the side is
167
+ * decided by position. A split try-it client is two rows inside one band, so
168
+ * counting bands puts every band after it on the side it just used. Counting
169
+ * rows is what keeps the page alternating across it.
170
+ *
171
+ * Pure, and here rather than in the component, because the page needs the
172
+ * answer BEFORE it renders the band: the offset of band four depends on how
173
+ * many rows bands one to three drew.
174
+ */
175
+ export function duxtShowcaseRows(showcase: {
176
+ full?: boolean;
177
+ demo?: { type?: string; layout?: string };
178
+ }): number {
179
+ const demo = showcase.demo;
180
+ if (demo?.type !== 'operation') return 1;
181
+
182
+ const layout = demo.layout ?? (showcase.full ? 'split' : 'panel');
183
+
184
+ return layout === 'split' ? 2 : 1;
185
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * A configured string that may also be a translation.
3
+ *
4
+ * Three shapes, because they serve three different consumers and none of them
5
+ * is served well by the other two:
6
+ *
7
+ * - a LITERAL — `'Guide'` — for the single-language site, which is most of
8
+ * them, and which must not have to learn i18n to name a navbar entry;
9
+ * - a KEY — `'nav.guide'` — for the site that already has locale files and
10
+ * wants the theme's own interface text to live in them;
11
+ * - a RECORD — `{ 'en-GB': 'Guide', 'de-DE': 'Anleitung' }` — for the site
12
+ * with two languages and eight labels, where creating and registering a
13
+ * locale file per language costs more than it saves.
14
+ *
15
+ * A literal and a key are the same TYPE, and the difference between them is
16
+ * decided by whether the key is registered, never by a heuristic on the text.
17
+ */
18
+
19
+ /** How a string is looked up. Returns undefined when nothing is registered. */
20
+ export type DuxtTextLookup = (key: string) => string | undefined;
21
+
22
+ /**
23
+ * Config keys whose values are prose.
24
+ *
25
+ * An allowlist rather than a walk over every string: `to`, `icon`, `variant`,
26
+ * `prefix` and `collection` are also strings, and running a URL through a
27
+ * translation lookup is how a link silently becomes something else.
28
+ */
29
+ const TEXT_KEYS = new Set([
30
+ 'badge',
31
+ 'copyright',
32
+ 'description',
33
+ 'group',
34
+ 'headline',
35
+ // A heading whose list is a sibling rather than a child, so it cannot be
36
+ // spelled `title` and be resolved by the entry above. A prose key added
37
+ // WITHOUT a line here does not fail — it renders its own locale record as
38
+ // JSON on the page, which is exactly how this one was found.
39
+ 'highlightsTitle',
40
+ 'label',
41
+ 'title'
42
+ ]);
43
+
44
+ /**
45
+ * A locale code, as a record's keys are written: `en`, `en-GB`, `zh-Hant-TW`.
46
+ *
47
+ * Needed because a text key may now hold something that is NOT a text: a badge
48
+ * is `{ label, icon, variant, to }`, and a record of locales is
49
+ * `{ 'en-GB': …, 'de-DE': … }`. Both are objects of strings, so the values
50
+ * cannot tell them apart — the KEYS can, and a config field whose keys are all
51
+ * locale codes is a locale record. `to` alone would match, which is why every
52
+ * key has to: `label` never will.
53
+ */
54
+ const LOCALE_CODE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
55
+
56
+ /** Whether a value is a text to resolve, rather than a tree to walk into. */
57
+ function isTextValue(value: unknown): boolean {
58
+ if (typeof value === 'string') return true;
59
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
60
+
61
+ const entries = Object.entries(value);
62
+ return (
63
+ entries.length > 0 &&
64
+ entries.every(
65
+ ([key, entry]) => typeof entry === 'string' && LOCALE_CODE.test(key)
66
+ )
67
+ );
68
+ }
69
+
70
+ /** One value: key if one is registered, own language, base language, or as written. */
71
+ export function resolveDuxtText(
72
+ value: unknown,
73
+ locale: string,
74
+ lookup: DuxtTextLookup
75
+ ): unknown {
76
+ if (typeof value === 'string') return lookup(value) ?? value;
77
+
78
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
79
+ const record = value as Record<string, string>;
80
+ if (Object.values(record).some((entry) => typeof entry !== 'string')) {
81
+ return value;
82
+ }
83
+
84
+ // `pt-BR` falls back to any `pt-*` the record does carry, mirroring what
85
+ // the locale FILES already do — a record written for `pt-PT` should not
86
+ // leave a Brazilian reader with English.
87
+ const base = locale.split('-')[0];
88
+ const sameLanguage = Object.entries(record).find(
89
+ ([code]) => code.split('-')[0] === base
90
+ );
91
+
92
+ return record[locale] ?? sameLanguage?.[1] ?? Object.values(record)[0];
93
+ }
94
+
95
+ return value;
96
+ }
97
+
98
+ /** The same, over a whole config tree: only the keys above are touched. */
99
+ export function resolveDuxtTexts<T>(
100
+ node: T,
101
+ locale: string,
102
+ lookup: DuxtTextLookup
103
+ ): T {
104
+ if (Array.isArray(node)) {
105
+ return node.map((entry) =>
106
+ resolveDuxtTexts(entry, locale, lookup)
107
+ ) as unknown as T;
108
+ }
109
+
110
+ if (!node || typeof node !== 'object') return node;
111
+
112
+ const result: Record<string, unknown> = {};
113
+ for (const [key, value] of Object.entries(node)) {
114
+ // A text key may carry a structure instead — `badge` takes either a text or
115
+ // a whole badge — so the value decides, not the key alone.
116
+ result[key] =
117
+ TEXT_KEYS.has(key) && isTextValue(value)
118
+ ? resolveDuxtText(value, locale, lookup)
119
+ : resolveDuxtTexts(value, locale, lookup);
120
+ }
121
+
122
+ return result as T;
123
+ }
124
+
125
+ /**
126
+ * A text field where a plain string is required — an aria-label, a title
127
+ * attribute, a list key.
128
+ *
129
+ * The config type keeps `DuxtText` because that is what a consumer may WRITE;
130
+ * by the time a component reads the value, `useDuxtConfig` has resolved it.
131
+ * This states that narrowing without asserting it: an unresolved record yields
132
+ * undefined rather than `[object Object]` in the DOM.
133
+ */
134
+ export const asText = (value: DuxtText | undefined): string | undefined =>
135
+ typeof value === 'string' ? value : undefined;