@lupinum/ginko-docs 0.2.2 → 0.2.5
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/README.md +130 -73
- package/app/app.config.ts +15 -13
- package/app/app.vue +4 -2
- package/app/assets/css/prose.css +2203 -149
- package/app/assets/css/tailwind.css +60 -38
- package/app/assets/css/theme-palettes.css +309 -0
- package/app/components/content/DocumentPageShell.vue +1 -1
- package/app/components/content/Feedback.vue +12 -11
- package/app/components/content/ImageZoomDialog.vue +84 -0
- package/app/components/content/PageMarkdownCopy.vue +2 -0
- package/app/components/content/imageZoom.ts +25 -0
- package/app/components/mdc/MdcAccordion.vue +6 -3
- package/app/components/mdc/MdcAccordionItem.vue +7 -16
- package/app/components/mdc/MdcApi.vue +159 -0
- package/app/components/mdc/MdcAside.vue +6 -10
- package/app/components/mdc/MdcCallout.vue +9 -10
- package/app/components/mdc/MdcCard.vue +36 -47
- package/app/components/mdc/MdcCards.vue +30 -0
- package/app/components/mdc/MdcCodeGroup.vue +4 -0
- package/app/components/mdc/MdcCodeTree.vue +234 -0
- package/app/components/mdc/MdcCollapse.vue +61 -0
- package/app/components/mdc/MdcError.vue +8 -1
- package/app/components/mdc/MdcExcerpt.vue +26 -0
- package/app/components/mdc/MdcFigure.vue +54 -20
- package/app/components/mdc/MdcFiles.vue +120 -0
- package/app/components/mdc/MdcIdea.vue +2 -1
- package/app/components/mdc/MdcInfo.vue +2 -1
- package/app/components/mdc/MdcInlineToc.vue +51 -0
- package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
- package/app/components/mdc/MdcNote.vue +2 -1
- package/app/components/mdc/MdcQuiz.vue +43 -83
- package/app/components/mdc/MdcQuizQuestion.vue +104 -109
- package/app/components/mdc/MdcReadMore.vue +57 -77
- package/app/components/mdc/MdcSteps.vue +45 -11
- package/app/components/mdc/MdcSuccess.vue +8 -1
- package/app/components/mdc/MdcTabs.vue +44 -26
- package/app/components/mdc/MdcTimeline.vue +4 -1
- package/app/components/mdc/MdcTimelineItem.vue +18 -27
- package/app/components/mdc/MdcWarning.vue +8 -1
- package/app/components/mdc/api.utils.ts +132 -0
- package/app/components/mdc/code-tree.utils.ts +22 -0
- package/app/components/mdc/quiz.utils.ts +38 -0
- package/app/components/prose/ProseBlockquote.vue +17 -1
- package/app/components/prose/ProseH1.vue +1 -1
- package/app/components/prose/ProseH2.vue +7 -2
- package/app/components/prose/ProseH3.vue +7 -2
- package/app/components/prose/ProseH4.vue +17 -2
- package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
- package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
- package/app/components/prose/ProseImg.vue +34 -99
- package/app/components/prose/ProsePre.vue +63 -100
- package/app/components/prose/ProseTable.vue +5 -3
- package/app/components/site/ModeToggle.vue +12 -3
- package/app/components/site/SiteBanner.vue +43 -2
- package/app/components/site/SiteFooter.vue +3 -5
- package/app/components/site/SiteHeader.vue +266 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +25 -9
- package/app/components/site/SiteSocialLinks.vue +31 -0
- package/app/components/ui/badge/Badge.vue +1 -1
- package/app/components/ui/button/Button.vue +1 -1
- package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
- package/app/components/ui/dropdown-menu/index.ts +0 -9
- package/app/components/ui/kbd/Kbd.vue +1 -1
- package/app/components/ui/kbd/index.ts +0 -1
- package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/separator/Separator.vue +1 -1
- package/app/components/ui/sheet/SheetContent.vue +1 -1
- package/app/components/ui/sheet/SheetDescription.vue +1 -1
- package/app/components/ui/sheet/SheetOverlay.vue +1 -1
- package/app/components/ui/sheet/SheetTitle.vue +1 -1
- package/app/components/ui/sheet/index.ts +0 -3
- package/app/components/ui/tabs/Tabs.vue +1 -1
- package/app/components/ui/tabs/TabsList.vue +1 -1
- package/app/components/ui/tabs/TabsTrigger.vue +1 -1
- package/app/components/ui/tabs/index.ts +0 -1
- package/app/composables/site-navigation.utils.ts +69 -12
- package/app/composables/useContentRouteAlternates.ts +13 -21
- package/app/composables/useLocalizedRouteSwitch.ts +1 -1
- package/app/composables/useMetaKey.ts +18 -0
- package/app/composables/useProseAppearance.ts +24 -0
- package/app/composables/useSiteNavigation.ts +10 -40
- package/app/config/site.utils.ts +1 -1
- package/app/error.vue +1 -5
- package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
- package/app/features/docs/components/DocsContributeLinks.vue +28 -18
- package/app/features/docs/components/DocsMobileToc.vue +12 -8
- package/app/features/docs/components/DocsPageContent.vue +93 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +37 -21
- package/app/features/docs/components/DocsSidebarItem.vue +53 -100
- package/app/features/docs/components/DocsSidebarRow.vue +27 -0
- package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
- package/app/features/docs/components/DocsToc.vue +80 -34
- package/app/features/docs/composables/useDocsNavigation.ts +2 -2
- package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
- package/app/features/docs/composables/useRevealActive.ts +58 -0
- package/app/features/docs/composables/useScrollspy.ts +87 -19
- package/app/features/docs/docs-navigation.ts +17 -30
- package/app/features/docs/toc-context.ts +6 -0
- package/app/features/search/command-center.ts +11 -90
- package/app/features/search/components/SiteCommandCenter.vue +58 -38
- package/app/features/search/useCommandCenter.ts +48 -48
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +26 -9
- package/app/pages/blog/index.vue +38 -14
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +180 -61
- package/app/plugins/ginko-docs-theme.ts +12 -0
- package/app/utils/content.ts +11 -0
- package/app/utils/file-icons.ts +90 -0
- package/app/utils/index.ts +7 -1
- package/app/utils/repository.ts +20 -0
- package/content.js +68 -50
- package/content.ts +51 -32
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +4 -0
- package/i18n/messages/global/nav.ts +6 -3
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +9 -56
- package/index.d.ts +7 -1
- package/modules/feature-routing.ts +15 -0
- package/nuxt.config.ts +21 -7
- package/package.json +29 -26
- package/runtime/server/agent-markdown.ts +0 -2
- package/server/api/_ginko-docs/redirects.get.ts +28 -0
- package/server/middleware/redirects.ts +22 -0
- package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
- package/server/routes/blog/rss.xml.get.ts +5 -0
- package/server/utils/blog-feed.ts +49 -0
- package/server/utils/feed.ts +70 -0
- package/server/utils/redirects.ts +38 -0
- package/server/utils/redirects.utils.ts +58 -0
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +75 -10
- package/tags.ts +47 -59
- package/app/components/mdc/MdcAlert.vue +0 -34
- package/app/components/mdc/MdcCardGroup.vue +0 -47
- package/app/components/mdc/MdcDocImg.vue +0 -25
- package/app/components/mdc/MdcField.vue +0 -62
- package/app/components/mdc/MdcFieldGroup.vue +0 -18
- package/app/components/mdc/MdcPassage.vue +0 -32
- package/app/components/mdc/MdcQuizOption.vue +0 -91
- package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
- package/app/components/mdc/MdcShortcut.vue +0 -46
- package/app/components/mdc/MdcStep.vue +0 -42
- package/app/components/ui/accordion/Accordion.vue +0 -15
- package/app/components/ui/accordion/AccordionContent.vue +0 -23
- package/app/components/ui/accordion/AccordionItem.vue +0 -24
- package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
- package/app/components/ui/accordion/index.ts +0 -4
- package/app/components/ui/alert/Alert.vue +0 -21
- package/app/components/ui/alert/AlertDescription.vue +0 -22
- package/app/components/ui/alert/AlertTitle.vue +0 -22
- package/app/components/ui/alert/index.ts +0 -34
- package/app/components/ui/card/Card.vue +0 -16
- package/app/components/ui/card/CardAction.vue +0 -17
- package/app/components/ui/card/CardContent.vue +0 -14
- package/app/components/ui/card/CardDescription.vue +0 -14
- package/app/components/ui/card/CardFooter.vue +0 -14
- package/app/components/ui/card/CardHeader.vue +0 -22
- package/app/components/ui/card/card-variants.ts +0 -15
- package/app/components/ui/card/index.ts +0 -8
- package/app/components/ui/checkbox/Checkbox.vue +0 -35
- package/app/components/ui/checkbox/index.ts +0 -1
- package/app/components/ui/dialog/Dialog.vue +0 -15
- package/app/components/ui/dialog/DialogClose.vue +0 -12
- package/app/components/ui/dialog/DialogContent.vue +0 -69
- package/app/components/ui/dialog/DialogDescription.vue +0 -21
- package/app/components/ui/dialog/DialogFooter.vue +0 -15
- package/app/components/ui/dialog/DialogOverlay.vue +0 -26
- package/app/components/ui/dialog/DialogPortal.vue +0 -12
- package/app/components/ui/dialog/DialogTitle.vue +0 -21
- package/app/components/ui/dialog/DialogTrigger.vue +0 -12
- package/app/components/ui/dialog/index.ts +0 -10
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
- package/app/components/ui/input/Input.vue +0 -23
- package/app/components/ui/input/index.ts +0 -1
- package/app/components/ui/label/Label.vue +0 -14
- package/app/components/ui/label/index.ts +0 -1
- package/app/components/ui/sheet/SheetClose.vue +0 -12
- package/app/components/ui/sheet/SheetFooter.vue +0 -12
- package/app/components/ui/sheet/SheetHeader.vue +0 -12
- package/app/components/ui/switch/Switch.vue +0 -39
- package/app/components/ui/switch/index.ts +0 -1
- package/app/components/ui/tabs/TabsContent.vue +0 -21
- package/app/components/ui/textarea/Textarea.vue +0 -31
- package/app/components/ui/textarea/index.ts +0 -1
- package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
- package/app/lib/utils.ts +0 -7
- package/i18n/messages/source.ts +0 -5
- package/runtime/markdown/inline-code-highlight.ts +0 -155
- package/runtime/utils.ts +0 -7
|
@@ -1,79 +1,126 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useScrollLock, onKeyStroke } from "@vueuse/core";
|
|
3
2
|
import { Button } from "#ginko-docs/components/ui/button";
|
|
4
3
|
import { Kbd } from "#ginko-docs/components/ui/kbd";
|
|
5
4
|
import { Separator } from "#ginko-docs/components/ui/separator";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Sheet,
|
|
7
|
+
SheetContent,
|
|
8
|
+
SheetDescription,
|
|
9
|
+
SheetTitle,
|
|
10
|
+
SheetTrigger,
|
|
11
|
+
} from "#ginko-docs/components/ui/sheet";
|
|
12
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
13
|
+
import {
|
|
14
|
+
findFirstNavigationPage,
|
|
15
|
+
navigationItemContainsPath,
|
|
16
|
+
normalizeNavigationPath,
|
|
17
|
+
} from "@lupinum/ginko-content/navigation";
|
|
7
18
|
import { useI18n, useRoute } from "#imports";
|
|
8
19
|
import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
|
|
9
20
|
import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
|
|
10
21
|
import { useCommandCenterState } from "#ginko-docs/features/search/useCommandCenter";
|
|
22
|
+
import { useMetaKey } from "#ginko-docs/composables/useMetaKey";
|
|
23
|
+
import { useDocsNavigationData } from "#ginko-docs/features/docs/composables/useDocsNavigationData";
|
|
24
|
+
import {
|
|
25
|
+
getDocsNavigationSections,
|
|
26
|
+
normalizeDocsNavigationItem,
|
|
27
|
+
} from "#ginko-docs/features/docs/docs-navigation";
|
|
11
28
|
import ModeToggle from "#ginko-docs/components/site/ModeToggle.vue";
|
|
29
|
+
import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
|
|
12
30
|
|
|
13
|
-
const { mainNav } = useSiteNavigation();
|
|
31
|
+
const { mainNav, socialLinks } = useSiteNavigation();
|
|
14
32
|
const { openCommandCenter } = useCommandCenterState();
|
|
33
|
+
const config = useGinkoDocsConfig();
|
|
15
34
|
const { t } = useI18n();
|
|
16
35
|
const route = useRoute();
|
|
17
36
|
const isMobileMenuOpen = ref(false);
|
|
18
|
-
const headerElement = ref<HTMLElement | null>(null);
|
|
19
|
-
const mobileMenuTop = ref("var(--site-header-height)");
|
|
20
37
|
const localizedPath = useLocalizedPath();
|
|
21
38
|
|
|
22
39
|
const isActive = (href: string) => route.path === href || route.path.startsWith(href + "/");
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
const metaKey = useMetaKey();
|
|
42
|
+
const searchShortcut = computed(() => (metaKey.value === "⌘" ? "⌘K" : "Ctrl K"));
|
|
43
|
+
const homePath = computed(() => localizedPath("home"));
|
|
44
|
+
const docsPath = computed(() => localizedPath("docs"));
|
|
45
|
+
|
|
46
|
+
// The mobile menu has two levels: the root nav in hero-scale type, and a docs
|
|
47
|
+
// level with the section entry links. The docs nav link drills in; every other
|
|
48
|
+
// link navigates directly.
|
|
49
|
+
const { data: docsNavData } = useDocsNavigationData();
|
|
50
|
+
const docsSections = computed(() =>
|
|
51
|
+
getDocsNavigationSections(
|
|
52
|
+
(docsNavData.value ?? []).map((item, index) => normalizeDocsNavigationItem(item, index)),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const docsMenuSections = computed(() => {
|
|
56
|
+
const currentPath = normalizeNavigationPath(route.path);
|
|
57
|
+
|
|
58
|
+
return docsSections.value
|
|
59
|
+
.map((section) => ({
|
|
60
|
+
id: section.id,
|
|
61
|
+
title: section.title,
|
|
62
|
+
links: section.items
|
|
63
|
+
.map((item) => ({
|
|
64
|
+
id: item.id,
|
|
65
|
+
label: item.title,
|
|
66
|
+
href: item.path ?? findFirstNavigationPage(item.children)?.path,
|
|
67
|
+
active: navigationItemContainsPath(item, currentPath),
|
|
68
|
+
}))
|
|
69
|
+
.filter((link): link is typeof link & { href: string } => Boolean(link.href)),
|
|
70
|
+
}))
|
|
71
|
+
.filter((section) => section.links.length > 0);
|
|
72
|
+
});
|
|
73
|
+
const hasDocsLevel = computed(() => docsMenuSections.value.length > 0);
|
|
29
74
|
|
|
30
|
-
const
|
|
75
|
+
const rootNavItems = computed(() =>
|
|
31
76
|
mainNav.value.map((item) => ({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
icon: mobileNavIcon(item.href),
|
|
35
|
-
children: [] as { label: string; href: string }[],
|
|
77
|
+
...item,
|
|
78
|
+
drill: hasDocsLevel.value && item.href === docsPath.value,
|
|
36
79
|
})),
|
|
37
80
|
);
|
|
38
|
-
const
|
|
81
|
+
const docsLevelTitle = computed(
|
|
82
|
+
() => rootNavItems.value.find((item) => item.drill)?.label ?? t("nav.documentation"),
|
|
83
|
+
);
|
|
39
84
|
|
|
40
|
-
|
|
41
|
-
const
|
|
85
|
+
type MenuLevel = "root" | "docs";
|
|
86
|
+
const menuLevel = ref<MenuLevel>("root");
|
|
87
|
+
const docsDrillButton = ref<HTMLButtonElement | null>(null);
|
|
88
|
+
const docsBackButton = ref<HTMLButtonElement | null>(null);
|
|
42
89
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
void nextTick(updateMobileMenuTop);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
90
|
+
function setDrillButtonRef(el: unknown) {
|
|
91
|
+
docsDrillButton.value = el instanceof HTMLButtonElement ? el : null;
|
|
92
|
+
}
|
|
49
93
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
async function drillIntoDocs() {
|
|
95
|
+
menuLevel.value = "docs";
|
|
96
|
+
await nextTick();
|
|
97
|
+
docsBackButton.value?.focus({ preventScroll: true });
|
|
53
98
|
}
|
|
54
99
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
100
|
+
async function backToRoot() {
|
|
101
|
+
menuLevel.value = "root";
|
|
102
|
+
await nextTick();
|
|
103
|
+
docsDrillButton.value?.focus({ preventScroll: true });
|
|
59
104
|
}
|
|
60
105
|
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
function closeMenu() {
|
|
107
|
+
isMobileMenuOpen.value = false;
|
|
108
|
+
}
|
|
63
109
|
|
|
110
|
+
// Scroll lock, Escape handling, and focus trapping come from the Sheet
|
|
111
|
+
// (reka-ui Dialog); only route-driven closing is ours.
|
|
64
112
|
function openSearch() {
|
|
65
113
|
isMobileMenuOpen.value = false;
|
|
66
114
|
openCommandCenter();
|
|
67
115
|
}
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
watch(isMobileMenuOpen, (open) => {
|
|
118
|
+
if (open) {
|
|
119
|
+
// Context-aware entry: docs readers land on their section links first.
|
|
120
|
+
menuLevel.value = hasDocsLevel.value && isActive(docsPath.value) ? "docs" : "root";
|
|
73
121
|
}
|
|
74
122
|
});
|
|
75
123
|
|
|
76
|
-
// Close menu on navigation
|
|
77
124
|
watch(
|
|
78
125
|
() => route.path,
|
|
79
126
|
() => {
|
|
@@ -84,7 +131,6 @@ watch(
|
|
|
84
131
|
|
|
85
132
|
<template>
|
|
86
133
|
<header
|
|
87
|
-
ref="headerElement"
|
|
88
134
|
class="sticky top-0 z-50 flex h-14 w-full shrink-0 items-center border-b border-border bg-background/90 backdrop-blur-md"
|
|
89
135
|
>
|
|
90
136
|
<div class="mx-auto flex w-full max-w-screen-2xl flex-1 items-center gap-4 px-4 md:px-6">
|
|
@@ -120,7 +166,7 @@ watch(
|
|
|
120
166
|
>
|
|
121
167
|
<Icon name="lucide:search" class="size-4 shrink-0" aria-hidden="true" />
|
|
122
168
|
<span class="flex-1 text-left">{{ t("nav.search") }}...</span>
|
|
123
|
-
<Kbd class="h-5 bg-background text-[10px]"
|
|
169
|
+
<Kbd class="h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
|
|
124
170
|
</Button>
|
|
125
171
|
<Button
|
|
126
172
|
variant="ghost"
|
|
@@ -148,103 +194,191 @@ watch(
|
|
|
148
194
|
</template>
|
|
149
195
|
</ClientOnly>
|
|
150
196
|
|
|
197
|
+
<div v-if="config.nav.socialIcons" class="hidden md:block">
|
|
198
|
+
<SiteSocialLinks />
|
|
199
|
+
</div>
|
|
200
|
+
|
|
151
201
|
<SiteLocaleSwitcher class="hidden md:flex" />
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
@click="isMobileMenuOpen = !isMobileMenuOpen"
|
|
160
|
-
>
|
|
161
|
-
<Icon
|
|
162
|
-
:name="isMobileMenuOpen ? 'lucide:x' : 'lucide:menu'"
|
|
163
|
-
class="size-5"
|
|
164
|
-
aria-hidden="true"
|
|
165
|
-
/>
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</header>
|
|
170
|
-
<Transition
|
|
171
|
-
enter-active-class="transition-all duration-200 ease-out"
|
|
172
|
-
enter-from-class="-translate-y-4 opacity-0"
|
|
173
|
-
enter-to-class="translate-y-0 opacity-100"
|
|
174
|
-
leave-active-class="transition-all duration-200 ease-in"
|
|
175
|
-
leave-from-class="translate-y-0 opacity-100"
|
|
176
|
-
leave-to-class="-translate-y-4 opacity-0"
|
|
177
|
-
>
|
|
178
|
-
<div
|
|
179
|
-
v-if="isMobileMenuOpen"
|
|
180
|
-
class="fixed inset-x-0 bottom-0 z-40 [scrollbar-width:none] overflow-y-auto bg-background/95 backdrop-blur-md md:hidden [&::-webkit-scrollbar]:hidden"
|
|
181
|
-
:style="{ top: mobileMenuTop }"
|
|
182
|
-
>
|
|
183
|
-
<div class="px-5 pt-9 pb-12">
|
|
184
|
-
<nav v-if="mobileNavItems.length" class="space-y-4" :aria-label="t('nav.mobile')">
|
|
185
|
-
<p class="px-3 text-xs font-bold tracking-[0.16em] text-muted-foreground uppercase">
|
|
186
|
-
{{ t("nav.navigationSection") }}
|
|
187
|
-
</p>
|
|
188
|
-
<div class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
|
189
|
-
<NuxtLink
|
|
190
|
-
v-for="(group, index) in mobileNavItems"
|
|
191
|
-
:key="group.href"
|
|
192
|
-
:to="group.href"
|
|
193
|
-
class="flex h-[4.25rem] items-center gap-4 px-5 text-base font-semibold transition-colors hover:bg-muted/50"
|
|
194
|
-
:class="[
|
|
195
|
-
isActive(group.href) ? 'text-primary' : 'text-foreground',
|
|
196
|
-
index > 0 && 'border-t border-border',
|
|
197
|
-
]"
|
|
198
|
-
@click="isMobileMenuOpen = false"
|
|
199
|
-
>
|
|
200
|
-
<span
|
|
201
|
-
class="flex size-11 shrink-0 items-center justify-center rounded-lg bg-muted/60 text-foreground"
|
|
202
|
-
>
|
|
203
|
-
<Icon :name="group.icon" class="size-5" aria-hidden="true" />
|
|
204
|
-
</span>
|
|
205
|
-
<span class="min-w-0 flex-1 truncate">{{ group.label }}</span>
|
|
206
|
-
<Icon
|
|
207
|
-
name="lucide:chevron-right"
|
|
208
|
-
class="size-5 shrink-0 text-muted-foreground"
|
|
209
|
-
aria-hidden="true"
|
|
210
|
-
/>
|
|
211
|
-
</NuxtLink>
|
|
212
|
-
</div>
|
|
213
|
-
</nav>
|
|
214
|
-
|
|
215
|
-
<div class="space-y-4" :class="mobileNavItems.length ? 'mt-9' : ''">
|
|
216
|
-
<p class="px-3 text-xs font-bold tracking-[0.16em] text-muted-foreground uppercase">
|
|
217
|
-
{{ t("nav.settingsHelp") }}
|
|
218
|
-
</p>
|
|
219
|
-
<div class="overflow-hidden rounded-xl border border-border bg-background shadow-xs">
|
|
220
|
-
<button
|
|
202
|
+
<Sheet v-model:open="isMobileMenuOpen">
|
|
203
|
+
<SheetTrigger as-child>
|
|
204
|
+
<Button
|
|
205
|
+
variant="ghost"
|
|
206
|
+
size="icon"
|
|
207
|
+
class="md:hidden"
|
|
208
|
+
:aria-label="t('nav.openMenu')"
|
|
221
209
|
type="button"
|
|
222
|
-
class="flex h-[4.25rem] w-full items-center gap-4 px-5 text-start text-base font-semibold transition-colors hover:bg-muted/50"
|
|
223
|
-
@click="openSearch"
|
|
224
210
|
>
|
|
225
|
-
<
|
|
226
|
-
|
|
211
|
+
<Icon name="lucide:menu" class="size-5" aria-hidden="true" />
|
|
212
|
+
</Button>
|
|
213
|
+
</SheetTrigger>
|
|
214
|
+
<SheetContent
|
|
215
|
+
side="top"
|
|
216
|
+
class="inset-0 h-dvh gap-0 overflow-hidden border-b-0 bg-background/95 p-0 backdrop-blur-md md:hidden"
|
|
217
|
+
>
|
|
218
|
+
<SheetTitle class="sr-only">{{ t("nav.mobile") }}</SheetTitle>
|
|
219
|
+
<SheetDescription class="sr-only">{{ t("nav.main") }}</SheetDescription>
|
|
220
|
+
<div class="flex h-full min-h-0 flex-col pt-16">
|
|
221
|
+
<div
|
|
222
|
+
class="flex min-h-0 w-[200%] flex-1 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none"
|
|
223
|
+
:class="menuLevel === 'docs' ? '-translate-x-1/2' : ''"
|
|
227
224
|
>
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
<nav
|
|
226
|
+
class="flex min-h-0 w-1/2 flex-col overflow-x-hidden overflow-y-auto px-6"
|
|
227
|
+
:aria-label="t('nav.mobile')"
|
|
228
|
+
:inert="menuLevel !== 'root'"
|
|
229
|
+
>
|
|
230
|
+
<div class="m-auto w-full space-y-8 py-6">
|
|
231
|
+
<template v-for="item in rootNavItems" :key="item.href">
|
|
232
|
+
<button
|
|
233
|
+
v-if="item.drill"
|
|
234
|
+
:ref="setDrillButtonRef"
|
|
235
|
+
type="button"
|
|
236
|
+
class="group block w-full text-start"
|
|
237
|
+
@click="drillIntoDocs"
|
|
238
|
+
>
|
|
239
|
+
<span
|
|
240
|
+
class="flex items-center gap-2.5 text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
|
|
241
|
+
:class="
|
|
242
|
+
isActive(item.href)
|
|
243
|
+
? 'text-foreground'
|
|
244
|
+
: 'text-foreground/70 transition-colors group-hover:text-foreground'
|
|
245
|
+
"
|
|
246
|
+
>
|
|
247
|
+
{{ item.label }}
|
|
248
|
+
<Icon
|
|
249
|
+
name="lucide:chevron-right"
|
|
250
|
+
class="size-6 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
/>
|
|
253
|
+
</span>
|
|
254
|
+
<span
|
|
255
|
+
v-if="item.description"
|
|
256
|
+
class="mt-1.5 block text-sm text-muted-foreground"
|
|
257
|
+
>
|
|
258
|
+
{{ item.description }}
|
|
259
|
+
</span>
|
|
260
|
+
</button>
|
|
261
|
+
<NuxtLink v-else :to="item.href" class="group block" @click="closeMenu">
|
|
262
|
+
<span
|
|
263
|
+
class="block text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
|
|
264
|
+
:class="
|
|
265
|
+
isActive(item.href)
|
|
266
|
+
? 'text-foreground'
|
|
267
|
+
: 'text-foreground/70 transition-colors group-hover:text-foreground'
|
|
268
|
+
"
|
|
269
|
+
>
|
|
270
|
+
{{ item.label }}
|
|
271
|
+
</span>
|
|
272
|
+
<span
|
|
273
|
+
v-if="item.description"
|
|
274
|
+
class="mt-1.5 block text-sm text-muted-foreground"
|
|
275
|
+
>
|
|
276
|
+
{{ item.description }}
|
|
277
|
+
</span>
|
|
278
|
+
</NuxtLink>
|
|
279
|
+
</template>
|
|
280
|
+
</div>
|
|
281
|
+
</nav>
|
|
282
|
+
|
|
283
|
+
<nav
|
|
284
|
+
v-if="hasDocsLevel"
|
|
285
|
+
class="flex min-h-0 w-1/2 flex-col px-6"
|
|
286
|
+
:aria-label="docsLevelTitle"
|
|
287
|
+
:inert="menuLevel !== 'docs'"
|
|
288
|
+
>
|
|
289
|
+
<button
|
|
290
|
+
ref="docsBackButton"
|
|
291
|
+
type="button"
|
|
292
|
+
class="flex items-center gap-1 self-start py-2 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
|
|
293
|
+
@click="backToRoot"
|
|
294
|
+
>
|
|
295
|
+
<Icon name="lucide:chevron-left" class="size-4" aria-hidden="true" />
|
|
296
|
+
{{ t("nav.menu") }}
|
|
297
|
+
</button>
|
|
298
|
+
<p
|
|
299
|
+
class="mt-1 text-[2rem] leading-[1.1] font-semibold tracking-[-0.03em] text-foreground"
|
|
300
|
+
>
|
|
301
|
+
{{ docsLevelTitle }}
|
|
302
|
+
</p>
|
|
303
|
+
<div class="-mx-2 mt-3 min-h-0 flex-1 overflow-x-hidden overflow-y-auto pb-4">
|
|
304
|
+
<template v-for="section in docsMenuSections" :key="section.id">
|
|
305
|
+
<p
|
|
306
|
+
v-if="section.title"
|
|
307
|
+
class="mt-6 mb-1 px-2 text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase first:mt-2"
|
|
308
|
+
>
|
|
309
|
+
{{ section.title }}
|
|
310
|
+
</p>
|
|
311
|
+
<NuxtLink
|
|
312
|
+
v-for="link in section.links"
|
|
313
|
+
:key="link.id"
|
|
314
|
+
:to="link.href"
|
|
315
|
+
class="flex items-center rounded-md px-2 py-2 text-lg font-semibold transition-colors"
|
|
316
|
+
:class="
|
|
317
|
+
link.active
|
|
318
|
+
? 'bg-primary/10 text-primary'
|
|
319
|
+
: 'text-foreground hover:bg-muted/60'
|
|
320
|
+
"
|
|
321
|
+
:aria-current="link.active ? 'true' : undefined"
|
|
322
|
+
@click="closeMenu"
|
|
323
|
+
>
|
|
324
|
+
{{ link.label }}
|
|
325
|
+
<span
|
|
326
|
+
v-if="link.active"
|
|
327
|
+
class="mr-1 ml-auto size-1.5 rounded-full bg-primary"
|
|
328
|
+
aria-hidden="true"
|
|
329
|
+
/>
|
|
330
|
+
</NuxtLink>
|
|
331
|
+
</template>
|
|
332
|
+
</div>
|
|
333
|
+
</nav>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="shrink-0 space-y-3.5 px-6 pt-4 pb-8">
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
class="flex h-11 w-full items-center gap-2.5 rounded-md border border-border bg-muted/40 px-4 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted"
|
|
340
|
+
@click="openSearch"
|
|
341
|
+
>
|
|
342
|
+
<Icon name="lucide:search" class="size-4" aria-hidden="true" />
|
|
343
|
+
{{ t("nav.search") }}...
|
|
344
|
+
<Kbd class="ml-auto h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
|
|
345
|
+
</button>
|
|
346
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
347
|
+
<ClientOnly>
|
|
348
|
+
<ModeToggle variant="pill" />
|
|
349
|
+
<template #fallback>
|
|
350
|
+
<span
|
|
351
|
+
class="h-9 w-24 rounded-full border border-border bg-background"
|
|
352
|
+
aria-hidden="true"
|
|
353
|
+
/>
|
|
354
|
+
</template>
|
|
355
|
+
</ClientOnly>
|
|
356
|
+
<SiteLocaleSwitcher variant="pill" class="flex" @navigate="closeMenu" />
|
|
357
|
+
<SiteSocialLinks
|
|
358
|
+
v-if="config.nav.socialIcons"
|
|
359
|
+
class="ml-auto"
|
|
360
|
+
@navigate="closeMenu"
|
|
361
|
+
/>
|
|
362
|
+
<span v-else-if="socialLinks.length" class="ml-auto flex items-center gap-2">
|
|
363
|
+
<NuxtLink
|
|
364
|
+
v-for="link in socialLinks"
|
|
365
|
+
:key="link.href"
|
|
366
|
+
:to="link.href"
|
|
367
|
+
target="_blank"
|
|
368
|
+
rel="noopener noreferrer"
|
|
369
|
+
class="inline-flex h-9 items-center gap-2 rounded-full border border-border px-4 text-sm font-semibold text-foreground transition-colors hover:bg-muted"
|
|
370
|
+
@click="closeMenu"
|
|
371
|
+
>
|
|
372
|
+
<Icon v-if="link.icon" :name="link.icon" class="size-4" aria-hidden="true" />
|
|
373
|
+
{{ link.label }}
|
|
374
|
+
</NuxtLink>
|
|
375
|
+
</span>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
244
378
|
</div>
|
|
245
|
-
</
|
|
246
|
-
</
|
|
379
|
+
</SheetContent>
|
|
380
|
+
</Sheet>
|
|
247
381
|
</div>
|
|
248
382
|
</div>
|
|
249
|
-
</
|
|
383
|
+
</header>
|
|
250
384
|
</template>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, useId } from "vue";
|
|
3
|
+
import { useAsyncData, useI18n } from "#imports";
|
|
4
|
+
import { cn } from "../../utils";
|
|
5
|
+
import { filenameExtension, resolveFileIcon } from "../../utils/file-icons";
|
|
6
|
+
|
|
7
|
+
export type SiteHeroCodeTab = {
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
filename?: string;
|
|
11
|
+
language?: string;
|
|
12
|
+
code: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{ tabs: SiteHeroCodeTab[] }>();
|
|
16
|
+
|
|
17
|
+
const { t } = useI18n();
|
|
18
|
+
const regionId = useId();
|
|
19
|
+
const activeIndex = ref(0);
|
|
20
|
+
|
|
21
|
+
const activeTab = computed(() => props.tabs[activeIndex.value] ?? props.tabs[0]);
|
|
22
|
+
|
|
23
|
+
function tabIcon(tab: SiteHeroCodeTab): string {
|
|
24
|
+
return tab.icon ?? resolveFileIcon(tab.filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HTML_ESCAPES: Record<string, string> = { "&": "&", "<": "<", ">": ">" };
|
|
28
|
+
|
|
29
|
+
function escapeHtml(input: string): string {
|
|
30
|
+
return input.replace(/[&<>]/g, (char) => HTML_ESCAPES[char] ?? char);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function plainLines(code: string): string {
|
|
34
|
+
return code
|
|
35
|
+
.split("\n")
|
|
36
|
+
.map((line) => `<span class="line">${escapeHtml(line)}</span>`)
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Config code arrives as raw strings, so it skips the build-time content
|
|
41
|
+
// pipeline; highlight here with the same Shiki themes the docs use.
|
|
42
|
+
const { data: highlighted } = await useAsyncData(`site-hero-code-${regionId}`, async () => {
|
|
43
|
+
const { codeToHast, hastToHtml } = await import("shiki");
|
|
44
|
+
|
|
45
|
+
return Promise.all(
|
|
46
|
+
props.tabs.map(async (tab) => {
|
|
47
|
+
const lang = tab.language ?? filenameExtension(tab.filename) ?? "text";
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const root = await codeToHast(tab.code, {
|
|
51
|
+
lang,
|
|
52
|
+
themes: { light: "light-plus", dark: "dark-plus" },
|
|
53
|
+
});
|
|
54
|
+
const pre = root.children.find((node) => node.type === "element");
|
|
55
|
+
const code = pre?.children.find((node) => node.type === "element");
|
|
56
|
+
if (!code) return plainLines(tab.code);
|
|
57
|
+
|
|
58
|
+
return hastToHtml({ type: "root" as const, children: code.children });
|
|
59
|
+
} catch {
|
|
60
|
+
return plainLines(tab.code);
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<div class="site-hero-code content-codegroup not-prose group my-0 shadow-md">
|
|
69
|
+
<div class="content-codegroup-header">
|
|
70
|
+
<div v-if="tabs.length > 1" class="content-codegroup-tabs" role="tablist">
|
|
71
|
+
<button
|
|
72
|
+
v-for="(tab, index) in tabs"
|
|
73
|
+
:key="tab.label"
|
|
74
|
+
type="button"
|
|
75
|
+
role="tab"
|
|
76
|
+
:aria-selected="activeIndex === index ? 'true' : 'false'"
|
|
77
|
+
:class="
|
|
78
|
+
cn('content-codegroup-tab', activeIndex === index && 'content-codegroup-tab-active')
|
|
79
|
+
"
|
|
80
|
+
@click="activeIndex = index"
|
|
81
|
+
>
|
|
82
|
+
<Icon :name="tabIcon(tab)" class="content-codegroup-tab-icon" aria-hidden="true" />
|
|
83
|
+
{{ tab.label }}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
v-else-if="activeTab"
|
|
88
|
+
class="flex min-w-0 items-center gap-2 text-sm font-medium text-muted-foreground"
|
|
89
|
+
>
|
|
90
|
+
<Icon :name="tabIcon(activeTab)" class="content-codegroup-tab-icon" aria-hidden="true" />
|
|
91
|
+
<span class="truncate">{{ activeTab.label }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="content-codegroup-panels grid">
|
|
95
|
+
<div
|
|
96
|
+
v-for="(tab, index) in tabs"
|
|
97
|
+
:key="tab.label"
|
|
98
|
+
role="tabpanel"
|
|
99
|
+
class="col-start-1 row-start-1 min-w-0"
|
|
100
|
+
:class="activeIndex === index ? '' : 'invisible'"
|
|
101
|
+
:inert="activeIndex !== index"
|
|
102
|
+
>
|
|
103
|
+
<figure
|
|
104
|
+
dir="ltr"
|
|
105
|
+
data-fd-codeblock
|
|
106
|
+
class="content-codeblock group shiki not-prose content-codeblock-in-group relative"
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
role="region"
|
|
110
|
+
tabindex="0"
|
|
111
|
+
:aria-label="`${t('docs.codeSample')}: ${tab.label} (${regionId}-${index})`"
|
|
112
|
+
class="fd-scroll-container max-h-[600px] overflow-auto focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset"
|
|
113
|
+
>
|
|
114
|
+
<!-- Shiki output over site-owner config code, never end-user input. -->
|
|
115
|
+
<!-- eslint-disable vue/no-v-html -->
|
|
116
|
+
<pre
|
|
117
|
+
class="w-max min-w-full"
|
|
118
|
+
><code v-html="highlighted?.[index] ?? plainLines(tab.code)" /></pre>
|
|
119
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
120
|
+
</div>
|
|
121
|
+
</figure>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<style scoped>
|
|
128
|
+
/* Hero scale: same chrome as docs code groups, one step larger. */
|
|
129
|
+
.site-hero-code {
|
|
130
|
+
padding: 0.25rem 0.375rem 0.375rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.site-hero-code .content-codegroup-header {
|
|
134
|
+
padding: 0.375rem 0.375rem 0.25rem 0.625rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.site-hero-code .content-codegroup-tabs {
|
|
138
|
+
gap: 0.375rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.site-hero-code .content-codegroup-tab {
|
|
142
|
+
height: 2.125rem;
|
|
143
|
+
gap: 0.5rem;
|
|
144
|
+
padding: 0 0.625rem 0.4375rem;
|
|
145
|
+
font-size: 0.9375rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.site-hero-code .content-codegroup-tab-icon {
|
|
149
|
+
width: 1rem;
|
|
150
|
+
height: 1rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.site-hero-code .content-codeblock pre {
|
|
154
|
+
padding-top: 1.125rem;
|
|
155
|
+
padding-bottom: 1.125rem;
|
|
156
|
+
font-size: 0.875rem;
|
|
157
|
+
line-height: 1.75;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.site-hero-code :deep([data-fd-codeblock] pre code .line) {
|
|
161
|
+
padding-right: 1.5rem;
|
|
162
|
+
padding-left: 1.5rem;
|
|
163
|
+
}
|
|
164
|
+
</style>
|