@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,479 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The landing page: its own route, full width, no sidebar. A consumer that
|
|
4
|
+
* wants a different one drops an `index.vue` of its own — Nuxt's layer
|
|
5
|
+
* override, no configuration.
|
|
6
|
+
*
|
|
7
|
+
* SIX BANDS, each of which draws nothing when its config is absent: the hero,
|
|
8
|
+
* its numbers, the window, a band per feature, the grid, the closing list. A
|
|
9
|
+
* site that sets only a headline gets a headline and a button, not five empty
|
|
10
|
+
* sections.
|
|
11
|
+
*
|
|
12
|
+
* The middle of that is what the page is for. A grid of cards states four claims
|
|
13
|
+
* in four words each; the bands between the window and the grid take one feature
|
|
14
|
+
* at a time and put the feature ITSELF beside the sentence — a live page of the
|
|
15
|
+
* API reference, the config that produced it. A reader who never leaves this
|
|
16
|
+
* page has still seen the thing work.
|
|
17
|
+
*
|
|
18
|
+
* No closing call to action: the hero's buttons ARE the call, and repeating them
|
|
19
|
+
* at the bottom asks a reader who just arrived to decide twice.
|
|
20
|
+
*/
|
|
21
|
+
const duxt = useDuxtConfig();
|
|
22
|
+
const localeLink = useDuxtLink();
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const notify = useDuxtToast();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Where a hero button goes when the config did not say — the same fallback
|
|
28
|
+
* `index.vue` and `DuxtHeader` use, because the layer cannot know what a
|
|
29
|
+
* consumer called its first page.
|
|
30
|
+
*/
|
|
31
|
+
const firstSection = computed(() => duxt.sections?.[0]?.to ?? '/');
|
|
32
|
+
const actionTarget = (action: DuxtResolved<DuxtAction>) =>
|
|
33
|
+
action.to ?? firstSection.value;
|
|
34
|
+
|
|
35
|
+
const landing = computed(() => duxt.landing);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The install command, highlighted on the SERVER for the reason the
|
|
39
|
+
* package-manager block does it there: Shiki is the single biggest thing that
|
|
40
|
+
* could end up in the client bundle, and a line of shell does not need it
|
|
41
|
+
* twice.
|
|
42
|
+
*/
|
|
43
|
+
const command = computed(() => landing.value?.command);
|
|
44
|
+
const { data: highlightedCommand } = await useAsyncData(
|
|
45
|
+
() => `landing-command-${command.value ?? ''}`,
|
|
46
|
+
() =>
|
|
47
|
+
command.value ? highlightCode(command.value, 'bash') : Promise.resolve(''),
|
|
48
|
+
{ watch: [command] }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const copied = ref(false);
|
|
52
|
+
async function copyCommand() {
|
|
53
|
+
if (!command.value) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await navigator.clipboard.writeText(command.value);
|
|
57
|
+
copied.value = true;
|
|
58
|
+
// The icon alone is a 3.5 rem change under the reader's own cursor, which
|
|
59
|
+
// is exactly where they are not looking after a click. The toast is the
|
|
60
|
+
// confirmation; the icon is what stays for the two seconds after it.
|
|
61
|
+
notify.success(t('duxt.code.copiedToast'));
|
|
62
|
+
setTimeout(() => (copied.value = false), 2000);
|
|
63
|
+
} catch {
|
|
64
|
+
notify.error(t('duxt.page.copyFailed'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The badge, in either of its two shapes. A string is the label and nothing
|
|
70
|
+
* else; an object may carry an icon, a colour and a link.
|
|
71
|
+
*/
|
|
72
|
+
const badge = computed(() => {
|
|
73
|
+
const value = landing.value?.badge as
|
|
74
|
+
| string
|
|
75
|
+
| DuxtResolved<DuxtBadge>
|
|
76
|
+
| undefined;
|
|
77
|
+
|
|
78
|
+
if (!value) return undefined;
|
|
79
|
+
|
|
80
|
+
const resolved = typeof value === 'string' ? { label: value } : value;
|
|
81
|
+
|
|
82
|
+
// `{version}` in the label reads the site's own `version`. A release badge
|
|
83
|
+
// wants the number, and a number typed out here is a second copy of what
|
|
84
|
+
// `version` already holds — wrong from the next release onwards.
|
|
85
|
+
return {
|
|
86
|
+
...resolved,
|
|
87
|
+
label: String(resolved.label ?? '').replace(
|
|
88
|
+
'{version}',
|
|
89
|
+
String(duxt.version ?? '')
|
|
90
|
+
)
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `<component :is>` needs the COMPONENT, not its name: handed the string
|
|
96
|
+
* `'NuxtLink'` Vue renders an unknown `<NuxtLink>` element, which the browser
|
|
97
|
+
* shows and nobody can click.
|
|
98
|
+
*/
|
|
99
|
+
const linkComponent = resolveComponent('NuxtLink');
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The tabs of the big window.
|
|
103
|
+
*
|
|
104
|
+
* `demo` is the tabbed form and wins where it is set; `preview` is the same
|
|
105
|
+
* idea with one page, and is what a site that has already configured
|
|
106
|
+
* `index.vue` still has. Neither: the first section, framed — the window is the
|
|
107
|
+
* one band worth drawing on a claim as thin as "there is a site behind this".
|
|
108
|
+
*/
|
|
109
|
+
const demoTabs = computed<DuxtResolved<DuxtDemoTab>[]>(() => {
|
|
110
|
+
const tabs = landing.value?.demo?.tabs;
|
|
111
|
+
if (tabs?.length) return tabs;
|
|
112
|
+
|
|
113
|
+
const preview = landing.value?.preview;
|
|
114
|
+
// A preview pointed at the root would frame this very page, which nests the
|
|
115
|
+
// landing page inside itself.
|
|
116
|
+
const to =
|
|
117
|
+
preview?.to && preview.to !== '/' ? preview.to : firstSection.value;
|
|
118
|
+
|
|
119
|
+
return [{ label: t('duxt.defaults.landing.preview'), to }];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const demoHeight = computed(
|
|
123
|
+
() => landing.value?.demo?.height ?? landing.value?.preview?.height
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The screenshot half of `preview`, which only the single-page form has.
|
|
128
|
+
*
|
|
129
|
+
* A poster is a picture of ONE page, so a tab bar cannot carry one: it would be
|
|
130
|
+
* right for the first tab and a lie for the other three. A site that configured
|
|
131
|
+
* `demo` has said it wants the live thing anyway.
|
|
132
|
+
*/
|
|
133
|
+
const poster = computed(() =>
|
|
134
|
+
landing.value?.demo?.tabs?.length ? undefined : landing.value?.preview
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The bands, each carrying the ROW it starts at rather than its own position.
|
|
139
|
+
*
|
|
140
|
+
* The sides alternate down the page, and a band decides its side from the
|
|
141
|
+
* number it is handed. A split try-it client draws two rows inside one band, so
|
|
142
|
+
* a count of bands puts every band after it back on the side the row above just
|
|
143
|
+
* used — the alternation stops halfway down the page, which is exactly what it
|
|
144
|
+
* did. Counting rows carries it across.
|
|
145
|
+
*/
|
|
146
|
+
const bands = computed(() => {
|
|
147
|
+
let row = 0;
|
|
148
|
+
|
|
149
|
+
return (landing.value?.showcase ?? []).map((showcase) => {
|
|
150
|
+
const at = row;
|
|
151
|
+
row += duxtShowcaseRows(showcase);
|
|
152
|
+
|
|
153
|
+
return { showcase, index: at };
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
useSeoMeta({
|
|
158
|
+
title: duxt.title,
|
|
159
|
+
description: landing.value?.description,
|
|
160
|
+
// The landing page is the site, not an article under it.
|
|
161
|
+
ogType: 'website'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
defineOgImage('Duxt', {
|
|
165
|
+
title: landing.value?.headline ?? duxt.title,
|
|
166
|
+
description: landing.value?.description,
|
|
167
|
+
site: duxt.title,
|
|
168
|
+
version: duxt.version ?? ''
|
|
169
|
+
});
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<template>
|
|
173
|
+
<div>
|
|
174
|
+
<!-- HERO. Three layers behind the words, all of them `aria-hidden` and
|
|
175
|
+
none of them able to take a click: the grid, two colour washes and a
|
|
176
|
+
fade into the page below. They are a background, not content.
|
|
177
|
+
|
|
178
|
+
The washes are `blur-3xl` circles rather than an image: they are
|
|
179
|
+
right in both themes because they are the theme's own primary colour
|
|
180
|
+
at 20% over the theme's own background, and they cost no request. -->
|
|
181
|
+
<section class="relative isolate overflow-hidden">
|
|
182
|
+
<div
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
class="pointer-events-none absolute inset-0 -z-10"
|
|
185
|
+
>
|
|
186
|
+
<div
|
|
187
|
+
class="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border)_1px,transparent_1px)] bg-[size:3rem_3rem] opacity-40 [mask-image:radial-gradient(ellipse_at_center,black,transparent_70%)]"
|
|
188
|
+
/>
|
|
189
|
+
<div
|
|
190
|
+
class="absolute -top-40 left-1/2 size-[36rem] -translate-x-1/2 rounded-full bg-primary/20 blur-3xl"
|
|
191
|
+
/>
|
|
192
|
+
<div
|
|
193
|
+
class="absolute top-20 -right-32 size-[28rem] rounded-full bg-primary/10 blur-3xl"
|
|
194
|
+
/>
|
|
195
|
+
<!-- The bottom edge. Without it the grid stops on a hard line across
|
|
196
|
+
the page, which reads as a section border nobody drew. -->
|
|
197
|
+
<div
|
|
198
|
+
class="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-b from-transparent to-background"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div
|
|
203
|
+
class="mx-auto max-w-5xl px-4 pt-24 pb-12 text-center sm:pt-32 sm:pb-16"
|
|
204
|
+
>
|
|
205
|
+
<!-- `as-child` around a link when the badge points somewhere: a pill
|
|
206
|
+
a reader can click has to BE the anchor, not sit inside one, or
|
|
207
|
+
the hover and focus rings belong to two different boxes. -->
|
|
208
|
+
<UiBadge
|
|
209
|
+
v-if="badge"
|
|
210
|
+
:variant="badge.variant ?? 'secondary'"
|
|
211
|
+
class="mb-6"
|
|
212
|
+
:as-child="Boolean(badge.to)"
|
|
213
|
+
>
|
|
214
|
+
<NuxtLink
|
|
215
|
+
v-if="badge.to"
|
|
216
|
+
:to="localeLink(badge.to)"
|
|
217
|
+
:target="badge.external ? '_blank' : undefined"
|
|
218
|
+
:rel="badge.external ? 'noopener' : undefined"
|
|
219
|
+
>
|
|
220
|
+
<Icon v-if="badge.icon" :name="badge.icon" class="size-3" />
|
|
221
|
+
{{ badge.label }}
|
|
222
|
+
</NuxtLink>
|
|
223
|
+
|
|
224
|
+
<template v-else>
|
|
225
|
+
<Icon v-if="badge.icon" :name="badge.icon" class="size-3" />
|
|
226
|
+
{{ badge.label }}
|
|
227
|
+
</template>
|
|
228
|
+
</UiBadge>
|
|
229
|
+
|
|
230
|
+
<h1
|
|
231
|
+
class="text-4xl font-semibold tracking-tight text-balance sm:text-6xl lg:text-7xl"
|
|
232
|
+
>
|
|
233
|
+
{{ landing?.headline ?? duxt.title }}
|
|
234
|
+
</h1>
|
|
235
|
+
|
|
236
|
+
<p
|
|
237
|
+
v-if="landing?.description"
|
|
238
|
+
class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground text-pretty sm:text-xl"
|
|
239
|
+
>
|
|
240
|
+
{{ landing.description }}
|
|
241
|
+
</p>
|
|
242
|
+
|
|
243
|
+
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
|
244
|
+
<UiButton
|
|
245
|
+
v-for="(action, index) in landing?.actions ?? []"
|
|
246
|
+
:key="index"
|
|
247
|
+
as-child
|
|
248
|
+
size="lg"
|
|
249
|
+
:variant="action.variant ?? 'default'"
|
|
250
|
+
>
|
|
251
|
+
<NuxtLink
|
|
252
|
+
:to="localeLink(actionTarget(action))"
|
|
253
|
+
:target="action.external ? '_blank' : undefined"
|
|
254
|
+
:rel="action.external ? 'noopener' : undefined"
|
|
255
|
+
>
|
|
256
|
+
<Icon v-if="action.icon" :name="action.icon" class="size-4" />
|
|
257
|
+
{{ action.label }}
|
|
258
|
+
</NuxtLink>
|
|
259
|
+
</UiButton>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- The command, if there is one. A <button> around the whole line
|
|
263
|
+
rather than an icon beside it: the line is not selectable text in
|
|
264
|
+
any useful sense on a phone, and the target should be what the
|
|
265
|
+
reader is looking at. -->
|
|
266
|
+
<div v-if="command" class="mt-8 flex justify-center">
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="group flex max-w-full cursor-pointer items-center gap-3 rounded-lg border bg-muted/40 px-4 py-2.5 font-mono text-sm transition-colors hover:bg-muted"
|
|
270
|
+
:aria-label="
|
|
271
|
+
copied ? $t('duxt.code.copied') : $t('duxt.code.copyCommand')
|
|
272
|
+
"
|
|
273
|
+
@click="copyCommand"
|
|
274
|
+
>
|
|
275
|
+
<span aria-hidden="true" class="text-muted-foreground select-none"
|
|
276
|
+
>$</span
|
|
277
|
+
>
|
|
278
|
+
<span
|
|
279
|
+
v-if="highlightedCommand"
|
|
280
|
+
class="min-w-0 overflow-x-auto text-left [&_pre]:bg-transparent!"
|
|
281
|
+
v-html="highlightedCommand"
|
|
282
|
+
/>
|
|
283
|
+
<span v-else class="min-w-0 overflow-x-auto text-left">{{
|
|
284
|
+
command
|
|
285
|
+
}}</span>
|
|
286
|
+
<Icon
|
|
287
|
+
:name="copied ? 'lucide:check' : 'lucide:copy'"
|
|
288
|
+
class="size-3.5 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground"
|
|
289
|
+
/>
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<!-- THE NUMBERS. A <dl>, because that is what a value and the words
|
|
294
|
+
naming it are — and it is what puts "7" and "locales" in one entry
|
|
295
|
+
for a screen reader rather than two adjacent strings.
|
|
296
|
+
|
|
297
|
+
WRAPPED, not gridded. A grid needs its column count written down,
|
|
298
|
+
and the count is the SITE's: three true numbers beat four with a
|
|
299
|
+
filler in the fourth, and the next site along may have two or five.
|
|
300
|
+
A row that wraps and centres is right for all of them;
|
|
301
|
+
`sm:grid-cols-4` was right for exactly one and left a hole
|
|
302
|
+
otherwise. -->
|
|
303
|
+
<dl
|
|
304
|
+
v-if="landing?.stats?.length"
|
|
305
|
+
class="mx-auto mt-14 flex max-w-3xl flex-wrap items-start justify-center gap-x-14 gap-y-8"
|
|
306
|
+
>
|
|
307
|
+
<div
|
|
308
|
+
v-for="stat in landing.stats"
|
|
309
|
+
:key="stat.value"
|
|
310
|
+
class="min-w-28 text-center"
|
|
311
|
+
>
|
|
312
|
+
<dt class="sr-only">{{ stat.label }}</dt>
|
|
313
|
+
<dd>
|
|
314
|
+
<span
|
|
315
|
+
class="block text-2xl font-semibold tracking-tight sm:text-3xl"
|
|
316
|
+
>
|
|
317
|
+
{{ stat.value }}
|
|
318
|
+
</span>
|
|
319
|
+
<span
|
|
320
|
+
aria-hidden="true"
|
|
321
|
+
class="mt-1 flex items-center justify-center gap-1.5 text-xs text-muted-foreground"
|
|
322
|
+
>
|
|
323
|
+
<Icon v-if="stat.icon" :name="stat.icon" class="size-3.5" />
|
|
324
|
+
{{ stat.label }}
|
|
325
|
+
</span>
|
|
326
|
+
</dd>
|
|
327
|
+
</div>
|
|
328
|
+
</dl>
|
|
329
|
+
</div>
|
|
330
|
+
</section>
|
|
331
|
+
|
|
332
|
+
<!-- THE LIVE DEMO. Not a picture of the documentation — the
|
|
333
|
+
documentation, in a frame, operable, with a tab bar over it: the
|
|
334
|
+
reader flips between the guide, the API reference and the changelog
|
|
335
|
+
without leaving this page. -->
|
|
336
|
+
<section
|
|
337
|
+
class="mx-auto max-w-[90rem] px-4 pb-12 sm:pb-16 lg:px-8"
|
|
338
|
+
aria-labelledby="duxt-demo"
|
|
339
|
+
>
|
|
340
|
+
<h2 id="duxt-demo" class="sr-only">
|
|
341
|
+
{{ $t('duxt.defaults.landing.demoTitle') }}
|
|
342
|
+
</h2>
|
|
343
|
+
|
|
344
|
+
<DuxtLiveWindow
|
|
345
|
+
:tabs="demoTabs"
|
|
346
|
+
:height="demoHeight"
|
|
347
|
+
:poster="poster?.src"
|
|
348
|
+
:poster-dark="poster?.srcDark"
|
|
349
|
+
:poster-alt="poster?.alt"
|
|
350
|
+
:live="poster?.live ?? true"
|
|
351
|
+
prefetch
|
|
352
|
+
/>
|
|
353
|
+
</section>
|
|
354
|
+
|
|
355
|
+
<!-- THE BANDS. Separated by their own spacing rather than by a rule: a
|
|
356
|
+
line across the page between two halves of one argument cuts the
|
|
357
|
+
argument up. -->
|
|
358
|
+
<DuxtLandingShowcase
|
|
359
|
+
v-for="band in bands"
|
|
360
|
+
:key="band.showcase.title"
|
|
361
|
+
:showcase="band.showcase"
|
|
362
|
+
:index="band.index"
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
<section
|
|
366
|
+
v-if="landing?.features?.length"
|
|
367
|
+
class="mx-auto max-w-[90rem] px-4 py-12 sm:py-16 lg:px-8"
|
|
368
|
+
aria-labelledby="duxt-features"
|
|
369
|
+
>
|
|
370
|
+
<!-- The cards are h3s and the hero above them is the h1: without a
|
|
371
|
+
heading here the document skips a level, which is what a screen
|
|
372
|
+
reader's heading list reads as a missing section. -->
|
|
373
|
+
<h2 id="duxt-features" class="sr-only">
|
|
374
|
+
{{ $t('duxt.defaults.landing.featuresTitle') }}
|
|
375
|
+
</h2>
|
|
376
|
+
|
|
377
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
378
|
+
<!-- A card with a `to` is a LINK, not a card containing one: a reader
|
|
379
|
+
aims at the card, and a hit area that is only the title is a hit
|
|
380
|
+
area that gets missed. -->
|
|
381
|
+
<component
|
|
382
|
+
:is="feature.to ? linkComponent : 'div'"
|
|
383
|
+
v-for="feature in landing.features"
|
|
384
|
+
:key="feature.title"
|
|
385
|
+
:to="feature.to ? localeLink(feature.to) : undefined"
|
|
386
|
+
:target="feature.external ? '_blank' : undefined"
|
|
387
|
+
:rel="feature.external ? 'noopener' : undefined"
|
|
388
|
+
class="group"
|
|
389
|
+
>
|
|
390
|
+
<UiCard
|
|
391
|
+
class="h-full transition-colors"
|
|
392
|
+
:class="
|
|
393
|
+
feature.to
|
|
394
|
+
? 'group-hover:border-foreground/20 group-hover:bg-muted/30'
|
|
395
|
+
: ''
|
|
396
|
+
"
|
|
397
|
+
>
|
|
398
|
+
<UiCardHeader>
|
|
399
|
+
<span
|
|
400
|
+
v-if="feature.icon"
|
|
401
|
+
class="mb-1 flex size-9 items-center justify-center rounded-lg border bg-muted/50"
|
|
402
|
+
>
|
|
403
|
+
<Icon :name="feature.icon" class="size-4.5 text-primary" />
|
|
404
|
+
</span>
|
|
405
|
+
|
|
406
|
+
<UiCardTitle class="flex items-center gap-1.5 text-base">
|
|
407
|
+
{{ feature.title }}
|
|
408
|
+
<Icon
|
|
409
|
+
v-if="feature.to"
|
|
410
|
+
:name="
|
|
411
|
+
feature.external
|
|
412
|
+
? 'lucide:arrow-up-right'
|
|
413
|
+
: 'lucide:arrow-right'
|
|
414
|
+
"
|
|
415
|
+
class="size-3.5 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
|
416
|
+
/>
|
|
417
|
+
</UiCardTitle>
|
|
418
|
+
|
|
419
|
+
<UiCardDescription class="text-pretty">
|
|
420
|
+
{{ feature.description }}
|
|
421
|
+
</UiCardDescription>
|
|
422
|
+
</UiCardHeader>
|
|
423
|
+
</UiCard>
|
|
424
|
+
</component>
|
|
425
|
+
</div>
|
|
426
|
+
</section>
|
|
427
|
+
|
|
428
|
+
<!-- THE REST. One line each, no cards and no links: these are real and
|
|
429
|
+
worth naming, and a card apiece would say they matter as much as the
|
|
430
|
+
four above.
|
|
431
|
+
|
|
432
|
+
A <ul>, and the entries are paragraphs rather than headings: this is a
|
|
433
|
+
list of things, not a part of the document with sections under it — and
|
|
434
|
+
where the site names no heading for it, an outline entry per item would
|
|
435
|
+
be six sections a reader cannot navigate to. -->
|
|
436
|
+
<!-- `duxt-flush-footer`: the tint runs into the footer instead of stopping
|
|
437
|
+
four rems above it — see the rule in `duxt.css`. -->
|
|
438
|
+
<section
|
|
439
|
+
v-if="landing?.highlights?.length"
|
|
440
|
+
class="duxt-flush-footer border-t bg-muted/20"
|
|
441
|
+
:aria-labelledby="landing.highlightsTitle ? 'duxt-highlights' : undefined"
|
|
442
|
+
>
|
|
443
|
+
<div class="mx-auto max-w-[90rem] px-4 py-12 sm:py-16 lg:px-8">
|
|
444
|
+
<h2
|
|
445
|
+
v-if="landing.highlightsTitle"
|
|
446
|
+
id="duxt-highlights"
|
|
447
|
+
class="text-center text-2xl font-semibold tracking-tight"
|
|
448
|
+
>
|
|
449
|
+
{{ landing.highlightsTitle }}
|
|
450
|
+
</h2>
|
|
451
|
+
|
|
452
|
+
<ul
|
|
453
|
+
class="grid gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-3"
|
|
454
|
+
:class="landing.highlightsTitle ? 'mt-10' : ''"
|
|
455
|
+
>
|
|
456
|
+
<li
|
|
457
|
+
v-for="item in landing.highlights"
|
|
458
|
+
:key="item.title"
|
|
459
|
+
class="flex items-start gap-3"
|
|
460
|
+
>
|
|
461
|
+
<Icon
|
|
462
|
+
:name="item.icon ?? 'lucide:check'"
|
|
463
|
+
class="mt-0.5 size-4.5 shrink-0 text-primary"
|
|
464
|
+
/>
|
|
465
|
+
<div>
|
|
466
|
+
<p class="text-sm font-medium">{{ item.title }}</p>
|
|
467
|
+
<p
|
|
468
|
+
v-if="item.description"
|
|
469
|
+
class="mt-1 text-sm text-muted-foreground text-pretty"
|
|
470
|
+
>
|
|
471
|
+
{{ item.description }}
|
|
472
|
+
</p>
|
|
473
|
+
</div>
|
|
474
|
+
</li>
|
|
475
|
+
</ul>
|
|
476
|
+
</div>
|
|
477
|
+
</section>
|
|
478
|
+
</div>
|
|
479
|
+
</template>
|