@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,183 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Several fences, one block, one tab each.
|
|
6
|
+
*
|
|
7
|
+
* `::code-group` around a run of fenced blocks. The label of each tab is the
|
|
8
|
+
* fence's own filename — ```ts [nuxt.config.ts] — because that is what a reader
|
|
9
|
+
* is choosing between, and it is already written.
|
|
10
|
+
*
|
|
11
|
+
* The children are read from the default slot's VNodes rather than registered
|
|
12
|
+
* by a child component: a fence is a `ProsePre`, not something this block can
|
|
13
|
+
* ask to announce itself, and its filename is a prop on that VNode.
|
|
14
|
+
*
|
|
15
|
+
* NOT ONLY FOR FENCES. Anything rendering a `DuxtCodeBlock` and carrying a
|
|
16
|
+
* `filename` or `language` prop works here — the landing page's code demos hand
|
|
17
|
+
* it blocks built from config rather than from Markdown. That is why this is a
|
|
18
|
+
* component and the landing page does not grow a second tab bar of its own.
|
|
19
|
+
*
|
|
20
|
+
* THE BAR IS `PackageManagers`' BAR: pills with the file's own icon, and the
|
|
21
|
+
* copy button at the end of the same row. Those two components are the same
|
|
22
|
+
* control — pick one of several spellings of one thing, copy the one you
|
|
23
|
+
* picked — and they read as one only if they look alike.
|
|
24
|
+
*/
|
|
25
|
+
const slots = useSlots();
|
|
26
|
+
|
|
27
|
+
interface Entry {
|
|
28
|
+
value: string;
|
|
29
|
+
label: string;
|
|
30
|
+
icon: string;
|
|
31
|
+
code: string;
|
|
32
|
+
node: VNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entries = computed<Entry[]>(() => {
|
|
36
|
+
const nodes = (slots.default?.() ?? []).flatMap((node) =>
|
|
37
|
+
Array.isArray(node.children) && typeof node.type === 'symbol'
|
|
38
|
+
? (node.children as VNode[])
|
|
39
|
+
: [node]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return nodes
|
|
43
|
+
.filter((node) => typeof node.type === 'object')
|
|
44
|
+
.map((node, index) => {
|
|
45
|
+
const props = (node.props ?? {}) as {
|
|
46
|
+
filename?: string;
|
|
47
|
+
language?: string;
|
|
48
|
+
code?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
value: `code-${index}`,
|
|
53
|
+
label: props.filename ?? props.language ?? `${index + 1}`,
|
|
54
|
+
icon: fileIcon(
|
|
55
|
+
props.filename ?? props.language,
|
|
56
|
+
props.language ? 'lucide:terminal' : 'lucide:file'
|
|
57
|
+
),
|
|
58
|
+
code: props.code ?? '',
|
|
59
|
+
node
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const active = ref('code-0');
|
|
65
|
+
|
|
66
|
+
watchEffect(() => {
|
|
67
|
+
if (!entries.value.some((entry) => entry.value === active.value)) {
|
|
68
|
+
active.value = entries.value[0]?.value ?? 'code-0';
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const current = computed(() =>
|
|
73
|
+
entries.value.find((entry) => entry.value === active.value)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The copy button, on the group rather than on each fence.
|
|
78
|
+
*
|
|
79
|
+
* `code` is the fence's raw source and is the honest thing to copy; a group
|
|
80
|
+
* whose children were built without it falls back to the rendered text of the
|
|
81
|
+
* panel that is showing, which is what `DuxtCodeBlock` does for itself.
|
|
82
|
+
*/
|
|
83
|
+
const root = useTemplateRef<HTMLElement>('root');
|
|
84
|
+
|
|
85
|
+
/** The height follows the file that is showing — see the composable. */
|
|
86
|
+
const shell = useTemplateRef<HTMLElement>('shell');
|
|
87
|
+
const body = useTemplateRef<HTMLElement>('body');
|
|
88
|
+
|
|
89
|
+
useDuxtAnimatedHeight(shell, body);
|
|
90
|
+
const copied = ref(false);
|
|
91
|
+
const notify = useDuxtToast();
|
|
92
|
+
const { t } = useI18n();
|
|
93
|
+
|
|
94
|
+
async function copy() {
|
|
95
|
+
const text =
|
|
96
|
+
current.value?.code ||
|
|
97
|
+
root.value?.querySelector('[data-state="active"] code')?.textContent ||
|
|
98
|
+
'';
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await navigator.clipboard.writeText(text);
|
|
102
|
+
copied.value = true;
|
|
103
|
+
notify.success(t('duxt.code.copiedToast'));
|
|
104
|
+
setTimeout(() => (copied.value = false), 2000);
|
|
105
|
+
} catch {
|
|
106
|
+
notify.error(t('duxt.page.copyFailed'));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<TabsRoot
|
|
113
|
+
ref="root"
|
|
114
|
+
v-model="active"
|
|
115
|
+
class="duxt-code my-6 overflow-hidden rounded-lg border bg-card"
|
|
116
|
+
>
|
|
117
|
+
<!-- THE BUTTON IS NOT IN THE TABLIST. It looks like one row and has to be
|
|
118
|
+
one row, but `role="tablist"` may only contain tabs — a copy button
|
|
119
|
+
inside it is a control a screen reader is told to treat as a tab and
|
|
120
|
+
cannot. So the row is a plain box and the tablist is the part of it
|
|
121
|
+
that actually holds tabs. -->
|
|
122
|
+
<div
|
|
123
|
+
class="flex min-h-11 items-center gap-1 border-b bg-muted/40 px-2 py-1.5"
|
|
124
|
+
>
|
|
125
|
+
<TabsList
|
|
126
|
+
class="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto"
|
|
127
|
+
:aria-label="$t('duxt.page.tabs') as string"
|
|
128
|
+
>
|
|
129
|
+
<TabsTrigger
|
|
130
|
+
v-for="entry in entries"
|
|
131
|
+
:key="entry.value"
|
|
132
|
+
:value="entry.value"
|
|
133
|
+
class="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1 font-mono text-xs transition-colors data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=inactive]:text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
134
|
+
>
|
|
135
|
+
<Icon :name="entry.icon" class="size-3.5" />
|
|
136
|
+
{{ entry.label }}
|
|
137
|
+
</TabsTrigger>
|
|
138
|
+
</TabsList>
|
|
139
|
+
|
|
140
|
+
<UiButton
|
|
141
|
+
variant="ghost"
|
|
142
|
+
size="icon"
|
|
143
|
+
class="size-7 shrink-0"
|
|
144
|
+
:aria-label="copied ? $t('duxt.code.copied') : $t('duxt.code.copy')"
|
|
145
|
+
@click="copy"
|
|
146
|
+
>
|
|
147
|
+
<Icon
|
|
148
|
+
:name="copied ? 'lucide:check' : 'lucide:copy'"
|
|
149
|
+
class="size-3.5"
|
|
150
|
+
/>
|
|
151
|
+
</UiButton>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- THE BLOCK FOLLOWS THE FILE THAT IS SHOWING, over 300ms.
|
|
155
|
+
|
|
156
|
+
The first version mounted every panel stacked in one grid cell, so the
|
|
157
|
+
block was always as tall as the LONGEST file. It never moved, which was
|
|
158
|
+
the point — and it left a three-line config sitting in the space a
|
|
159
|
+
twelve-line one needs, which is a lot of empty box on a landing page.
|
|
160
|
+
|
|
161
|
+
So one panel at a time, and the height is animated instead of switched:
|
|
162
|
+
the same treatment the request-sample card gets, out of the same
|
|
163
|
+
composable. Nothing below the block jumps, and nothing above it sits in
|
|
164
|
+
a hole. -->
|
|
165
|
+
<div
|
|
166
|
+
ref="shell"
|
|
167
|
+
class="overflow-hidden motion-safe:transition-[height] motion-safe:duration-300 motion-safe:ease-out"
|
|
168
|
+
>
|
|
169
|
+
<div ref="body">
|
|
170
|
+
<TabsContent
|
|
171
|
+
v-for="entry in entries"
|
|
172
|
+
:key="entry.value"
|
|
173
|
+
:value="entry.value"
|
|
174
|
+
class="focus-visible:outline-none [&_.duxt-code]:my-0 [&_.duxt-code]:rounded-none [&_.duxt-code]:border-0"
|
|
175
|
+
>
|
|
176
|
+
<!-- `:header="false"`: the tab above already says what the bar inside
|
|
177
|
+
would, and the copy button now sits beside the tabs. -->
|
|
178
|
+
<component :is="entry.node" :header="false" />
|
|
179
|
+
</TabsContent>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</TabsRoot>
|
|
183
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* `::devtools-panel{tab="pages"}` — one panel of the devtools tab, embedded.
|
|
4
|
+
*
|
|
5
|
+
* The alternative was a screenshot, and a screenshot of a table is out of date
|
|
6
|
+
* the day a column is added; the panel's real route is no alternative at all,
|
|
7
|
+
* because `modules/devtools.ts` registers nothing outside a dev server, so a
|
|
8
|
+
* published page would frame a 404. What is embedded here is the panel's OWN
|
|
9
|
+
* renderer, run over a fixture site at build time and written to
|
|
10
|
+
* `public/devtools/<tab>.html` by `pnpm previews` — the LAYER's public
|
|
11
|
+
* directory, so the documents ship in the package beside the pages that ask
|
|
12
|
+
* for them. Nuxt serves every layer's `public/`, which is what lets a consumer
|
|
13
|
+
* rendering this reference get the frames rather than ten empty boxes.
|
|
14
|
+
*
|
|
15
|
+
* An iframe rather than inline markup, and deliberately: the panel ships its
|
|
16
|
+
* own stylesheet, written against a documentation site's tokens it knows
|
|
17
|
+
* nothing about. Inlining it would mean two cascades in one document, each
|
|
18
|
+
* quietly restyling the other.
|
|
19
|
+
*
|
|
20
|
+
* Two messages cross the frame. The theme goes in, because the reader's choice
|
|
21
|
+
* is a class on this page and a media query is all the frame would otherwise
|
|
22
|
+
* see; the height comes back, because only the framed document knows how tall
|
|
23
|
+
* its table turned out. Both are best-effort — with the script blocked, the
|
|
24
|
+
* frame keeps its default height and follows the system theme.
|
|
25
|
+
*
|
|
26
|
+
* `allow-scripts` WITHOUT `allow-same-origin`, and the pair is the point: on a
|
|
27
|
+
* document served from this site's own origin the two together hand the frame
|
|
28
|
+
* back everything the attribute withheld. Nothing here needs it — `postMessage`
|
|
29
|
+
* crosses origins by design, and the two messages above are all the framed page
|
|
30
|
+
* does. What is framed is generated HTML nobody reviews per release, so the
|
|
31
|
+
* sandbox is worth keeping teeth in.
|
|
32
|
+
*/
|
|
33
|
+
const props = withDefaults(
|
|
34
|
+
defineProps<{
|
|
35
|
+
/** The panel's slug, as the tab row spells it. */
|
|
36
|
+
tab?: string;
|
|
37
|
+
/** Starting height in pixels, before the frame reports its own. */
|
|
38
|
+
height?: number | string;
|
|
39
|
+
/** Upper bound, so a long table scrolls inside the frame. */
|
|
40
|
+
max?: number | string;
|
|
41
|
+
}>(),
|
|
42
|
+
{ tab: 'sources', height: 360, max: 620 }
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const frame = useTemplateRef<HTMLIFrameElement>('frame');
|
|
46
|
+
const colorMode = useColorMode();
|
|
47
|
+
const { t } = useI18n();
|
|
48
|
+
|
|
49
|
+
const number = (value: number | string, fallback: number) => {
|
|
50
|
+
const parsed = typeof value === 'number' ? value : Number.parseInt(value, 10);
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ceiling = computed(() => number(props.max, 620));
|
|
55
|
+
const height = ref(number(props.height, 360));
|
|
56
|
+
|
|
57
|
+
const dark = computed(() => colorMode.value === 'dark');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The theme is in the URL as well as in a message: the query settles the very
|
|
61
|
+
* first paint, the message every change after it. Without the query the frame
|
|
62
|
+
* renders light and flips a moment later, which is exactly the flash the
|
|
63
|
+
* theme-aware image support elsewhere in this layer exists to avoid.
|
|
64
|
+
*/
|
|
65
|
+
const src = computed(
|
|
66
|
+
() =>
|
|
67
|
+
`/devtools/${props.tab || 'sources'}.html?theme=${dark.value ? 'dark' : 'light'}`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
function tell() {
|
|
71
|
+
frame.value?.contentWindow?.postMessage(
|
|
72
|
+
{ duxtTheme: dark.value ? 'dark' : 'light' },
|
|
73
|
+
'*'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
watch(dark, tell);
|
|
78
|
+
|
|
79
|
+
function onMessage(event: MessageEvent) {
|
|
80
|
+
// Only this frame's own message, and only a number: the listener is on the
|
|
81
|
+
// window, which every other frame and script on the page can post to.
|
|
82
|
+
if (event.source !== frame.value?.contentWindow) return;
|
|
83
|
+
|
|
84
|
+
const reported = (event.data as { duxtHeight?: unknown } | null)?.duxtHeight;
|
|
85
|
+
if (typeof reported !== 'number') return;
|
|
86
|
+
|
|
87
|
+
height.value = Math.min(Math.max(Math.round(reported), 200), ceiling.value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onMounted(() => window.addEventListener('message', onMessage));
|
|
91
|
+
onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div class="border-border my-6 overflow-hidden rounded-lg border">
|
|
96
|
+
<iframe
|
|
97
|
+
ref="frame"
|
|
98
|
+
:src="src"
|
|
99
|
+
:title="t('duxt.devtools.preview', { tab })"
|
|
100
|
+
:style="{ height: `${height}px` }"
|
|
101
|
+
class="block w-full"
|
|
102
|
+
loading="lazy"
|
|
103
|
+
sandbox="allow-scripts"
|
|
104
|
+
@load="tell"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One parameter, property or option — the most-used component a docs theme is
|
|
4
|
+
* missing.
|
|
5
|
+
*
|
|
6
|
+
* ::field{name="showRepo" type="boolean" default="false"}
|
|
7
|
+
* Force a repository segment even with a single repository.
|
|
8
|
+
* ::
|
|
9
|
+
*
|
|
10
|
+
* A definition list would be the semantic choice and is the wrong one here: the
|
|
11
|
+
* name, the type, the default and whether it is required are four different
|
|
12
|
+
* things, and `<dt>` gives them one slot between them. So each is drawn as
|
|
13
|
+
* itself, and the description is the only prose.
|
|
14
|
+
*/
|
|
15
|
+
defineProps<{
|
|
16
|
+
name?: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
default?: string;
|
|
19
|
+
required?: boolean;
|
|
20
|
+
}>();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="border-b py-3 last:border-b-0">
|
|
25
|
+
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
26
|
+
<code class="font-mono text-sm font-medium text-foreground">{{
|
|
27
|
+
name
|
|
28
|
+
}}</code>
|
|
29
|
+
|
|
30
|
+
<code v-if="type" class="font-mono text-xs text-muted-foreground">{{
|
|
31
|
+
type
|
|
32
|
+
}}</code>
|
|
33
|
+
|
|
34
|
+
<UiBadge v-if="required" variant="secondary" class="text-[10px]">
|
|
35
|
+
required
|
|
36
|
+
</UiBadge>
|
|
37
|
+
|
|
38
|
+
<code
|
|
39
|
+
v-if="$props.default !== undefined"
|
|
40
|
+
class="ml-auto font-mono text-xs text-muted-foreground"
|
|
41
|
+
>
|
|
42
|
+
= {{ $props.default }}
|
|
43
|
+
</code>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="mt-1.5 text-sm text-muted-foreground [&>*:last-child]:mb-0">
|
|
47
|
+
<slot />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- `::field-group` around several `::field` blocks. One border round the
|
|
3
|
+
set, so a list of options reads as a table without being one — a table
|
|
4
|
+
cell cannot hold the paragraph a field's description usually is. -->
|
|
5
|
+
<div class="my-6 rounded-lg border px-4">
|
|
6
|
+
<slot />
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { TreeItem, TreeRoot } from 'reka-ui';
|
|
3
|
+
|
|
4
|
+
interface Entry {
|
|
5
|
+
name: string;
|
|
6
|
+
children?: Entry[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// `::file-tree` with a `tree` prop. Built on Reka UI's Tree rather than
|
|
10
|
+
// hand-rolled `<ul>` recursion, so it comes with roving focus, arrow-key
|
|
11
|
+
// navigation, expand and collapse, and the ARIA roles a tree needs — all of
|
|
12
|
+
// which the hand-rolled version had none of.
|
|
13
|
+
const props = defineProps<{ tree?: Entry[]; title?: string }>();
|
|
14
|
+
|
|
15
|
+
const items = computed(() => props.tree ?? []);
|
|
16
|
+
|
|
17
|
+
const isDirectory = (entry: Entry) =>
|
|
18
|
+
Boolean(entry.children?.length) || entry.name.endsWith('/');
|
|
19
|
+
|
|
20
|
+
const label = (entry: Entry) => entry.name.replace(/\/$/, '');
|
|
21
|
+
|
|
22
|
+
/** Unique per node: names repeat across branches, paths do not. */
|
|
23
|
+
function key(entry: Entry) {
|
|
24
|
+
return entry.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Everything open on first render — a docs tree is there to be read, not explored. */
|
|
28
|
+
const expanded = computed(() => {
|
|
29
|
+
const keys: string[] = [];
|
|
30
|
+
|
|
31
|
+
const walk = (entries: Entry[]) => {
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.children?.length) {
|
|
34
|
+
keys.push(key(entry));
|
|
35
|
+
walk(entry.children);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
walk(items.value);
|
|
41
|
+
return keys;
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="not-typeset my-6 overflow-hidden rounded-lg border bg-card">
|
|
47
|
+
<div
|
|
48
|
+
v-if="title"
|
|
49
|
+
class="flex min-h-11 items-center gap-2 border-b bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
|
|
50
|
+
>
|
|
51
|
+
<Icon name="lucide:folder-tree" class="size-4" />
|
|
52
|
+
<span class="font-mono">{{ title }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<TreeRoot
|
|
56
|
+
v-slot="{ flattenItems }"
|
|
57
|
+
:items="items"
|
|
58
|
+
:get-key="key"
|
|
59
|
+
:get-children="(entry: Entry) => entry.children"
|
|
60
|
+
:default-expanded="expanded"
|
|
61
|
+
class="overflow-x-auto p-3 font-mono text-[13px] leading-6 select-none"
|
|
62
|
+
>
|
|
63
|
+
<TreeItem
|
|
64
|
+
v-for="item in flattenItems"
|
|
65
|
+
v-slot="{ isExpanded }"
|
|
66
|
+
:key="item._id"
|
|
67
|
+
v-bind="item.bind"
|
|
68
|
+
:style="{ paddingLeft: `${item.level - 1}rem` }"
|
|
69
|
+
class="flex items-center gap-1.5 rounded px-1 py-[3px] outline-none focus:bg-accent data-[selected]:bg-accent/60"
|
|
70
|
+
>
|
|
71
|
+
<template v-if="isDirectory(item.value)">
|
|
72
|
+
<Icon
|
|
73
|
+
name="lucide:chevron-right"
|
|
74
|
+
class="size-3 shrink-0 text-muted-foreground transition-transform"
|
|
75
|
+
:class="{ 'rotate-90': isExpanded }"
|
|
76
|
+
/>
|
|
77
|
+
<Icon
|
|
78
|
+
:name="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"
|
|
79
|
+
class="size-4 shrink-0 text-muted-foreground/70"
|
|
80
|
+
/>
|
|
81
|
+
<span class="text-foreground">{{ label(item.value) }}</span>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<template v-else>
|
|
85
|
+
<span class="w-3 shrink-0" />
|
|
86
|
+
<Icon :name="fileIcon(item.value.name)" class="size-4 shrink-0" />
|
|
87
|
+
<span class="text-muted-foreground">{{ label(item.value) }}</span>
|
|
88
|
+
</template>
|
|
89
|
+
</TreeItem>
|
|
90
|
+
</TreeRoot>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A mermaid diagram, from ```mermaid or from `::mermaid`.
|
|
4
|
+
*
|
|
5
|
+
* Loaded on demand and only in the browser: mermaid is roughly half a megabyte
|
|
6
|
+
* and pulls in its own parser, and a documentation site where three pages in
|
|
7
|
+
* sixty carry a diagram must not make the other fifty-seven pay for it. That
|
|
8
|
+
* also means the diagram is client-rendered — the SSR output is the source
|
|
9
|
+
* text, which is the honest fallback for a reader with no JavaScript.
|
|
10
|
+
*
|
|
11
|
+
* The theme follows the site's own: mermaid takes a base theme at init, so the
|
|
12
|
+
* colour mode is read and the diagram is re-rendered when it changes.
|
|
13
|
+
*/
|
|
14
|
+
const props = defineProps<{ code?: string }>();
|
|
15
|
+
|
|
16
|
+
const container = useTemplateRef<HTMLElement>('container');
|
|
17
|
+
const colorMode = useColorMode();
|
|
18
|
+
const slots = useSlots();
|
|
19
|
+
const source = ref('');
|
|
20
|
+
const failed = ref(false);
|
|
21
|
+
|
|
22
|
+
let mermaid: typeof import('mermaid').default | undefined;
|
|
23
|
+
let counter = 0;
|
|
24
|
+
|
|
25
|
+
async function render() {
|
|
26
|
+
if (!import.meta.client || !container.value) return;
|
|
27
|
+
|
|
28
|
+
const text = source.value.trim();
|
|
29
|
+
if (!text) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
mermaid ??= (await import('mermaid')).default;
|
|
33
|
+
|
|
34
|
+
mermaid.initialize({
|
|
35
|
+
startOnLoad: false,
|
|
36
|
+
securityLevel: 'strict',
|
|
37
|
+
theme: colorMode.value === 'dark' ? 'dark' : 'default'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { svg } = await mermaid.render(`duxt-mermaid-${counter++}`, text);
|
|
41
|
+
container.value.innerHTML = svg;
|
|
42
|
+
failed.value = false;
|
|
43
|
+
} catch {
|
|
44
|
+
// A diagram that does not parse must not take the page with it. The source
|
|
45
|
+
// stays on screen, which is more useful than an empty box.
|
|
46
|
+
failed.value = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onMounted(() => {
|
|
51
|
+
source.value =
|
|
52
|
+
props.code ??
|
|
53
|
+
container.value?.textContent?.trim() ??
|
|
54
|
+
(typeof slots.default?.()[0]?.children === 'string'
|
|
55
|
+
? (slots.default()[0]!.children as string)
|
|
56
|
+
: '');
|
|
57
|
+
|
|
58
|
+
void render();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
watch(() => colorMode.value, render);
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div class="my-6 flex justify-center overflow-x-auto rounded-lg border p-4">
|
|
66
|
+
<div ref="container" :class="{ 'text-sm whitespace-pre': failed }">
|
|
67
|
+
<slot>{{ code }}</slot>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|