@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,271 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Two roots — the trigger and the dialog — so Vue cannot decide which one
|
|
3
|
+
// inherits a class from the parent and drops it with a warning. The trigger
|
|
4
|
+
// takes them explicitly.
|
|
5
|
+
defineOptions({ inheritAttrs: false });
|
|
6
|
+
|
|
7
|
+
import type { DuxtSearchHit } from '@duxt/composables/useDuxtSearch';
|
|
8
|
+
|
|
9
|
+
// Full-text search over EVERY source, not just the one being read. Content
|
|
10
|
+
// builds the index at build time and queries it with SQLite's FTS, so the
|
|
11
|
+
// ranking is the database's rather than a substring match of ours — and there
|
|
12
|
+
// is no search service to run. The merge across sources, and why the results
|
|
13
|
+
// are ranked together rather than grouped per repository, is in
|
|
14
|
+
// `useDuxtSearch`.
|
|
15
|
+
//
|
|
16
|
+
// The index is fetched on first open, not shipped with every page: a docs site
|
|
17
|
+
// should not pay for search on a page nobody searches from.
|
|
18
|
+
const open = ref(false);
|
|
19
|
+
const query = ref('');
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const localeLink = useDuxtLink();
|
|
22
|
+
|
|
23
|
+
const { search, init, labelled } = useDuxtSearch();
|
|
24
|
+
|
|
25
|
+
const results = ref<DuxtSearchHit[]>([]);
|
|
26
|
+
|
|
27
|
+
/** True while the list shows near-misses rather than actual matches. */
|
|
28
|
+
const approximate = ref(false);
|
|
29
|
+
|
|
30
|
+
let pending: ReturnType<typeof setTimeout> | undefined;
|
|
31
|
+
|
|
32
|
+
// Two awaits per keystroke, so a slow answer could land after a newer one and
|
|
33
|
+
// overwrite it. Each run takes a number and drops its result if the term moved
|
|
34
|
+
// on in the meantime.
|
|
35
|
+
let run = 0;
|
|
36
|
+
|
|
37
|
+
// Debounced by hand rather than through VueUse: one timer is not worth another
|
|
38
|
+
// dependency in a layer a consumer installs.
|
|
39
|
+
watch(query, (term) => {
|
|
40
|
+
clearTimeout(pending);
|
|
41
|
+
pending = setTimeout(async () => {
|
|
42
|
+
const current = ++run;
|
|
43
|
+
|
|
44
|
+
const settle = (hits: DuxtSearchHit[], fuzzy: boolean) => {
|
|
45
|
+
if (current !== run) return;
|
|
46
|
+
results.value = hits;
|
|
47
|
+
approximate.value = fuzzy;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!term.trim()) return settle([], false);
|
|
51
|
+
|
|
52
|
+
const found = await search(term, 20);
|
|
53
|
+
settle(found.hits, found.approximate);
|
|
54
|
+
}, 120);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const duxt = useDuxtConfig();
|
|
58
|
+
const { recent, load } = useRecentPages();
|
|
59
|
+
|
|
60
|
+
/** Which section a path belongs to, for grouping the hits. */
|
|
61
|
+
function sectionOf(path: string) {
|
|
62
|
+
return (
|
|
63
|
+
duxt.sections?.find((section) => section.to && path.startsWith(section.to))
|
|
64
|
+
?.label ?? 'Documentation'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hits grouped by section rather than by page. Grouping by page produced one
|
|
70
|
+
* heading per result; a section groups many, which is what a heading is for —
|
|
71
|
+
* and the reader already thinks in sections, because the navbar shows them.
|
|
72
|
+
*/
|
|
73
|
+
const grouped = computed(() => {
|
|
74
|
+
// With several sources the list is ONE ranked list with a badge per hit —
|
|
75
|
+
// grouping it by section would re-sort exactly the ranking the merge just
|
|
76
|
+
// produced. See `useDuxtSearch` for why ranking beats grouping here.
|
|
77
|
+
if (labelled.value) {
|
|
78
|
+
return [{ label: '', hits: results.value }];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const bySection = new Map<string, DuxtSearchHit[]>();
|
|
82
|
+
|
|
83
|
+
for (const hit of results.value) {
|
|
84
|
+
// A section label may be configured per locale; the group key has to be a
|
|
85
|
+
// plain string, and by this point useDuxtConfig has resolved it.
|
|
86
|
+
const label = asText(sectionOf(hit.id)) ?? 'Documentation';
|
|
87
|
+
bySection.set(label, [...(bySection.get(label) ?? []), hit]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [...bySection.entries()].map(([label, hits]) => ({ label, hits }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
async function show() {
|
|
94
|
+
load();
|
|
95
|
+
open.value = true;
|
|
96
|
+
await init();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function go(id: string) {
|
|
100
|
+
open.value = false;
|
|
101
|
+
query.value = '';
|
|
102
|
+
results.value = [];
|
|
103
|
+
approximate.value = false;
|
|
104
|
+
router.push(localeLink(id)!);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Where a hit sits: the page, and the headings above it inside that page. */
|
|
108
|
+
function context(result: DuxtSearchSection) {
|
|
109
|
+
const page = result.titles[0];
|
|
110
|
+
const between = result.titles.slice(1);
|
|
111
|
+
|
|
112
|
+
return [page, ...between].filter(Boolean).join(' › ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
const handler = (event: KeyboardEvent) => {
|
|
117
|
+
if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
|
|
120
|
+
if (open.value) {
|
|
121
|
+
open.value = false;
|
|
122
|
+
} else {
|
|
123
|
+
void show();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
window.addEventListener('keydown', handler);
|
|
129
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', handler));
|
|
130
|
+
});
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<template>
|
|
134
|
+
<UiButton
|
|
135
|
+
v-bind="$attrs"
|
|
136
|
+
variant="outline"
|
|
137
|
+
size="sm"
|
|
138
|
+
class="w-full justify-start gap-2 text-muted-foreground"
|
|
139
|
+
@click="show"
|
|
140
|
+
>
|
|
141
|
+
<Icon name="lucide:search" class="size-4" />
|
|
142
|
+
<span class="text-sm">{{ $t('duxt.search.label') }}</span>
|
|
143
|
+
<kbd
|
|
144
|
+
class="ml-auto hidden rounded border bg-muted px-1.5 font-mono text-[10px] sm:inline-block"
|
|
145
|
+
>
|
|
146
|
+
⌘K
|
|
147
|
+
</kbd>
|
|
148
|
+
</UiButton>
|
|
149
|
+
|
|
150
|
+
<UiCommandDialog v-model:open="open">
|
|
151
|
+
<!-- The input is ours, not CommandInput: that one writes into Command's own
|
|
152
|
+
filterState, and Command would then score the list a second time
|
|
153
|
+
against rendered text — dropping entries that mounted after the term
|
|
154
|
+
changed. Leaving its state empty keeps one filter in charge, the
|
|
155
|
+
database's. It also means CommandEmpty never renders, so the empty
|
|
156
|
+
state is ours too.
|
|
157
|
+
|
|
158
|
+
No clear button: DialogContent already draws a close X, and two of
|
|
159
|
+
them in the same corner is one too many. -->
|
|
160
|
+
<div class="flex items-center gap-2 border-b px-3">
|
|
161
|
+
<Icon
|
|
162
|
+
name="lucide:search"
|
|
163
|
+
class="size-4 shrink-0 text-muted-foreground"
|
|
164
|
+
/>
|
|
165
|
+
<input
|
|
166
|
+
v-model="query"
|
|
167
|
+
class="flex h-11 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
168
|
+
:placeholder="$t('duxt.search.placeholder')"
|
|
169
|
+
autofocus
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<UiCommandList class="max-h-[60vh]">
|
|
174
|
+
<!-- An empty box is a dead end. Without a term the dialog offers the
|
|
175
|
+
sections as entry points and, once there is any history, the pages
|
|
176
|
+
this reader came from — which is what they are most likely after. -->
|
|
177
|
+
<template v-if="!query.trim()">
|
|
178
|
+
<UiCommandGroup
|
|
179
|
+
v-if="recent.length"
|
|
180
|
+
:heading="$t('duxt.search.recent')"
|
|
181
|
+
>
|
|
182
|
+
<UiCommandItem
|
|
183
|
+
v-for="page in recent"
|
|
184
|
+
:key="page.path"
|
|
185
|
+
:value="`recent ${page.path}`"
|
|
186
|
+
class="gap-2"
|
|
187
|
+
@select="go(page.path)"
|
|
188
|
+
>
|
|
189
|
+
<Icon
|
|
190
|
+
name="lucide:history"
|
|
191
|
+
class="size-3.5 shrink-0 text-muted-foreground"
|
|
192
|
+
/>
|
|
193
|
+
<span class="truncate">{{ page.title }}</span>
|
|
194
|
+
<span class="ml-auto truncate pl-3 text-xs text-muted-foreground">
|
|
195
|
+
{{ sectionOf(page.path) }}
|
|
196
|
+
</span>
|
|
197
|
+
</UiCommandItem>
|
|
198
|
+
</UiCommandGroup>
|
|
199
|
+
|
|
200
|
+
<UiCommandGroup :heading="$t('duxt.search.sections')">
|
|
201
|
+
<UiCommandItem
|
|
202
|
+
v-for="section in duxt.sections ?? []"
|
|
203
|
+
:key="section.to"
|
|
204
|
+
:value="`section ${section.label}`"
|
|
205
|
+
class="gap-2"
|
|
206
|
+
@select="go(section.to ?? '/')"
|
|
207
|
+
>
|
|
208
|
+
<Icon
|
|
209
|
+
:name="section.icon ?? 'lucide:book-open'"
|
|
210
|
+
class="size-3.5 shrink-0 text-muted-foreground"
|
|
211
|
+
/>
|
|
212
|
+
<span class="truncate">{{ section.label }}</span>
|
|
213
|
+
</UiCommandItem>
|
|
214
|
+
</UiCommandGroup>
|
|
215
|
+
</template>
|
|
216
|
+
|
|
217
|
+
<div
|
|
218
|
+
v-else-if="!results.length"
|
|
219
|
+
class="flex flex-col items-center gap-2 py-12 text-sm text-muted-foreground"
|
|
220
|
+
>
|
|
221
|
+
<Icon name="lucide:search-x" class="size-5 opacity-60" />
|
|
222
|
+
{{ $t('duxt.search.empty', { query }) }}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Say so when the exact search came up empty, otherwise a near-miss
|
|
226
|
+
reads as a match and the reader wonders why their term is missing
|
|
227
|
+
from the result. -->
|
|
228
|
+
<div
|
|
229
|
+
v-if="approximate"
|
|
230
|
+
class="flex items-center gap-2 px-3 pt-3 pb-1 text-xs text-muted-foreground"
|
|
231
|
+
>
|
|
232
|
+
<Icon name="lucide:sparkles" class="size-3.5 shrink-0" />
|
|
233
|
+
{{ $t('duxt.search.approximate', { query }) }}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- One line per hit, grouped by section: the page is context on the
|
|
237
|
+
right, not a heading of its own. -->
|
|
238
|
+
<UiCommandGroup
|
|
239
|
+
v-for="group in grouped"
|
|
240
|
+
:key="group.label"
|
|
241
|
+
:heading="group.label || undefined"
|
|
242
|
+
>
|
|
243
|
+
<UiCommandItem
|
|
244
|
+
v-for="hit in group.hits"
|
|
245
|
+
:key="hit.id"
|
|
246
|
+
:value="hit.id"
|
|
247
|
+
class="gap-2"
|
|
248
|
+
@select="go(hit.id)"
|
|
249
|
+
>
|
|
250
|
+
<Icon
|
|
251
|
+
:name="hit.level > 1 ? 'lucide:hash' : 'lucide:file-text'"
|
|
252
|
+
class="size-3.5 shrink-0 text-muted-foreground"
|
|
253
|
+
/>
|
|
254
|
+
<span class="truncate">{{ hit.title }}</span>
|
|
255
|
+
<span class="ml-auto truncate pl-3 text-xs text-muted-foreground">
|
|
256
|
+
{{ context(hit) }}
|
|
257
|
+
</span>
|
|
258
|
+
<!-- Which repository and version this came out of. Only drawn where
|
|
259
|
+
there is more than one, so a single-source site sees nothing. -->
|
|
260
|
+
<UiBadge
|
|
261
|
+
v-if="hit.source"
|
|
262
|
+
variant="secondary"
|
|
263
|
+
class="shrink-0 font-mono text-[10px]"
|
|
264
|
+
>
|
|
265
|
+
{{ hit.source.label }}
|
|
266
|
+
</UiBadge>
|
|
267
|
+
</UiCommandItem>
|
|
268
|
+
</UiCommandGroup>
|
|
269
|
+
</UiCommandList>
|
|
270
|
+
</UiCommandDialog>
|
|
271
|
+
</template>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The second navbar row: top-level sections of the documentation, each an
|
|
3
|
+
// entry point into a part of the tree. nuxt.com's docs read this way, and it
|
|
4
|
+
// keeps the sidebar showing one section instead of everything at once.
|
|
5
|
+
const path = useDuxtPath();
|
|
6
|
+
const localeLink = useDuxtLink();
|
|
7
|
+
|
|
8
|
+
// Whether the row shows at all is shared with the layouts, which offset their
|
|
9
|
+
// sticky columns by the header this row is part of — see `useDuxtSectionRow`.
|
|
10
|
+
const { sections, visible } = useDuxtSectionRow();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ONE entry is lit, never two — see `currentSection`. An area whose parts nest
|
|
14
|
+
* (`/demo` and `/demo/api`) had both chips marked, and a row that says the
|
|
15
|
+
* reader is in two places at once says nothing.
|
|
16
|
+
*/
|
|
17
|
+
const active = computed(() => currentSection(sections.value, path.value));
|
|
18
|
+
|
|
19
|
+
function isActive(to?: string) {
|
|
20
|
+
return Boolean(to && to === active.value?.to);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A section is rarely the page itself — it is the branch the page sits in. So
|
|
25
|
+
* the exact match announces `page` and an ancestor announces `true`, which is
|
|
26
|
+
* the distinction `aria-current` exists to make.
|
|
27
|
+
*/
|
|
28
|
+
function current(to?: string) {
|
|
29
|
+
if (!isActive(to)) return undefined;
|
|
30
|
+
return path.value === to ? 'page' : 'true';
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<!-- Hidden below lg: the mobile sheet lists the same sections, and a row
|
|
36
|
+
that scrolls sideways under the header is worse than no row. -->
|
|
37
|
+
<!-- `top-14` is one pixel short: the header is h-14 PLUS its own border-b,
|
|
38
|
+
so the row parked over that border and the two swapped places by a pixel
|
|
39
|
+
as the browser rounded the scroll offset. -->
|
|
40
|
+
<div
|
|
41
|
+
v-if="visible"
|
|
42
|
+
class="sticky top-[calc(3.5rem_+_1px)] z-40 hidden border-b bg-background/95 backdrop-blur-md lg:block"
|
|
43
|
+
>
|
|
44
|
+
<nav
|
|
45
|
+
class="mx-auto flex max-w-[90rem] items-center gap-1 overflow-x-auto px-4 lg:px-8"
|
|
46
|
+
:aria-label="$t('duxt.nav.sections')"
|
|
47
|
+
>
|
|
48
|
+
<NuxtLink
|
|
49
|
+
v-for="section in sections"
|
|
50
|
+
:key="section.to"
|
|
51
|
+
:to="localeLink(section.to)"
|
|
52
|
+
:aria-current="current(section.to)"
|
|
53
|
+
class="my-1.5 flex shrink-0 items-center gap-2 rounded-md px-3 py-1.5 text-sm transition-colors"
|
|
54
|
+
:class="
|
|
55
|
+
isActive(section.to)
|
|
56
|
+
? 'bg-accent font-medium text-foreground'
|
|
57
|
+
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground'
|
|
58
|
+
"
|
|
59
|
+
>
|
|
60
|
+
<Icon v-if="section.icon" :name="section.icon" class="size-4" />
|
|
61
|
+
{{ section.label }}
|
|
62
|
+
</NuxtLink>
|
|
63
|
+
</nav>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The shortcut sheet, on `?`.
|
|
4
|
+
*
|
|
5
|
+
* Its list is `duxtShortcuts`, the same array the handlers match against, so
|
|
6
|
+
* the sheet cannot come to advertise a key nothing binds.
|
|
7
|
+
*/
|
|
8
|
+
const open = ref(false);
|
|
9
|
+
|
|
10
|
+
onDuxtShortcut(
|
|
11
|
+
(event) => event.key === '?' && !event.metaKey && !event.ctrlKey,
|
|
12
|
+
() => (open.value = !open.value)
|
|
13
|
+
);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<UiDialog v-model:open="open">
|
|
18
|
+
<UiDialogContent class="sm:max-w-sm">
|
|
19
|
+
<UiDialogHeader>
|
|
20
|
+
<UiDialogTitle>{{ $t('duxt.shortcuts.title') }}</UiDialogTitle>
|
|
21
|
+
<UiDialogDescription class="sr-only">
|
|
22
|
+
{{ $t('duxt.shortcuts.title') }}
|
|
23
|
+
</UiDialogDescription>
|
|
24
|
+
</UiDialogHeader>
|
|
25
|
+
|
|
26
|
+
<ul class="flex flex-col gap-2 text-sm">
|
|
27
|
+
<li
|
|
28
|
+
v-for="shortcut in duxtShortcuts"
|
|
29
|
+
:key="shortcut.label"
|
|
30
|
+
class="flex items-center justify-between gap-4"
|
|
31
|
+
>
|
|
32
|
+
<span>{{ $t(shortcut.label) }}</span>
|
|
33
|
+
<span class="flex gap-1">
|
|
34
|
+
<kbd
|
|
35
|
+
v-for="key in shortcut.keys"
|
|
36
|
+
:key="key"
|
|
37
|
+
class="rounded border bg-muted px-1.5 py-0.5 font-mono text-xs"
|
|
38
|
+
>
|
|
39
|
+
{{ key }}
|
|
40
|
+
</kbd>
|
|
41
|
+
</span>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</UiDialogContent>
|
|
45
|
+
</UiDialog>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The first focusable element on the page.
|
|
4
|
+
*
|
|
5
|
+
* Without it every page change means tabbing through the header, the section
|
|
6
|
+
* row and the whole sidebar before the first word of the text. Invisible until
|
|
7
|
+
* focused — `sr-only` collapses it to a single pixel, and the focus state puts
|
|
8
|
+
* it back on screen above everything else.
|
|
9
|
+
*
|
|
10
|
+
* The click is handled rather than left to the hash, because the browser only
|
|
11
|
+
* moves FOCUS to a fragment target that can hold it. Moving it here keeps the
|
|
12
|
+
* URL clean and works whether or not `<main>` carries a tabindex.
|
|
13
|
+
*/
|
|
14
|
+
const target = '#duxt-main';
|
|
15
|
+
|
|
16
|
+
function jump(event: MouseEvent) {
|
|
17
|
+
const main = document.querySelector<HTMLElement>(target);
|
|
18
|
+
if (!main) return;
|
|
19
|
+
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
main.focus();
|
|
22
|
+
main.scrollIntoView();
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<a
|
|
28
|
+
:href="target"
|
|
29
|
+
class="sr-only rounded-md bg-background px-4 py-2 text-sm font-medium ring-2 ring-ring focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-100"
|
|
30
|
+
@click="jump"
|
|
31
|
+
>
|
|
32
|
+
{{ $t('duxt.nav.skip') }}
|
|
33
|
+
</a>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface TocLink {
|
|
3
|
+
id: string;
|
|
4
|
+
text: string;
|
|
5
|
+
depth: number;
|
|
6
|
+
children?: TocLink[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{ links: TocLink[] }>();
|
|
10
|
+
|
|
11
|
+
const duxt = useDuxtConfig();
|
|
12
|
+
const localeLink = useDuxtLink();
|
|
13
|
+
|
|
14
|
+
// Flat list of every id, in document order, for the observer to watch.
|
|
15
|
+
const ids = computed(() =>
|
|
16
|
+
props.links.flatMap((link) => [
|
|
17
|
+
link.id,
|
|
18
|
+
...(link.children ?? []).map((child) => child.id)
|
|
19
|
+
])
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const active = useActiveHeading(ids);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div class="space-y-8 text-sm">
|
|
27
|
+
<nav v-if="links?.length" :aria-label="$t('duxt.toc.title')">
|
|
28
|
+
<p class="mb-3 font-medium">{{ $t('duxt.toc.title') }}</p>
|
|
29
|
+
<ul class="space-y-1 border-l">
|
|
30
|
+
<li v-for="link in links" :key="link.id">
|
|
31
|
+
<a
|
|
32
|
+
:href="`#${link.id}`"
|
|
33
|
+
class="-ml-px block border-l py-1 pl-4 transition-colors"
|
|
34
|
+
:class="
|
|
35
|
+
active === link.id
|
|
36
|
+
? 'border-primary text-foreground'
|
|
37
|
+
: 'border-transparent text-muted-foreground hover:border-primary/60 hover:text-foreground'
|
|
38
|
+
"
|
|
39
|
+
>
|
|
40
|
+
{{ link.text }}
|
|
41
|
+
</a>
|
|
42
|
+
<ul v-if="link.children?.length">
|
|
43
|
+
<li v-for="child in link.children" :key="child.id">
|
|
44
|
+
<a
|
|
45
|
+
:href="`#${child.id}`"
|
|
46
|
+
class="-ml-px block border-l py-1 pl-7 transition-colors"
|
|
47
|
+
:class="
|
|
48
|
+
active === child.id
|
|
49
|
+
? 'border-primary text-foreground'
|
|
50
|
+
: 'border-transparent text-muted-foreground hover:border-primary/60 hover:text-foreground'
|
|
51
|
+
"
|
|
52
|
+
>
|
|
53
|
+
{{ child.text }}
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</li>
|
|
58
|
+
</ul>
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<!-- The fixed link block that belongs on every page, configured once. -->
|
|
62
|
+
<nav
|
|
63
|
+
v-if="duxt.aside?.links?.length"
|
|
64
|
+
:aria-label="duxt.aside.title ?? 'Community'"
|
|
65
|
+
>
|
|
66
|
+
<p class="mb-3 font-medium">{{ duxt.aside.title ?? 'Community' }}</p>
|
|
67
|
+
<ul class="space-y-2">
|
|
68
|
+
<li v-for="link in duxt.aside.links" :key="link.to">
|
|
69
|
+
<NuxtLink
|
|
70
|
+
:to="localeLink(link.to)"
|
|
71
|
+
:target="link.external ? '_blank' : undefined"
|
|
72
|
+
class="flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
|
|
73
|
+
>
|
|
74
|
+
<Icon v-if="link.icon" :name="link.icon" class="size-4" />
|
|
75
|
+
{{ link.label }}
|
|
76
|
+
<Icon
|
|
77
|
+
v-if="link.external"
|
|
78
|
+
name="lucide:arrow-up-right"
|
|
79
|
+
class="size-3 opacity-50"
|
|
80
|
+
/>
|
|
81
|
+
</NuxtLink>
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</nav>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The visible half of a missing translation.
|
|
4
|
+
*
|
|
5
|
+
* `noindex` and the canonical in `[...slug].vue` tell a crawler that this page
|
|
6
|
+
* is not a German page; they tell the reader nothing, and the reader is the one
|
|
7
|
+
* looking at English text on a URL that promised their language. Silently
|
|
8
|
+
* serving the fallback is what Docusaurus and VitePress do, and it is why a
|
|
9
|
+
* stale or absent translation there is invisible to everyone except the person
|
|
10
|
+
* reading it.
|
|
11
|
+
*
|
|
12
|
+
* A 404 would be the other option and the worse one: it punishes a reader for a
|
|
13
|
+
* gap the writer left.
|
|
14
|
+
*/
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
/** The locale actually served; absent means the untranslated original. */
|
|
17
|
+
from?: string;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const { locales, defaultLocale } = useI18n();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The language's own name, not its code.
|
|
24
|
+
*
|
|
25
|
+
* `nuxt.config.ts` carries the endonym on every locale — each language written
|
|
26
|
+
* in itself — because that is what a language switcher must show, and it is
|
|
27
|
+
* what this sentence needs too.
|
|
28
|
+
*/
|
|
29
|
+
const languageOf = (code: string | undefined) =>
|
|
30
|
+
locales.value.find((entry) =>
|
|
31
|
+
typeof entry === 'string' ? entry === code : entry.code === code
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const shown = computed(() => {
|
|
35
|
+
const entry = languageOf(props.from ?? defaultLocale);
|
|
36
|
+
return typeof entry === 'string' ? entry : (entry?.name ?? entry?.code ?? '');
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<!-- Same shape as DuxtVersionBanner, deliberately: a reader learns what a
|
|
42
|
+
bordered notice above the title means once, and a second style for a
|
|
43
|
+
second kind of notice is one style too many. Sky, because this is
|
|
44
|
+
information about the page rather than a warning about its content. -->
|
|
45
|
+
<UiAlert
|
|
46
|
+
role="status"
|
|
47
|
+
class="mb-6 flex items-start gap-2.5 rounded-md border border-l-2 border-l-sky-500 bg-muted/30 px-3 py-2.5"
|
|
48
|
+
>
|
|
49
|
+
<Icon
|
|
50
|
+
name="lucide:languages"
|
|
51
|
+
class="mt-[3px] size-4 shrink-0 text-sky-500"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<div class="min-w-0 flex-1">
|
|
55
|
+
<UiAlertTitle class="mb-0.5 font-medium text-foreground">
|
|
56
|
+
{{ $t('duxt.page.untranslated.title') }}
|
|
57
|
+
</UiAlertTitle>
|
|
58
|
+
|
|
59
|
+
<UiAlertDescription class="text-muted-foreground">
|
|
60
|
+
{{ $t('duxt.page.untranslated.description', { language: shown }) }}
|
|
61
|
+
</UiAlertDescription>
|
|
62
|
+
</div>
|
|
63
|
+
</UiAlert>
|
|
64
|
+
</template>
|