@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,295 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { sourcesForRoute } from '../../sources-resolve';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One feature, given a whole band: its prose on one side and the thing itself
|
|
6
|
+
* on the other.
|
|
7
|
+
*
|
|
8
|
+
* The feature GRID says what a site can do in four words per card. This says it
|
|
9
|
+
* in a paragraph and a running example, which is the difference between a list
|
|
10
|
+
* of claims and a demonstration — and the reason a landing page needs both
|
|
11
|
+
* rather than a longer grid.
|
|
12
|
+
*
|
|
13
|
+
* The sides alternate, and the alternation is decided by the CALLER's index
|
|
14
|
+
* rather than counted here: a component that counted its own instances would
|
|
15
|
+
* flip when a band is added above it, and a `reverse` written per band would be
|
|
16
|
+
* a layout decision in every entry of a config that should only carry content.
|
|
17
|
+
*/
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
showcase: DuxtResolved<DuxtShowcase>;
|
|
20
|
+
/** Position in the list. Even bands put the demo on the right. */
|
|
21
|
+
index: number;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const localeLink = useDuxtLink();
|
|
25
|
+
|
|
26
|
+
/** A band's own `reverse` wins; otherwise the position decides. */
|
|
27
|
+
const reversed = computed(
|
|
28
|
+
() => props.showcase.reverse ?? props.index % 2 === 1
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const demo = computed(() => props.showcase.demo);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the CLIENT lays the band out rather than this component.
|
|
35
|
+
*
|
|
36
|
+
* A split client is two rows with prose beside each, and only it knows where
|
|
37
|
+
* those rows are — so it takes the words through its slots and this component
|
|
38
|
+
* draws none of its own. Anything else keeps the ordinary two columns.
|
|
39
|
+
*/
|
|
40
|
+
const splitClient = computed(
|
|
41
|
+
() =>
|
|
42
|
+
demo.value.type === 'operation' &&
|
|
43
|
+
(demo.value.layout ?? (props.showcase.full ? 'split' : 'panel')) === 'split'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The heading each band contributes to the document outline.
|
|
48
|
+
*
|
|
49
|
+
* An id per band rather than one shared one: `aria-labelledby` on the section
|
|
50
|
+
* needs a target that is THIS band's title, and a landing page with five bands
|
|
51
|
+
* pointing at the same node is five sections claiming one name.
|
|
52
|
+
*/
|
|
53
|
+
const headingId = computed(() => `duxt-showcase-${props.index}`);
|
|
54
|
+
|
|
55
|
+
/** The files of a code demo. One draws a plain block, several draw a group. */
|
|
56
|
+
const files = computed(() =>
|
|
57
|
+
demo.value.type === 'code' ? demo.value.files : []
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The operation behind an `operation` demo — the try-it client on its own.
|
|
62
|
+
*
|
|
63
|
+
* Read out of the page Content already built for that endpoint, so nothing here
|
|
64
|
+
* parses OpenAPI a second time and a band and its page can never disagree. The
|
|
65
|
+
* collection is resolved from the TARGET's path rather than from the route this
|
|
66
|
+
* component happens to stand on, which is the landing page and belongs to no
|
|
67
|
+
* source.
|
|
68
|
+
*/
|
|
69
|
+
const duxt = useDuxtConfig();
|
|
70
|
+
const { locale, fallbackLocale } = useI18n();
|
|
71
|
+
|
|
72
|
+
const operationPath = computed(() =>
|
|
73
|
+
demo.value.type === 'operation' ? demo.value.to : undefined
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const { data: operationPage } = await useAsyncData(
|
|
77
|
+
() => `duxt-showcase-operation-${operationPath.value ?? props.index}`,
|
|
78
|
+
() => {
|
|
79
|
+
const path = operationPath.value;
|
|
80
|
+
if (!path) return Promise.resolve(null);
|
|
81
|
+
|
|
82
|
+
const chain = sourcesForRoute(
|
|
83
|
+
path,
|
|
84
|
+
locale.value,
|
|
85
|
+
duxt.resolvedSources ?? [],
|
|
86
|
+
fallbackLocale.value as string | string[] | undefined
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const collection = (chain[0]?.collection ?? 'docs') as DuxtCollectionArg;
|
|
90
|
+
|
|
91
|
+
return queryCollection(collection).path(path).first();
|
|
92
|
+
},
|
|
93
|
+
{ watch: [operationPath, locale] }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Its props, in the shape `OpenApiOperation` hands the client.
|
|
98
|
+
*
|
|
99
|
+
* Undefined where the page carries no operation — a mistyped path, a section
|
|
100
|
+
* that is not an API reference — and the band then draws no demo rather than
|
|
101
|
+
* throwing on the landing page.
|
|
102
|
+
*/
|
|
103
|
+
const operation = computed(() =>
|
|
104
|
+
openApiOperationProps(
|
|
105
|
+
(operationPage.value as { body?: unknown } | null)?.body
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<section
|
|
112
|
+
class="mx-auto max-w-[90rem] px-4 py-12 sm:py-16 lg:px-8"
|
|
113
|
+
:aria-labelledby="headingId"
|
|
114
|
+
>
|
|
115
|
+
<!-- TWO COLUMNS, OR ONE. A demo that is a PICTURE of something — a framed
|
|
116
|
+
page, a screenshot, a block of config — reads fine beside its
|
|
117
|
+
paragraph. A demo that IS the thing needs the width the thing was built
|
|
118
|
+
for, and then the prose goes above it rather than beside it. -->
|
|
119
|
+
<div
|
|
120
|
+
:class="
|
|
121
|
+
splitClient
|
|
122
|
+
? ''
|
|
123
|
+
: showcase.full
|
|
124
|
+
? 'flex flex-col gap-8'
|
|
125
|
+
: 'grid items-center gap-8 lg:grid-cols-2 lg:gap-16'
|
|
126
|
+
"
|
|
127
|
+
>
|
|
128
|
+
<!-- THE PROSE. `lg:order-2` rather than a reordered DOM: the reading
|
|
129
|
+
order is title, text, list, link in every band, and a source order
|
|
130
|
+
that alternates would hand a screen reader a demo before the sentence
|
|
131
|
+
that says what it is.
|
|
132
|
+
|
|
133
|
+
A SPLIT CLIENT DRAWS ITS OWN. It is two rows, each with prose beside
|
|
134
|
+
it, and both come from this same component through its slots — see
|
|
135
|
+
the demo below. -->
|
|
136
|
+
<DuxtLandingProse
|
|
137
|
+
v-if="!splitClient"
|
|
138
|
+
:prose="showcase"
|
|
139
|
+
:heading-id="headingId"
|
|
140
|
+
:class="[
|
|
141
|
+
reversed && !showcase.full ? 'lg:order-2' : '',
|
|
142
|
+
showcase.full ? 'max-w-3xl' : ''
|
|
143
|
+
]"
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<!-- THE DEMO. `min-w-0` because a highlighted line of config is wider
|
|
147
|
+
than its column and a grid track sized to its content would push the
|
|
148
|
+
prose off the page rather than scroll the code. -->
|
|
149
|
+
<!-- `self-start`: a demo that changes height — a code group whose tabs
|
|
150
|
+
hold files of different lengths — grows DOWNWARDS from a fixed top
|
|
151
|
+
edge. Centred it grew in both directions, so the tab strip the reader
|
|
152
|
+
just clicked moved up under their cursor. The prose beside it stays
|
|
153
|
+
centred, which is what the row's `items-center` does. -->
|
|
154
|
+
<div
|
|
155
|
+
class="min-w-0 lg:self-start"
|
|
156
|
+
:class="reversed && !showcase.full ? 'lg:order-1' : ''"
|
|
157
|
+
>
|
|
158
|
+
<DuxtLiveWindow
|
|
159
|
+
v-if="demo.type === 'frame'"
|
|
160
|
+
:tabs="[
|
|
161
|
+
{ label: showcase.title, to: demo.to, skeleton: demo.skeleton }
|
|
162
|
+
]"
|
|
163
|
+
:height="demo.height"
|
|
164
|
+
:chrome="demo.chrome ?? true"
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
<!-- THE CLIENT, ON ITS OWN. Not the page it lives on: the panel sits
|
|
168
|
+
beside the endpoint only above 80rem and under the whole
|
|
169
|
+
description everywhere narrower, so a band-sized frame of that page
|
|
170
|
+
showed prose and hid the control the band is named for. This is the
|
|
171
|
+
control, with nothing around it — and less to download than the
|
|
172
|
+
second copy of the application a frame would have booted. -->
|
|
173
|
+
<!-- AND IT SCROLLS ITSELF, the way the operation page bounds the same
|
|
174
|
+
panel at `xl`. A full client is server, authentication, parameters,
|
|
175
|
+
a body editor, the send button and the samples under it — running
|
|
176
|
+
to twice the height of every other band, with its own paragraph
|
|
177
|
+
sitting in the middle of a column of nothing.
|
|
178
|
+
|
|
179
|
+
`max-h`, not `h`: a GET with two parameters is a short panel, and
|
|
180
|
+
padding it out to a POST's height is a box mostly empty. No border
|
|
181
|
+
and no padding here either — the client brings its own, and a
|
|
182
|
+
second one around it was a box drawn around a box.
|
|
183
|
+
|
|
184
|
+
NONE OF THAT IN A `full` BAND. There the client has the width it
|
|
185
|
+
was built for and splits itself into two cards, so there is no
|
|
186
|
+
column of nothing to rescue and nothing to cap. -->
|
|
187
|
+
<div
|
|
188
|
+
v-else-if="demo.type === 'operation'"
|
|
189
|
+
class="not-typeset"
|
|
190
|
+
:class="
|
|
191
|
+
showcase.full
|
|
192
|
+
? ''
|
|
193
|
+
: 'max-h-[38rem] overflow-y-auto max-lg:max-h-[32rem]'
|
|
194
|
+
"
|
|
195
|
+
:style="
|
|
196
|
+
!showcase.full && demo.height
|
|
197
|
+
? { maxHeight: demo.height }
|
|
198
|
+
: undefined
|
|
199
|
+
"
|
|
200
|
+
>
|
|
201
|
+
<!-- `v-bind` on a typed component: the props come out of a page's
|
|
202
|
+
body, so they are a record here and the component's own contract
|
|
203
|
+
is what checks them at the other end. -->
|
|
204
|
+
<DuxtOpenApiClient
|
|
205
|
+
v-if="operation"
|
|
206
|
+
:key="demo.to"
|
|
207
|
+
v-bind="operation as any"
|
|
208
|
+
:layout="demo.layout ?? (showcase.full ? 'split' : 'panel')"
|
|
209
|
+
:reverse="reversed"
|
|
210
|
+
>
|
|
211
|
+
<!-- The band's own words, handed to the row they belong beside.
|
|
212
|
+
The `h2` the section is labelled by goes with the form; the
|
|
213
|
+
samples row gets an `h3`, because it is a part of this section
|
|
214
|
+
rather than a section of its own. -->
|
|
215
|
+
<template #beside-form>
|
|
216
|
+
<DuxtLandingProse :prose="showcase" :heading-id="headingId" />
|
|
217
|
+
</template>
|
|
218
|
+
|
|
219
|
+
<!-- THE `v-if` IS ON THE CONTENT, NOT ON THE SLOT. A
|
|
220
|
+
`<template v-if>` around a named slot means the slot is not
|
|
221
|
+
PASSED at all when the condition is false — and the server and
|
|
222
|
+
the client disagree about what stands in its place, which is
|
|
223
|
+
the hydration mismatch this produced:
|
|
224
|
+
|
|
225
|
+
rendered on server: (start of fragment)
|
|
226
|
+
expected on client: Symbol(v-cmt)
|
|
227
|
+
|
|
228
|
+
Passed always, with the condition inside, both render the same
|
|
229
|
+
comment node when there is nothing to show. -->
|
|
230
|
+
<template #beside-samples>
|
|
231
|
+
<DuxtLandingProse
|
|
232
|
+
v-if="demo.samples"
|
|
233
|
+
:prose="demo.samples"
|
|
234
|
+
:level="3"
|
|
235
|
+
/>
|
|
236
|
+
</template>
|
|
237
|
+
</DuxtOpenApiClient>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- ONE FILE: the block, with its own header naming it.
|
|
241
|
+
|
|
242
|
+
SEVERAL: `CodeGroup` — the same component `::code-group` draws on a
|
|
243
|
+
docs page, handed blocks built from config instead of from
|
|
244
|
+
Markdown. It reads the label off each child's `filename` and
|
|
245
|
+
switches that child's own header off, so a reader meets one control
|
|
246
|
+
in both places and this page grows no tab bar of its own. -->
|
|
247
|
+
<CodeGroup
|
|
248
|
+
v-else-if="demo.type === 'code' && files.length > 1"
|
|
249
|
+
class="my-0!"
|
|
250
|
+
>
|
|
251
|
+
<DuxtCodeBlock
|
|
252
|
+
v-for="(file, position) in files"
|
|
253
|
+
:key="file.name ?? position"
|
|
254
|
+
:code="file.code"
|
|
255
|
+
:language="file.language"
|
|
256
|
+
:filename="file.name"
|
|
257
|
+
/>
|
|
258
|
+
</CodeGroup>
|
|
259
|
+
|
|
260
|
+
<DuxtCodeBlock
|
|
261
|
+
v-else-if="demo.type === 'code' && files[0]"
|
|
262
|
+
:code="files[0].code"
|
|
263
|
+
:language="files[0].language"
|
|
264
|
+
:filename="files[0].name"
|
|
265
|
+
class="my-0!"
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
<!-- Two <img>s rather than one: a screenshot of a light interface on a
|
|
269
|
+
dark page is a torch, and `srcDark` is how a site hands over the
|
|
270
|
+
other file. Without one, `src` serves both. -->
|
|
271
|
+
<div
|
|
272
|
+
v-else-if="demo.type === 'image'"
|
|
273
|
+
class="overflow-hidden rounded-xl border bg-background shadow-sm"
|
|
274
|
+
>
|
|
275
|
+
<img
|
|
276
|
+
:src="demo.src"
|
|
277
|
+
:alt="demo.alt ?? ''"
|
|
278
|
+
loading="lazy"
|
|
279
|
+
decoding="async"
|
|
280
|
+
class="w-full"
|
|
281
|
+
:class="demo.srcDark ? 'dark:hidden' : ''"
|
|
282
|
+
/>
|
|
283
|
+
<img
|
|
284
|
+
v-if="demo.srcDark"
|
|
285
|
+
:src="demo.srcDark"
|
|
286
|
+
alt=""
|
|
287
|
+
loading="lazy"
|
|
288
|
+
decoding="async"
|
|
289
|
+
class="hidden w-full dark:block"
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
</template>
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A page of this site, framed as a browser window and operable — the reader
|
|
4
|
+
* scrolls it, opens its navigation and flips its theme without leaving the
|
|
5
|
+
* page it stands on.
|
|
6
|
+
*
|
|
7
|
+
* The landing page had one of these written into it. It now needs four: the big
|
|
8
|
+
* one under the hero, and one per showcase band that would rather show the real
|
|
9
|
+
* thing than a screenshot of it. So the window is a component, and everything
|
|
10
|
+
* that made the original one expensive to get right lives here once:
|
|
11
|
+
*
|
|
12
|
+
* - it is mounted ON APPROACH, never on the server. The frame is a second copy
|
|
13
|
+
* of the application, and an iframe in the initial HTML is a second full page
|
|
14
|
+
* load competing with the first paint;
|
|
15
|
+
* - a POSTER covers the box until that copy has booted, so the band has its
|
|
16
|
+
* height from the first byte rather than jumping to a page's worth;
|
|
17
|
+
* - the address bar is polled rather than listened for. The frame is
|
|
18
|
+
* same-origin, so its location is readable — but the site inside it is a Nuxt
|
|
19
|
+
* app, and a client-side route change fires no `load` on the element.
|
|
20
|
+
*
|
|
21
|
+
* TABS ARE PAGES, NOT PANES. One frame, one page loaded at a time: four frames
|
|
22
|
+
* behind a tab bar would be four copies of the application booting for a band
|
|
23
|
+
* the reader sees one quarter of.
|
|
24
|
+
*/
|
|
25
|
+
const props = withDefaults(
|
|
26
|
+
defineProps<{
|
|
27
|
+
/** The pages behind the tab bar. One entry draws no tab bar at all. */
|
|
28
|
+
tabs?: DuxtResolved<DuxtDemoTab>[];
|
|
29
|
+
/** Window height, any CSS length. */
|
|
30
|
+
height?: string;
|
|
31
|
+
/** The browser chrome. `false` leaves the page in a plain bordered box. */
|
|
32
|
+
chrome?: boolean;
|
|
33
|
+
/** Prefetch the first tab's document while the browser is idle. */
|
|
34
|
+
prefetch?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* A screenshot shown until the live page is up — or instead of it, where
|
|
37
|
+
* `live` is false. Without one the window draws the SHAPE of a page from
|
|
38
|
+
* the theme's own tokens, which needs no asset and is right in both modes
|
|
39
|
+
* by construction.
|
|
40
|
+
*/
|
|
41
|
+
poster?: string;
|
|
42
|
+
/** The dark-mode file. Without one, `poster` serves both — a torch. */
|
|
43
|
+
posterDark?: string;
|
|
44
|
+
/** The poster's alt text. Only read where the poster is the content. */
|
|
45
|
+
posterAlt?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Load the page at all. `false` shows only the poster, which is what a site
|
|
48
|
+
* picks when a second copy of the application is a cost it will not pay.
|
|
49
|
+
*/
|
|
50
|
+
live?: boolean;
|
|
51
|
+
}>(),
|
|
52
|
+
{ chrome: true, prefetch: false, live: true }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const localeLink = useDuxtLink();
|
|
56
|
+
const { t } = useI18n();
|
|
57
|
+
|
|
58
|
+
const active = ref(0);
|
|
59
|
+
const tabs = computed(() => props.tabs ?? []);
|
|
60
|
+
const current = computed(() => tabs.value[active.value] ?? tabs.value[0]);
|
|
61
|
+
|
|
62
|
+
/** Where the frame is pointed, in the reader's own language. */
|
|
63
|
+
const target = computed(() => localeLink(current.value?.to ?? '/'));
|
|
64
|
+
|
|
65
|
+
const root = useTemplateRef<HTMLElement>('root');
|
|
66
|
+
const frame = useTemplateRef<HTMLIFrameElement>('frame');
|
|
67
|
+
|
|
68
|
+
/** Mounted only once the band is on its way in — see the note above. */
|
|
69
|
+
const visible = ref(false);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* True until the framed page has loaded once. Reset on a tab change, because
|
|
73
|
+
* that IS a first load — a different page, fetched from scratch.
|
|
74
|
+
*
|
|
75
|
+
* Only the first load of each: a route change the reader makes INSIDE the frame
|
|
76
|
+
* is the app's own navigation, which draws its own progress bar, and a second
|
|
77
|
+
* spinner over it would report the same thing twice.
|
|
78
|
+
*/
|
|
79
|
+
const loading = ref(true);
|
|
80
|
+
|
|
81
|
+
const location = ref<{ href: string; path: string }>();
|
|
82
|
+
|
|
83
|
+
function readFrameLocation() {
|
|
84
|
+
try {
|
|
85
|
+
const view = frame.value?.contentWindow;
|
|
86
|
+
if (!view) return;
|
|
87
|
+
|
|
88
|
+
const { href, pathname, search, hash } = view.location;
|
|
89
|
+
// The bar shows the whole URL the way a browser's does — origin included,
|
|
90
|
+
// which is only knowable in the browser. The link keeps the path, because
|
|
91
|
+
// that is what the router routes on.
|
|
92
|
+
location.value = { href, path: `${pathname}${search}${hash}` };
|
|
93
|
+
} catch {
|
|
94
|
+
// A cross-origin document: nothing to read, and nothing to report.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function select(index: number) {
|
|
99
|
+
if (index === active.value) return;
|
|
100
|
+
active.value = index;
|
|
101
|
+
loading.value = true;
|
|
102
|
+
// The bar would otherwise keep printing the page we just navigated away
|
|
103
|
+
// from, for as long as the new one takes to answer.
|
|
104
|
+
location.value = undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The document, fetched into the cache while the browser is idle.
|
|
109
|
+
*
|
|
110
|
+
* The frame still mounts on approach — a prefetch is not an execution. What it
|
|
111
|
+
* removes is the SLOW half: a document, its entry chunk, its payload. By the
|
|
112
|
+
* time the observer says "now" there is nothing left to download.
|
|
113
|
+
* `as="document"` is what makes the browser reuse it for an iframe rather than
|
|
114
|
+
* fetch it a second time.
|
|
115
|
+
*
|
|
116
|
+
* Off by default, and set only on the window a reader is certain to reach. Four
|
|
117
|
+
* prefetched applications is not a saving, it is the first paint again.
|
|
118
|
+
*/
|
|
119
|
+
useHead(() => ({
|
|
120
|
+
link:
|
|
121
|
+
props.prefetch && props.live
|
|
122
|
+
? [{ rel: 'prefetch', as: 'document', href: target.value }]
|
|
123
|
+
: []
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
if (!props.live) return;
|
|
128
|
+
|
|
129
|
+
const element = root.value;
|
|
130
|
+
if (!element) return;
|
|
131
|
+
|
|
132
|
+
// No IntersectionObserver (an old browser, a test environment): show it
|
|
133
|
+
// rather than leave an empty box on the page.
|
|
134
|
+
if (!('IntersectionObserver' in window)) {
|
|
135
|
+
visible.value = true;
|
|
136
|
+
} else {
|
|
137
|
+
const observer = new IntersectionObserver(
|
|
138
|
+
(entries) => {
|
|
139
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
140
|
+
visible.value = true;
|
|
141
|
+
observer.disconnect();
|
|
142
|
+
},
|
|
143
|
+
// Far enough ahead that the frame has begun booting by the time the band
|
|
144
|
+
// is on screen: it is a second copy of the application, so the gap
|
|
145
|
+
// between "in view" and "usable" is a page load rather than a paint.
|
|
146
|
+
{ rootMargin: '600px' }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
observer.observe(element);
|
|
150
|
+
onBeforeUnmount(() => observer.disconnect());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const timer = setInterval(readFrameLocation, 500);
|
|
154
|
+
onBeforeUnmount(() => clearInterval(timer));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The origin comes from the REQUEST, which both halves can read: `window` on
|
|
159
|
+
* the client, the incoming headers on the server. Every other source swaps the
|
|
160
|
+
* printed string once — at the frame's first load, or at hydration — and that
|
|
161
|
+
* swap is a flicker between two spellings of one page.
|
|
162
|
+
*/
|
|
163
|
+
const requestUrl = useRequestURL({
|
|
164
|
+
xForwardedHost: true,
|
|
165
|
+
xForwardedProto: true
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const href = computed(
|
|
169
|
+
() => location.value?.href ?? `${requestUrl.origin}${target.value}`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
/** The frame's current page, falling back to the one it was pointed at. */
|
|
173
|
+
const path = computed(() => location.value?.path ?? target.value);
|
|
174
|
+
|
|
175
|
+
const title = computed(
|
|
176
|
+
() => current.value?.label ?? t('duxt.defaults.landing.preview')
|
|
177
|
+
);
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<template>
|
|
181
|
+
<div
|
|
182
|
+
ref="root"
|
|
183
|
+
class="overflow-hidden rounded-xl border bg-background shadow-sm"
|
|
184
|
+
>
|
|
185
|
+
<!-- The window's own frame. The address bar is a <div>, not an input: it
|
|
186
|
+
says which page is inside, and a text field a reader can type into but
|
|
187
|
+
not submit is a control that lies. -->
|
|
188
|
+
<div
|
|
189
|
+
v-if="chrome"
|
|
190
|
+
class="flex items-center gap-3 border-b bg-muted/60 px-4 py-2.5"
|
|
191
|
+
>
|
|
192
|
+
<div aria-hidden="true" class="flex shrink-0 items-center gap-1.5">
|
|
193
|
+
<span class="size-2.5 rounded-full bg-muted-foreground/30" />
|
|
194
|
+
<span class="size-2.5 rounded-full bg-muted-foreground/30" />
|
|
195
|
+
<span class="size-2.5 rounded-full bg-muted-foreground/30" />
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- One fixed width, centred: a bar sized to its text grows and shrinks
|
|
199
|
+
on every navigation inside the frame, which reads as the frame
|
|
200
|
+
jittering rather than as a URL changing. The status icon sits where a
|
|
201
|
+
browser puts its padlock — leftmost, always there, so the URL never
|
|
202
|
+
shifts when it changes. -->
|
|
203
|
+
<div class="flex min-w-0 flex-1 justify-center">
|
|
204
|
+
<div
|
|
205
|
+
class="flex w-full max-w-md items-center gap-2 rounded-md bg-background/70 px-3 py-1 font-mono text-xs text-muted-foreground"
|
|
206
|
+
>
|
|
207
|
+
<Icon
|
|
208
|
+
:name="loading && live ? 'lucide:loader-circle' : 'lucide:globe'"
|
|
209
|
+
class="size-3 shrink-0"
|
|
210
|
+
:class="loading && live ? 'animate-spin' : 'opacity-60'"
|
|
211
|
+
role="status"
|
|
212
|
+
:aria-label="
|
|
213
|
+
loading && live ? $t('duxt.defaults.landing.preview') : undefined
|
|
214
|
+
"
|
|
215
|
+
/>
|
|
216
|
+
<span class="truncate">{{ href }}</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- The way out of the frame. A page read inside a 30 rem window is a
|
|
221
|
+
demonstration; at some point the reader wants the real one. -->
|
|
222
|
+
<UiButton as-child size="sm" variant="ghost" class="shrink-0">
|
|
223
|
+
<NuxtLink :to="path">
|
|
224
|
+
<Icon name="lucide:external-link" class="size-3.5" />
|
|
225
|
+
<span class="sr-only sm:not-sr-only">
|
|
226
|
+
{{ $t('duxt.defaults.landing.previewOpen') }}
|
|
227
|
+
</span>
|
|
228
|
+
</NuxtLink>
|
|
229
|
+
</UiButton>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- The tab bar, where there is more than one page to show. Real buttons
|
|
233
|
+
with `aria-selected` rather than a styled div: this is a tablist, the
|
|
234
|
+
arrow keys are expected to work, and they do because the buttons are
|
|
235
|
+
focusable in order. -->
|
|
236
|
+
<div
|
|
237
|
+
v-if="tabs.length > 1"
|
|
238
|
+
role="tablist"
|
|
239
|
+
:aria-label="$t('duxt.defaults.landing.demoTitle')"
|
|
240
|
+
class="flex gap-1 overflow-x-auto border-b bg-muted/30 px-2 py-1.5"
|
|
241
|
+
>
|
|
242
|
+
<button
|
|
243
|
+
v-for="(tab, index) in tabs"
|
|
244
|
+
:key="tab.to"
|
|
245
|
+
type="button"
|
|
246
|
+
role="tab"
|
|
247
|
+
:aria-selected="index === active"
|
|
248
|
+
class="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
|
|
249
|
+
:class="
|
|
250
|
+
index === active
|
|
251
|
+
? 'bg-background text-foreground shadow-sm'
|
|
252
|
+
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'
|
|
253
|
+
"
|
|
254
|
+
@click="select(index)"
|
|
255
|
+
>
|
|
256
|
+
<Icon v-if="tab.icon" :name="tab.icon" class="size-4" />
|
|
257
|
+
{{ tab.label }}
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<!-- THE BOX OWNS THE HEIGHT, not whatever happens to be inside it. The
|
|
262
|
+
frame is mounted late and the poster is absolutely positioned, so with
|
|
263
|
+
the height on the frame the band would have none at all until the frame
|
|
264
|
+
arrived, and then jump to a page's worth. -->
|
|
265
|
+
<!-- `@container`, because what is inside is sized by THIS BOX and not by
|
|
266
|
+
the browser window. The frame gets that for free — an iframe carries a
|
|
267
|
+
viewport of its own — but the poster is drawn in this document, and
|
|
268
|
+
with viewport breakpoints a half-width window on a wide screen drew the
|
|
269
|
+
full desktop page squeezed into it. -->
|
|
270
|
+
<div
|
|
271
|
+
class="relative @container h-[44rem] max-lg:h-[36rem] max-sm:h-[28rem]"
|
|
272
|
+
:style="height ? { height } : undefined"
|
|
273
|
+
>
|
|
274
|
+
<ClientOnly v-if="live">
|
|
275
|
+
<!-- `:key` on the target, so a tab change REPLACES the element rather
|
|
276
|
+
than repointing it: repointing an iframe pushes an entry onto the
|
|
277
|
+
browser's own history, and the reader's back button then walks the
|
|
278
|
+
tab bar instead of leaving the page.
|
|
279
|
+
|
|
280
|
+
NO `loading="lazy"`. The observer above has already decided this
|
|
281
|
+
frame should load, and the attribute puts the browser's heuristic
|
|
282
|
+
in front of that decision. -->
|
|
283
|
+
<iframe
|
|
284
|
+
v-if="visible"
|
|
285
|
+
:key="target"
|
|
286
|
+
ref="frame"
|
|
287
|
+
:src="target"
|
|
288
|
+
:title="title"
|
|
289
|
+
class="h-full w-full"
|
|
290
|
+
@load="
|
|
291
|
+
loading = false;
|
|
292
|
+
readFrameLocation();
|
|
293
|
+
"
|
|
294
|
+
/>
|
|
295
|
+
</ClientOnly>
|
|
296
|
+
|
|
297
|
+
<!-- Over the frame until it has loaded once, then gone. Not `v-if`-ed
|
|
298
|
+
away on the same tick it stops being needed: a poster that vanishes
|
|
299
|
+
the instant the frame reports `load` swaps one picture for another
|
|
300
|
+
mid-paint, and a fade covers exactly that. `pointer-events-none` so a
|
|
301
|
+
poster on its way out cannot eat the click that lands underneath. -->
|
|
302
|
+
<Transition
|
|
303
|
+
enter-from-class="opacity-0"
|
|
304
|
+
leave-to-class="opacity-0"
|
|
305
|
+
enter-active-class="transition-opacity"
|
|
306
|
+
leave-active-class="transition-opacity duration-500"
|
|
307
|
+
>
|
|
308
|
+
<div
|
|
309
|
+
v-if="loading || !live"
|
|
310
|
+
class="absolute inset-0 overflow-hidden bg-background"
|
|
311
|
+
:class="live ? 'pointer-events-none' : ''"
|
|
312
|
+
:aria-hidden="live ? 'true' : undefined"
|
|
313
|
+
>
|
|
314
|
+
<!-- Two <img>s rather than one: a screenshot of a light theme on a
|
|
315
|
+
dark page is a torch, and `posterDark` is how a site hands over
|
|
316
|
+
the other file. Without one, `poster` serves both. -->
|
|
317
|
+
<template v-if="poster">
|
|
318
|
+
<img
|
|
319
|
+
:src="poster"
|
|
320
|
+
:alt="posterAlt ?? ''"
|
|
321
|
+
decoding="async"
|
|
322
|
+
class="h-full w-full object-cover object-top"
|
|
323
|
+
:class="posterDark ? 'dark:hidden' : ''"
|
|
324
|
+
/>
|
|
325
|
+
<img
|
|
326
|
+
v-if="posterDark"
|
|
327
|
+
:src="posterDark"
|
|
328
|
+
alt=""
|
|
329
|
+
decoding="async"
|
|
330
|
+
class="hidden h-full w-full object-cover object-top dark:block"
|
|
331
|
+
/>
|
|
332
|
+
</template>
|
|
333
|
+
|
|
334
|
+
<!-- No screenshot: the SHAPE of a documentation page, drawn from the
|
|
335
|
+
theme's own tokens. It needs no asset, it is right in both modes
|
|
336
|
+
by construction, and it says "a page is coming" where a blank box
|
|
337
|
+
said nothing. -->
|
|
338
|
+
<DuxtPreviewSkeleton v-else />
|
|
339
|
+
</div>
|
|
340
|
+
</Transition>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</template>
|