@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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/app.config.ts +13 -0
- package/app/app.vue +122 -0
- package/app/assets/css/duxt.css +555 -0
- package/app/assets/css/typeset.css +498 -0
- package/app/components/DuxtBrand.vue +53 -0
- package/app/components/DuxtBreadcrumb.vue +37 -0
- package/app/components/DuxtCodeBlock.vue +143 -0
- package/app/components/DuxtCopyPage.vue +117 -0
- package/app/components/DuxtFooter.vue +86 -0
- package/app/components/DuxtHeader.vue +496 -0
- package/app/components/DuxtJsonEditor.vue +359 -0
- package/app/components/DuxtLandingProse.vue +90 -0
- package/app/components/DuxtLandingShowcase.vue +295 -0
- package/app/components/DuxtLiveWindow.vue +343 -0
- package/app/components/DuxtLocale.vue +86 -0
- package/app/components/DuxtMetaList.vue +76 -0
- package/app/components/DuxtNavigation.vue +94 -0
- package/app/components/DuxtNavigationLink.vue +41 -0
- package/app/components/DuxtOpenApiCallbacks.vue +92 -0
- package/app/components/DuxtOpenApiClient.vue +1339 -0
- package/app/components/DuxtOpenApiMedia.vue +108 -0
- package/app/components/DuxtOpenApiMethod.vue +59 -0
- package/app/components/DuxtOpenApiParameters.vue +114 -0
- package/app/components/DuxtOpenApiResponses.vue +109 -0
- package/app/components/DuxtOpenApiSchema.vue +365 -0
- package/app/components/DuxtOpenApiSecurity.vue +115 -0
- package/app/components/DuxtPageFeedback.vue +56 -0
- package/app/components/DuxtPageInfo.vue +194 -0
- package/app/components/DuxtPageNav.vue +93 -0
- package/app/components/DuxtPreviewSkeleton.vue +262 -0
- package/app/components/DuxtProgress.vue +45 -0
- package/app/components/DuxtSearch.vue +271 -0
- package/app/components/DuxtSections.vue +65 -0
- package/app/components/DuxtShortcuts.vue +46 -0
- package/app/components/DuxtSkipLink.vue +34 -0
- package/app/components/DuxtToc.vue +86 -0
- package/app/components/DuxtTranslationBanner.vue +64 -0
- package/app/components/DuxtVersion.vue +168 -0
- package/app/components/DuxtVersionBanner.vue +109 -0
- package/app/components/OgImage/Duxt.satori.vue +82 -0
- package/app/components/content/Accordion.vue +37 -0
- package/app/components/content/AccordionItem.vue +46 -0
- package/app/components/content/Callout.vue +58 -0
- package/app/components/content/ChangelogGroup.vue +91 -0
- package/app/components/content/ChangelogReleases.vue +520 -0
- package/app/components/content/CodeGroup.vue +183 -0
- package/app/components/content/DevtoolsPanel.vue +107 -0
- package/app/components/content/Field.vue +50 -0
- package/app/components/content/FieldGroup.vue +8 -0
- package/app/components/content/FileTree.vue +92 -0
- package/app/components/content/Mermaid.vue +70 -0
- package/app/components/content/OpenApiOperation.vue +185 -0
- package/app/components/content/OpenApiOperations.vue +72 -0
- package/app/components/content/OpenApiOverview.vue +302 -0
- package/app/components/content/PackageManagers.vue +187 -0
- package/app/components/content/PageCards.vue +48 -0
- package/app/components/content/Partial.vue +65 -0
- package/app/components/content/Preview.vue +173 -0
- package/app/components/content/ProseA.vue +55 -0
- package/app/components/content/ProseH2.vue +53 -0
- package/app/components/content/ProseH3.vue +53 -0
- package/app/components/content/ProseH4.vue +53 -0
- package/app/components/content/ProseImg.vue +87 -0
- package/app/components/content/ProsePre.vue +58 -0
- package/app/components/content/ProseTable.vue +11 -0
- package/app/components/content/Since.vue +21 -0
- package/app/components/content/Steps.vue +20 -0
- package/app/components/content/Tab.vue +23 -0
- package/app/components/content/TabGroup.vue +58 -0
- package/app/components/ui/alert/Alert.vue +21 -0
- package/app/components/ui/alert/AlertDescription.vue +22 -0
- package/app/components/ui/alert/AlertTitle.vue +22 -0
- package/app/components/ui/alert/index.ts +24 -0
- package/app/components/ui/badge/Badge.vue +28 -0
- package/app/components/ui/badge/index.ts +52 -0
- package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
- package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
- package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- package/app/components/ui/breadcrumb/index.ts +7 -0
- package/app/components/ui/button/Button.vue +31 -0
- package/app/components/ui/button/index.ts +39 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +22 -0
- package/app/components/ui/card/CardContent.vue +14 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +22 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/collapsible/Collapsible.vue +19 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/app/components/ui/collapsible/index.ts +3 -0
- package/app/components/ui/command/Command.vue +98 -0
- package/app/components/ui/command/CommandDialog.vue +42 -0
- package/app/components/ui/command/CommandEmpty.vue +31 -0
- package/app/components/ui/command/CommandGroup.vue +52 -0
- package/app/components/ui/command/CommandInput.vue +45 -0
- package/app/components/ui/command/CommandItem.vue +85 -0
- package/app/components/ui/command/CommandList.vue +32 -0
- package/app/components/ui/command/CommandSeparator.vue +23 -0
- package/app/components/ui/command/CommandShortcut.vue +19 -0
- package/app/components/ui/command/index.ts +29 -0
- package/app/components/ui/dialog/Dialog.vue +15 -0
- package/app/components/ui/dialog/DialogClose.vue +12 -0
- package/app/components/ui/dialog/DialogContent.vue +61 -0
- package/app/components/ui/dialog/DialogDescription.vue +25 -0
- package/app/components/ui/dialog/DialogFooter.vue +30 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +28 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/app/components/ui/dialog/DialogTitle.vue +25 -0
- package/app/components/ui/dialog/DialogTrigger.vue +12 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/app/components/ui/dropdown-menu/index.ts +16 -0
- package/app/components/ui/input/Input.vue +35 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
- package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
- package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
- package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
- package/app/components/ui/navigation-menu/index.ts +14 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/select/Select.vue +15 -0
- package/app/components/ui/select/SelectContent.vue +59 -0
- package/app/components/ui/select/SelectGroup.vue +19 -0
- package/app/components/ui/select/SelectItem.vue +42 -0
- package/app/components/ui/select/SelectItemText.vue +12 -0
- package/app/components/ui/select/SelectLabel.vue +16 -0
- package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
- package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
- package/app/components/ui/select/SelectSeparator.vue +20 -0
- package/app/components/ui/select/SelectTrigger.vue +32 -0
- package/app/components/ui/select/SelectValue.vue +12 -0
- package/app/components/ui/select/index.ts +11 -0
- package/app/components/ui/separator/Separator.vue +30 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +15 -0
- package/app/components/ui/sheet/SheetClose.vue +12 -0
- package/app/components/ui/sheet/SheetContent.vue +68 -0
- package/app/components/ui/sheet/SheetDescription.vue +23 -0
- package/app/components/ui/sheet/SheetFooter.vue +15 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +28 -0
- package/app/components/ui/sheet/SheetTitle.vue +23 -0
- package/app/components/ui/sheet/SheetTrigger.vue +12 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/sidebar/Sidebar.vue +110 -0
- package/app/components/ui/sidebar/SidebarContent.vue +23 -0
- package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
- package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/app/components/ui/sidebar/SidebarInput.vue +19 -0
- package/app/components/ui/sidebar/SidebarInset.vue +23 -0
- package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
- package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
- package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
- package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
- package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
- package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
- package/app/components/ui/sidebar/SidebarRail.vue +35 -0
- package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
- package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
- package/app/components/ui/sidebar/index.ts +62 -0
- package/app/components/ui/sidebar/utils.ts +19 -0
- package/app/components/ui/skeleton/Skeleton.vue +17 -0
- package/app/components/ui/skeleton/index.ts +1 -0
- package/app/components/ui/sonner/Sonner.vue +67 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/components/ui/tabs/Tabs.vue +26 -0
- package/app/components/ui/tabs/TabsContent.vue +23 -0
- package/app/components/ui/tabs/TabsList.vue +28 -0
- package/app/components/ui/tabs/TabsTrigger.vue +30 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/textarea/Textarea.vue +32 -0
- package/app/components/ui/textarea/index.ts +1 -0
- package/app/components/ui/tooltip/Tooltip.vue +15 -0
- package/app/components/ui/tooltip/TooltipContent.vue +49 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useActiveHeading.ts +90 -0
- package/app/composables/useDuxtAnimatedHeight.ts +155 -0
- package/app/composables/useDuxtBreadcrumb.ts +60 -0
- package/app/composables/useDuxtChoice.ts +28 -0
- package/app/composables/useDuxtCollection.ts +89 -0
- package/app/composables/useDuxtConfig.ts +47 -0
- package/app/composables/useDuxtNavigation.ts +87 -0
- package/app/composables/useDuxtPageFocus.ts +30 -0
- package/app/composables/useDuxtPath.ts +42 -0
- package/app/composables/useDuxtSearch.ts +208 -0
- package/app/composables/useDuxtSection.ts +44 -0
- package/app/composables/useDuxtSectionRow.ts +49 -0
- package/app/composables/useDuxtShortcuts.ts +58 -0
- package/app/composables/useDuxtSiteUrl.ts +27 -0
- package/app/composables/useDuxtToast.ts +25 -0
- package/app/composables/useDuxtVersion.ts +138 -0
- package/app/composables/useFuzzySearch.ts +83 -0
- package/app/composables/usePackageManager.ts +4 -0
- package/app/composables/useRecentPages.ts +52 -0
- package/app/error.vue +133 -0
- package/app/layouts/changelog.vue +85 -0
- package/app/layouts/default.vue +23 -0
- package/app/layouts/docs.vue +62 -0
- package/app/layouts/reference.vue +85 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/duxt-section-layout.global.ts +57 -0
- package/app/pages/[...slug].vue +441 -0
- package/app/pages/index.vue +479 -0
- package/app/types/duxt.d.ts +955 -0
- package/app/utils/aside.ts +16 -0
- package/app/utils/changelog.ts +182 -0
- package/app/utils/code-highlight.ts +116 -0
- package/app/utils/duxt-config.ts +185 -0
- package/app/utils/duxt-text.ts +135 -0
- package/app/utils/file-icons.ts +62 -0
- package/app/utils/generated-sections.ts +190 -0
- package/app/utils/generated-toc.ts +216 -0
- package/app/utils/heading-link.ts +32 -0
- package/app/utils/locale-path.ts +45 -0
- package/app/utils/navigation-tree.ts +206 -0
- package/app/utils/nearest-page.ts +72 -0
- package/app/utils/openapi.ts +1028 -0
- package/app/utils/package-command.ts +162 -0
- package/app/utils/request-samples.ts +635 -0
- package/app/utils/section-scope.ts +97 -0
- package/app/utils/sheet-navigation.ts +100 -0
- package/app/utils/source-file.ts +84 -0
- package/app/utils/version-choices.ts +68 -0
- package/app/utils/version-paths.ts +65 -0
- package/bin/duxt-report.mjs +45 -0
- package/components.json +19 -0
- package/content-cache.ts +193 -0
- package/content.config.ts +38 -0
- package/duxt-app-config.ts +124 -0
- package/highlight-langs.ts +68 -0
- package/i18n/i18n.config.ts +16 -0
- package/i18n/locales/de/duxt/changelog.json +17 -0
- package/i18n/locales/de/duxt/code.json +13 -0
- package/i18n/locales/de/duxt/defaults.json +22 -0
- package/i18n/locales/de/duxt/devtools.json +7 -0
- package/i18n/locales/de/duxt/error.json +10 -0
- package/i18n/locales/de/duxt/footer.json +7 -0
- package/i18n/locales/de/duxt/locale.json +7 -0
- package/i18n/locales/de/duxt/nav.json +17 -0
- package/i18n/locales/de/duxt/openapi.json +89 -0
- package/i18n/locales/de/duxt/page.json +34 -0
- package/i18n/locales/de/duxt/search.json +12 -0
- package/i18n/locales/de/duxt/shortcuts.json +11 -0
- package/i18n/locales/de/duxt/theme.json +7 -0
- package/i18n/locales/de/duxt/toc.json +7 -0
- package/i18n/locales/de/duxt/version.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +17 -0
- package/i18n/locales/en/duxt/code.json +13 -0
- package/i18n/locales/en/duxt/defaults.json +22 -0
- package/i18n/locales/en/duxt/devtools.json +7 -0
- package/i18n/locales/en/duxt/error.json +10 -0
- package/i18n/locales/en/duxt/footer.json +7 -0
- package/i18n/locales/en/duxt/locale.json +7 -0
- package/i18n/locales/en/duxt/nav.json +17 -0
- package/i18n/locales/en/duxt/openapi.json +89 -0
- package/i18n/locales/en/duxt/page.json +34 -0
- package/i18n/locales/en/duxt/search.json +12 -0
- package/i18n/locales/en/duxt/shortcuts.json +11 -0
- package/i18n/locales/en/duxt/theme.json +7 -0
- package/i18n/locales/en/duxt/toc.json +7 -0
- package/i18n/locales/en/duxt/version.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +17 -0
- package/i18n/locales/es/duxt/code.json +13 -0
- package/i18n/locales/es/duxt/defaults.json +22 -0
- package/i18n/locales/es/duxt/devtools.json +7 -0
- package/i18n/locales/es/duxt/error.json +10 -0
- package/i18n/locales/es/duxt/footer.json +7 -0
- package/i18n/locales/es/duxt/locale.json +7 -0
- package/i18n/locales/es/duxt/nav.json +17 -0
- package/i18n/locales/es/duxt/openapi.json +89 -0
- package/i18n/locales/es/duxt/page.json +34 -0
- package/i18n/locales/es/duxt/search.json +12 -0
- package/i18n/locales/es/duxt/shortcuts.json +11 -0
- package/i18n/locales/es/duxt/theme.json +7 -0
- package/i18n/locales/es/duxt/toc.json +7 -0
- package/i18n/locales/es/duxt/version.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +17 -0
- package/i18n/locales/fr/duxt/code.json +13 -0
- package/i18n/locales/fr/duxt/defaults.json +22 -0
- package/i18n/locales/fr/duxt/devtools.json +7 -0
- package/i18n/locales/fr/duxt/error.json +10 -0
- package/i18n/locales/fr/duxt/footer.json +7 -0
- package/i18n/locales/fr/duxt/locale.json +7 -0
- package/i18n/locales/fr/duxt/nav.json +17 -0
- package/i18n/locales/fr/duxt/openapi.json +89 -0
- package/i18n/locales/fr/duxt/page.json +34 -0
- package/i18n/locales/fr/duxt/search.json +12 -0
- package/i18n/locales/fr/duxt/shortcuts.json +11 -0
- package/i18n/locales/fr/duxt/theme.json +7 -0
- package/i18n/locales/fr/duxt/toc.json +7 -0
- package/i18n/locales/fr/duxt/version.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +17 -0
- package/i18n/locales/pt/duxt/code.json +13 -0
- package/i18n/locales/pt/duxt/defaults.json +22 -0
- package/i18n/locales/pt/duxt/devtools.json +7 -0
- package/i18n/locales/pt/duxt/error.json +10 -0
- package/i18n/locales/pt/duxt/footer.json +7 -0
- package/i18n/locales/pt/duxt/locale.json +7 -0
- package/i18n/locales/pt/duxt/nav.json +17 -0
- package/i18n/locales/pt/duxt/openapi.json +89 -0
- package/i18n/locales/pt/duxt/page.json +34 -0
- package/i18n/locales/pt/duxt/search.json +12 -0
- package/i18n/locales/pt/duxt/shortcuts.json +11 -0
- package/i18n/locales/pt/duxt/theme.json +7 -0
- package/i18n/locales/pt/duxt/toc.json +7 -0
- package/i18n/locales/pt/duxt/version.json +25 -0
- package/i18n/locales/pt-BR/duxt/error.json +8 -0
- package/i18n/locales/pt-BR/duxt/nav.json +10 -0
- package/i18n/locales/pt-BR/duxt/page.json +10 -0
- package/i18n/locales/pt-BR/duxt/search.json +11 -0
- package/i18n/locales/pt-BR/duxt/version.json +13 -0
- package/mdc.config.ts +28 -0
- package/modules/config.ts +440 -0
- package/modules/devtools.ts +126 -0
- package/modules/git-meta.ts +349 -0
- package/modules/redirects.ts +152 -0
- package/modules/validate.ts +137 -0
- package/nuxt.config.ts +530 -0
- package/openapi-model.ts +358 -0
- package/openapi-parse.ts +1052 -0
- package/package.json +161 -0
- package/public/devtools/cache.html +318 -0
- package/public/devtools/checks.html +309 -0
- package/public/devtools/config.html +461 -0
- package/public/devtools/i18n.html +309 -0
- package/public/devtools/pages.html +348 -0
- package/public/devtools/paths.html +315 -0
- package/public/devtools/redirects.html +309 -0
- package/public/devtools/search.html +324 -0
- package/public/devtools/sources.html +309 -0
- package/public/devtools/versions.html +318 -0
- package/report.ts +303 -0
- package/repository-root.ts +26 -0
- package/section-reports.ts +55 -0
- package/sections-changelog.ts +644 -0
- package/sections-openapi.ts +450 -0
- package/sections-resolve.ts +711 -0
- package/sections.ts +157 -0
- package/server/devtools/content.ts +120 -0
- package/server/devtools/context.ts +38 -0
- package/server/devtools/entry-path.ts +30 -0
- package/server/devtools/handler.ts +104 -0
- package/server/devtools/preview.ts +461 -0
- package/server/devtools/render/content.ts +233 -0
- package/server/devtools/render/sources.ts +414 -0
- package/server/devtools/render/system.ts +352 -0
- package/server/devtools/shell.ts +611 -0
- package/server/devtools/sources.ts +122 -0
- package/server/devtools/system.ts +178 -0
- package/server/mcp/tools/list-pages.ts +52 -0
- package/server/mcp/tools/read-page.ts +57 -0
- package/server/mcp/tools/search-docs.ts +74 -0
- package/server/middleware/raw-markdown.ts +86 -0
- package/server/routes/llms-full.txt.get.ts +75 -0
- package/server/routes/llms.txt.get.ts +63 -0
- package/server/routes/rss.xml.get.ts +125 -0
- package/server/utils/duxt-server-text.ts +46 -0
- package/sources-git.ts +113 -0
- package/sources-resolve.ts +908 -0
- package/sources.ts +287 -0
- package/validate-report.ts +494 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A box whose height FOLLOWS its content instead of switching to it.
|
|
3
|
+
*
|
|
4
|
+
* Two places need this and they need exactly the same thing: the request-sample
|
|
5
|
+
* card, whose `curl` of four lines becomes a Go client of twenty, and
|
|
6
|
+
* `CodeGroup`, whose tabs hold files of different lengths. Both used to change
|
|
7
|
+
* height in a single frame, which moves everything below them at once.
|
|
8
|
+
*
|
|
9
|
+
* MEASURED, because CSS cannot do it. `transition: height` needs two computed
|
|
10
|
+
* values to travel between, and a box that sizes to its content is `auto`
|
|
11
|
+
* before and `auto` after — no change a browser can see. `interpolate-size:
|
|
12
|
+
* allow-keywords` does not help either: it makes `auto` a value a transition
|
|
13
|
+
* can REACH, not one that transitions to itself.
|
|
14
|
+
*
|
|
15
|
+
* A `ResizeObserver` rather than a watcher on whatever changed, and that is the
|
|
16
|
+
* part that matters. Highlighted code arrives asynchronously — the string
|
|
17
|
+
* changes on the click, the markup a tick or two later — so anything measuring
|
|
18
|
+
* straight after the click measures the content on its way out. The observer
|
|
19
|
+
* measures what is actually there, whenever it is there, and catches the other
|
|
20
|
+
* reasons a box resizes too: the window narrowing, a line rewrapping, a font
|
|
21
|
+
* finishing loading.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The step the height moves in — a line of code plus its leading.
|
|
26
|
+
*
|
|
27
|
+
* A threshold on the CHANGE was the first attempt and it still jumped: below it
|
|
28
|
+
* the box snapped, above it the box slid, and a reader switching tabs got both.
|
|
29
|
+
* A grid removes the small changes instead of deciding what to do about them. A
|
|
30
|
+
* reflow of two pixels lands on the same step and is not a height change at
|
|
31
|
+
* all; a different file is at least one step away and always animates.
|
|
32
|
+
*
|
|
33
|
+
* The buffer it adds is never more than a line's worth of space under the last
|
|
34
|
+
* line.
|
|
35
|
+
*/
|
|
36
|
+
const HEIGHT_STEP = 24;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How long a measurement has to hold still before it counts.
|
|
40
|
+
*
|
|
41
|
+
* Long enough to cover an unmount, a mount and the tick that swaps plain code
|
|
42
|
+
* for highlighted code; short enough that the box does not feel late. Every
|
|
43
|
+
* resize inside the window replaces the one before it, so only the last one is
|
|
44
|
+
* ever acted on.
|
|
45
|
+
*/
|
|
46
|
+
const SETTLE_MS = 60;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How long the content takes to come back once the box is on its way.
|
|
50
|
+
*
|
|
51
|
+
* Shorter than the box's own 300ms on purpose: the point is to be readable
|
|
52
|
+
* again well before the box stops moving, not to make the reader wait out two
|
|
53
|
+
* animations in a row.
|
|
54
|
+
*/
|
|
55
|
+
const FADE_MS = 200;
|
|
56
|
+
|
|
57
|
+
export function useDuxtAnimatedHeight(
|
|
58
|
+
shell: Readonly<import('vue').ShallowRef<HTMLElement | null>>,
|
|
59
|
+
body: Readonly<import('vue').ShallowRef<HTMLElement | null>>,
|
|
60
|
+
/** Whether to follow at all — a layout that should size at once passes false. */
|
|
61
|
+
enabled: () => boolean = () => true
|
|
62
|
+
): void {
|
|
63
|
+
onMounted(() => {
|
|
64
|
+
if (!enabled() || !('ResizeObserver' in window)) return;
|
|
65
|
+
|
|
66
|
+
const box = shell.value;
|
|
67
|
+
const content = body.value;
|
|
68
|
+
if (!box || !content) return;
|
|
69
|
+
|
|
70
|
+
// The box carries the border and `box-sizing: border-box`, so its height
|
|
71
|
+
// has to include what that border takes; the content's height does not.
|
|
72
|
+
const chrome = box.offsetHeight - box.clientHeight;
|
|
73
|
+
|
|
74
|
+
const step = (height: number) =>
|
|
75
|
+
Math.ceil((height + chrome) / HEIGHT_STEP) * HEIGHT_STEP;
|
|
76
|
+
|
|
77
|
+
let last = step(content.offsetHeight);
|
|
78
|
+
|
|
79
|
+
// PIN THE STARTING HEIGHT, without animating to it.
|
|
80
|
+
//
|
|
81
|
+
// Until a height is set the box is `auto`, and `auto` is not a value a
|
|
82
|
+
// transition can travel FROM — so the very first change jumped instead of
|
|
83
|
+
// sliding, however carefully everything after it was handled. Set once, on
|
|
84
|
+
// mount, with the transition off for that one frame.
|
|
85
|
+
box.style.transition = 'none';
|
|
86
|
+
box.style.height = `${last}px`;
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
box.style.transition = '';
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
let settle: ReturnType<typeof setTimeout> | undefined;
|
|
92
|
+
|
|
93
|
+
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
94
|
+
|
|
95
|
+
const observer = new ResizeObserver(() => {
|
|
96
|
+
const next = step(content.offsetHeight);
|
|
97
|
+
if (next === last) return;
|
|
98
|
+
|
|
99
|
+
// THE CONTENT FADES WHILE THE BOX TRAVELS.
|
|
100
|
+
//
|
|
101
|
+
// The new panel is there the instant the tab is clicked, and until the
|
|
102
|
+
// box has grown to fit it, `overflow-hidden` cuts it off — a taller
|
|
103
|
+
// sample appeared with its last lines sliced away for the length of the
|
|
104
|
+
// animation. Hidden for that moment and faded back in as the box arrives,
|
|
105
|
+
// there is nothing cut off to see: what a reader watches is one sample
|
|
106
|
+
// replacing another, not a box catching up with its contents.
|
|
107
|
+
//
|
|
108
|
+
// Set with the transition off, so it is instant going out and animated
|
|
109
|
+
// coming back — a fade in both directions would be twice as long a wait
|
|
110
|
+
// for the same effect.
|
|
111
|
+
if (!reduced.matches) {
|
|
112
|
+
content.style.transition = 'none';
|
|
113
|
+
content.style.opacity = '0';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ONLY THE VALUE IT SETTLES ON, never the ones on the way.
|
|
117
|
+
//
|
|
118
|
+
// A tab change is not one resize, it is several: the old panel unmounts,
|
|
119
|
+
// leaving nothing for a frame; the new one mounts unhighlighted; the
|
|
120
|
+
// highlighted markup replaces it a tick later. Followed literally, the box
|
|
121
|
+
// collapsed to nothing, sprang open at one size and slid to another —
|
|
122
|
+
// which is the "the tab disappears first, then it expands" of it.
|
|
123
|
+
//
|
|
124
|
+
// Waiting for the measurement to hold still skips all of that: one
|
|
125
|
+
// animation, from the height before the click to the height after it.
|
|
126
|
+
clearTimeout(settle);
|
|
127
|
+
settle = setTimeout(() => {
|
|
128
|
+
const settled = step(content.offsetHeight);
|
|
129
|
+
|
|
130
|
+
if (settled !== last) {
|
|
131
|
+
last = settled;
|
|
132
|
+
box.style.height = `${settled}px`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!reduced.matches) {
|
|
136
|
+
requestAnimationFrame(() => {
|
|
137
|
+
content.style.transition = `opacity ${FADE_MS}ms ease-out`;
|
|
138
|
+
content.style.opacity = '1';
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}, SETTLE_MS);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
observer.observe(content);
|
|
145
|
+
|
|
146
|
+
onBeforeUnmount(() => {
|
|
147
|
+
observer.disconnect();
|
|
148
|
+
clearTimeout(settle);
|
|
149
|
+
box.style.height = '';
|
|
150
|
+
box.style.transition = '';
|
|
151
|
+
content.style.opacity = '';
|
|
152
|
+
content.style.transition = '';
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from '@nuxt/content';
|
|
2
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The trail from the section down to a page.
|
|
6
|
+
*
|
|
7
|
+
* Lives in a composable rather than in `DuxtBreadcrumb` because two things want
|
|
8
|
+
* it: the component draws it, and the page emits it as `BreadcrumbList`
|
|
9
|
+
* JSON-LD. Computing it twice is how the drawn trail and the structured one
|
|
10
|
+
* would come to disagree.
|
|
11
|
+
*/
|
|
12
|
+
export async function useDuxtBreadcrumb(path: MaybeRefOrGetter<string>) {
|
|
13
|
+
// EVERY composable call happens before the await, deliberately. This is a
|
|
14
|
+
// plain async function, not a `<script setup>` block, so nothing restores the
|
|
15
|
+
// Nuxt instance after an await inside it — a call placed below one dies with
|
|
16
|
+
// E1001 and takes the whole page render with it. `useAsyncData`'s return is
|
|
17
|
+
// awaitable as well as reactive, which is what lets the fetch be started here
|
|
18
|
+
// and waited for at the end.
|
|
19
|
+
const navigation = useDuxtNavigation();
|
|
20
|
+
const { source } = useDuxtCollection();
|
|
21
|
+
const { section } = useDuxtSection(navigation.data);
|
|
22
|
+
|
|
23
|
+
const trail = computed<ContentNavigationItem[]>(() => {
|
|
24
|
+
const current = toValue(path);
|
|
25
|
+
|
|
26
|
+
// The trail starts at the section — the entry the reader clicked in the row
|
|
27
|
+
// above — and continues with what lies below the source's own prefix.
|
|
28
|
+
const below = trailBelowPrefix(
|
|
29
|
+
navigation.data.value ?? [],
|
|
30
|
+
current,
|
|
31
|
+
source.value?.prefix ?? ''
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const head = section.value?.to
|
|
35
|
+
? [
|
|
36
|
+
{
|
|
37
|
+
title: section.value.label,
|
|
38
|
+
path: section.value.to
|
|
39
|
+
} as ContentNavigationItem
|
|
40
|
+
]
|
|
41
|
+
: [];
|
|
42
|
+
|
|
43
|
+
// A section's index page is the section: both carry `/duxt/getting-started`,
|
|
44
|
+
// so the trail listed the same destination twice under two names — the
|
|
45
|
+
// config's label and the page's own title. Keep the first of each path.
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
|
|
48
|
+
return [...head, ...below].filter((item) => {
|
|
49
|
+
const itemPath = item.path ?? '';
|
|
50
|
+
if (seen.has(itemPath)) return false;
|
|
51
|
+
|
|
52
|
+
seen.add(itemPath);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await navigation;
|
|
58
|
+
|
|
59
|
+
return trail;
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A choice the reader made, remembered across pages.
|
|
3
|
+
*
|
|
4
|
+
* A cookie rather than localStorage, and the reason is the jump: storage is
|
|
5
|
+
* readable only in the browser, so the server always renders the default and
|
|
6
|
+
* the control visibly switches after hydration. Reading it earlier does not
|
|
7
|
+
* help — the server has already sent the wrong markup. A cookie travels with
|
|
8
|
+
* the request, so the first byte is already correct.
|
|
9
|
+
*
|
|
10
|
+
* Still privacy-clean: first-party, no identifier, no analytics, a value the
|
|
11
|
+
* reader set by clicking a tab. Under GDPR/ePrivacy that is a functional
|
|
12
|
+
* preference the reader asked for, not something needing consent — the same
|
|
13
|
+
* category as a language or theme choice. SameSite=Lax keeps it off cross-site
|
|
14
|
+
* requests, and it expires after a year.
|
|
15
|
+
*
|
|
16
|
+
* ONE PER KIND OF CONTROL, never one per instance: a reader who picked `pnpm`
|
|
17
|
+
* on the install page means it on every other page, and the same is true of
|
|
18
|
+
* `curl` over `fetch` and of the form over raw JSON. A component that stores
|
|
19
|
+
* its own answer per block asks the same question again three screens later.
|
|
20
|
+
*/
|
|
21
|
+
export function useDuxtChoice(name: string) {
|
|
22
|
+
return useCookie<string | undefined>(`duxt-${name}`, {
|
|
23
|
+
sameSite: 'lax',
|
|
24
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
25
|
+
// No `secure: true`: it would drop the cookie on a plain-HTTP preview.
|
|
26
|
+
default: () => undefined
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { sourcesForRoute } from '../../sources-resolve';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which collection serves the current route.
|
|
5
|
+
*
|
|
6
|
+
* `duxtSources` generates one collection per source, version and language,
|
|
7
|
+
* named after what it serves. Every query in the theme has to ask the right
|
|
8
|
+
* one, or a site with two sources shows nothing at all — the collection a
|
|
9
|
+
* single source produces is called `docs`, and the ones a versioned or
|
|
10
|
+
* translated site produces are not.
|
|
11
|
+
*
|
|
12
|
+
* The choice itself is `sourcesForRoute`, pure and tested beside the resolver,
|
|
13
|
+
* so the devtools path debugger answers with the same chain rather than a
|
|
14
|
+
* second implementation of it. This composable is the reactive wrapper: the
|
|
15
|
+
* route, the locale, and vue-i18n's own fallback — used so the interface and
|
|
16
|
+
* the pages agree on where a missing translation resolves to.
|
|
17
|
+
*
|
|
18
|
+
* The manifest reaches the app through `app.config.ts`, because Content loads
|
|
19
|
+
* `content.config.ts` in its own pass and the app never sees the result. Both
|
|
20
|
+
* come from one call site; see the Sources reference.
|
|
21
|
+
*/
|
|
22
|
+
export function useDuxtCollection() {
|
|
23
|
+
const duxt = useDuxtConfig();
|
|
24
|
+
const path = useDuxtPath();
|
|
25
|
+
const { locale, fallbackLocale } = useI18n();
|
|
26
|
+
|
|
27
|
+
const sources = computed(() => duxt.resolvedSources ?? []);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The collections to try, best first.
|
|
31
|
+
*
|
|
32
|
+
* One entry on a site without translations, which is every site that does
|
|
33
|
+
* not set `locales` — and then this composable behaves exactly as it did
|
|
34
|
+
* before the key existed.
|
|
35
|
+
*/
|
|
36
|
+
const chain = computed(() =>
|
|
37
|
+
sourcesForRoute(
|
|
38
|
+
path.value,
|
|
39
|
+
locale.value,
|
|
40
|
+
sources.value,
|
|
41
|
+
fallbackLocale.value as string | string[] | undefined
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const current = computed(() => chain.value[0]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The default-language entry for this prefix.
|
|
49
|
+
*
|
|
50
|
+
* The one collection guaranteed to hold every page, which is what a
|
|
51
|
+
* navigation tree has to be built from — see `overlayTranslations`.
|
|
52
|
+
*/
|
|
53
|
+
const base = computed(
|
|
54
|
+
() =>
|
|
55
|
+
sources.value.find(
|
|
56
|
+
(source) =>
|
|
57
|
+
source.prefix === current.value?.prefix && source.isDefaultLocale
|
|
58
|
+
) ?? current.value
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** Cast because the name is data: Content types collections from the config. */
|
|
62
|
+
const name = computed(
|
|
63
|
+
() => (current.value?.collection ?? 'docs') as DuxtCollectionName
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
collection: name,
|
|
68
|
+
/** The complete tree's collection — the default language for this prefix. */
|
|
69
|
+
baseCollection: computed(
|
|
70
|
+
() => (base.value?.collection ?? 'docs') as DuxtCollectionName
|
|
71
|
+
),
|
|
72
|
+
source: current,
|
|
73
|
+
/** The collections a missing page falls back to, after the first. */
|
|
74
|
+
fallbacks: computed(() => chain.value.slice(1)),
|
|
75
|
+
/**
|
|
76
|
+
* Whether this site declares translations at all.
|
|
77
|
+
*
|
|
78
|
+
* Without it the fallback banner cannot be told apart from the normal
|
|
79
|
+
* state of a single-language site: there, every locale but the default one
|
|
80
|
+
* is served the same pages by design, and announcing that as a missing
|
|
81
|
+
* translation would put a notice on six of seven languages of every duxt
|
|
82
|
+
* site that never asked for any of this.
|
|
83
|
+
*/
|
|
84
|
+
translates: computed(() =>
|
|
85
|
+
sources.value.some((source) => source.locale && !source.isDefaultLocale)
|
|
86
|
+
),
|
|
87
|
+
sources: computed(() => sources.value)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { toReactive } from '@vueuse/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The layer's config, with the consumer's app.config merged over it and every
|
|
5
|
+
* text field resolved for the current locale.
|
|
6
|
+
*
|
|
7
|
+
* Two things happen here that a component should never have to know about:
|
|
8
|
+
*
|
|
9
|
+
* - Nuxt's own app.config merge APPENDS arrays instead of replacing them, so
|
|
10
|
+
* a consumer overriding `navigation` would get the layer's entries after
|
|
11
|
+
* its own — see `duxt-config.ts`.
|
|
12
|
+
* - a label may be a literal, an i18n key or a per-locale record, and
|
|
13
|
+
* `resolveDuxtTexts` collapses all three to a string — see `duxt-text.ts`.
|
|
14
|
+
* - a generated section's navbar entry is not in the config a human wrote at
|
|
15
|
+
* all; it follows from the resolved manifest AND from where the reader
|
|
16
|
+
* currently is — see `withGeneratedSections`.
|
|
17
|
+
*
|
|
18
|
+
* Resolving here rather than at each call site is the point: `DuxtHeader`,
|
|
19
|
+
* `DuxtFooter` and the rest keep writing `{{ section.label }}` and none of them
|
|
20
|
+
* imports i18n. `toReactive` keeps the result live, so switching language
|
|
21
|
+
* updates the navbar without a full page load.
|
|
22
|
+
*/
|
|
23
|
+
export function useDuxtConfig(): DuxtConfigResolved {
|
|
24
|
+
const appConfig = useAppConfig();
|
|
25
|
+
const { t, te, locale } = useI18n();
|
|
26
|
+
// The reader's position, because a section resolved per version has one
|
|
27
|
+
// entry and it is the one for the version being read.
|
|
28
|
+
const path = useDuxtPath();
|
|
29
|
+
|
|
30
|
+
return toReactive(
|
|
31
|
+
computed(() =>
|
|
32
|
+
resolveDuxtTexts(
|
|
33
|
+
// After the merge, because a generated section's entry is APPENDED to
|
|
34
|
+
// whatever list the consumer wrote — see `withGeneratedSections`.
|
|
35
|
+
withGeneratedSections(
|
|
36
|
+
mergeDuxtConfig(appConfig.duxt, duxtDefaults),
|
|
37
|
+
path.value
|
|
38
|
+
),
|
|
39
|
+
locale.value,
|
|
40
|
+
// `te` first, so a literal never reaches `t` and never produces a
|
|
41
|
+
// "key not found" warning. duxt's OWN missing keys still warn, which a
|
|
42
|
+
// global `missingWarn: false` would have silenced along with them.
|
|
43
|
+
(key) => (te(key) ? t(key) : undefined)
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
) as DuxtConfigResolved;
|
|
47
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from '@nuxt/content';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which collections the navigation is being fetched from.
|
|
5
|
+
*
|
|
6
|
+
* `useState`, and neither a module-level ref nor a closure, because the two
|
|
7
|
+
* obvious options each break one half of this:
|
|
8
|
+
*
|
|
9
|
+
* - A MODULE-LEVEL ref is one variable shared by every request the server has
|
|
10
|
+
* in flight. With two of them and two awaits in the handler below, one
|
|
11
|
+
* request could read what another had just written — the server then
|
|
12
|
+
* rendered one collection's navigation and the browser another, and the
|
|
13
|
+
* hydration mismatch took the page down with it.
|
|
14
|
+
* - A CLOSURE is a new function on every call, and Nuxt compares handlers by
|
|
15
|
+
* reference: several components asking for the same key with a handler each
|
|
16
|
+
* is NUXT_E3004, after which the first handler wins and the rest are
|
|
17
|
+
* silently ignored.
|
|
18
|
+
*
|
|
19
|
+
* `useState` is per request and reachable from a handler that stays one stable
|
|
20
|
+
* function, which is what both halves need.
|
|
21
|
+
*/
|
|
22
|
+
const useNavigationSource = () =>
|
|
23
|
+
useState('duxt-navigation-source', () => ({
|
|
24
|
+
base: 'docs' as DuxtCollectionName,
|
|
25
|
+
translation: 'docs' as DuxtCollectionName
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The tree of the default language, wearing the translated titles.
|
|
30
|
+
*
|
|
31
|
+
* Never the translation's own tree: a partly translated documentation would
|
|
32
|
+
* then lose every page it has not reached yet — pages the fallback serves
|
|
33
|
+
* perfectly well, so hiding them from the sidebar leaves the reader with no way
|
|
34
|
+
* to them at all. Structure from the original, wording from the translation;
|
|
35
|
+
* see `overlayTranslations`.
|
|
36
|
+
*/
|
|
37
|
+
const handler = async (): Promise<ContentNavigationItem[]> => {
|
|
38
|
+
// Read BEFORE the first await: after it, the route may have moved on.
|
|
39
|
+
const { base, translation } = useNavigationSource().value;
|
|
40
|
+
|
|
41
|
+
const tree = await queryCollectionNavigation(base as DuxtCollectionArg, [
|
|
42
|
+
'icon',
|
|
43
|
+
'description'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Before the overlay: a folder's title comes from its own index page, and a
|
|
47
|
+
// translated index has to be able to carry that up with it.
|
|
48
|
+
const named = titleFoldersFromIndex(tree);
|
|
49
|
+
|
|
50
|
+
if (translation === base) return named;
|
|
51
|
+
|
|
52
|
+
const translated = await queryCollection(translation as DuxtCollectionArg)
|
|
53
|
+
.select('path', 'title', 'description')
|
|
54
|
+
.all();
|
|
55
|
+
|
|
56
|
+
return titleFoldersFromIndex(
|
|
57
|
+
overlayTranslations(
|
|
58
|
+
named,
|
|
59
|
+
new Map(translated.map((page) => [page.path, page]))
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The navigation tree for the collection serving this route.
|
|
66
|
+
*
|
|
67
|
+
* The key tracks both collections: two sources are two trees and must not share
|
|
68
|
+
* one cache entry, and neither may two languages of one.
|
|
69
|
+
*/
|
|
70
|
+
export function useDuxtNavigation() {
|
|
71
|
+
const { collection, baseCollection } = useDuxtCollection();
|
|
72
|
+
const source = useNavigationSource();
|
|
73
|
+
|
|
74
|
+
// Kept in step before every fetch, including the ones `watch` triggers.
|
|
75
|
+
watchEffect(() => {
|
|
76
|
+
source.value = {
|
|
77
|
+
base: baseCollection.value,
|
|
78
|
+
translation: collection.value
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return useAsyncData<ContentNavigationItem[]>(
|
|
83
|
+
() => `duxt-navigation-${baseCollection.value}-${collection.value}`,
|
|
84
|
+
handler,
|
|
85
|
+
{ watch: [collection, baseCollection] }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move focus to the page's own heading after a client-side navigation.
|
|
3
|
+
*
|
|
4
|
+
* Vue Router replaces the view without touching focus, so after a sidebar click
|
|
5
|
+
* the focus is still on the link that was left behind: a keyboard reader's next
|
|
6
|
+
* Tab continues down the sidebar, and a screen reader is told nothing at all
|
|
7
|
+
* about the page that just arrived. Focusing the `<h1>` puts both back at the
|
|
8
|
+
* top of the new document.
|
|
9
|
+
*
|
|
10
|
+
* Not on the first paint. On a full load the focus is already at the start of
|
|
11
|
+
* the document, and taking it would scroll past the header before the reader
|
|
12
|
+
* has seen the page — so only a route CHANGE moves it.
|
|
13
|
+
*
|
|
14
|
+
* The announcement is a separate job, done by `<NuxtRouteAnnouncer>` in
|
|
15
|
+
* `app.vue`, which reads the new title into a live region.
|
|
16
|
+
*/
|
|
17
|
+
export function useDuxtPageFocus() {
|
|
18
|
+
const heading = ref<HTMLElement | null>(null);
|
|
19
|
+
const route = useRoute();
|
|
20
|
+
|
|
21
|
+
watch(
|
|
22
|
+
() => route.fullPath,
|
|
23
|
+
async () => {
|
|
24
|
+
await nextTick();
|
|
25
|
+
heading.value?.focus();
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return heading;
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path the DOCUMENTATION is at, as opposed to the one the browser shows.
|
|
3
|
+
*
|
|
4
|
+
* The two differ by the locale segment. Read `stripLocalePrefix` for why every
|
|
5
|
+
* content lookup wants this one rather than `route.path`.
|
|
6
|
+
*/
|
|
7
|
+
export function useDuxtPath() {
|
|
8
|
+
const route = useRoute();
|
|
9
|
+
const { locales } = useI18n();
|
|
10
|
+
|
|
11
|
+
const codes = computed(() =>
|
|
12
|
+
locales.value.map((entry) =>
|
|
13
|
+
typeof entry === 'string' ? entry : entry.code
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return computed(() => stripLocalePrefix(route.path, codes.value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The counterpart: a documentation path, written back out as a link the
|
|
22
|
+
* current locale can be reached at.
|
|
23
|
+
*
|
|
24
|
+
* Only paths are localised. An absolute URL, a mail link or a bare hash passes
|
|
25
|
+
* through untouched, so the same helper serves the navbar's external entries
|
|
26
|
+
* and the sidebar's internal ones without either side special-casing the other.
|
|
27
|
+
*/
|
|
28
|
+
export function useDuxtLink() {
|
|
29
|
+
const localePath = useLocalePath();
|
|
30
|
+
|
|
31
|
+
return (to?: string) => {
|
|
32
|
+
if (!to?.startsWith('/')) return to;
|
|
33
|
+
|
|
34
|
+
// localePath resolves a route, and a trailing anchor is not part of one.
|
|
35
|
+
// Split it off and put it back, so a sidebar entry pointing at a heading
|
|
36
|
+
// keeps pointing at that heading.
|
|
37
|
+
const [path, hash] = to.split('#');
|
|
38
|
+
const localised = localePath(path!);
|
|
39
|
+
|
|
40
|
+
return hash ? `${localised}#${hash}` : localised;
|
|
41
|
+
};
|
|
42
|
+
}
|