@lupinum/ginko-docs 0.2.2 → 0.2.3
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 +14 -12
- package/app/app.vue +2 -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 +19 -112
- 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 +255 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +22 -4
- 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 +29 -11
- 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 +3 -18
- 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 +84 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +30 -20
- 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 +46 -46
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +16 -8
- package/app/pages/blog/index.vue +26 -13
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +174 -59
- 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 +23 -26
- package/content.ts +30 -28
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +3 -0
- package/i18n/messages/global/nav.ts +6 -2
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +5 -0
- package/index.d.ts +7 -1
- package/nuxt.config.ts +2 -2
- package/package.json +9 -6
- package/runtime/server/agent-markdown.ts +0 -2
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +63 -6
- 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,124 @@
|
|
|
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";
|
|
12
29
|
|
|
13
|
-
const { mainNav } = useSiteNavigation();
|
|
30
|
+
const { mainNav, socialLinks } = useSiteNavigation();
|
|
14
31
|
const { openCommandCenter } = useCommandCenterState();
|
|
15
32
|
const { t } = useI18n();
|
|
16
33
|
const route = useRoute();
|
|
17
34
|
const isMobileMenuOpen = ref(false);
|
|
18
|
-
const headerElement = ref<HTMLElement | null>(null);
|
|
19
|
-
const mobileMenuTop = ref("var(--site-header-height)");
|
|
20
35
|
const localizedPath = useLocalizedPath();
|
|
21
36
|
|
|
22
37
|
const isActive = (href: string) => route.path === href || route.path.startsWith(href + "/");
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const metaKey = useMetaKey();
|
|
40
|
+
const searchShortcut = computed(() => (metaKey.value === "⌘" ? "⌘K" : "Ctrl K"));
|
|
41
|
+
const homePath = computed(() => localizedPath("home"));
|
|
42
|
+
const docsPath = computed(() => localizedPath("docs"));
|
|
43
|
+
|
|
44
|
+
// The mobile menu has two levels: the root nav in hero-scale type, and a docs
|
|
45
|
+
// level with the section entry links. The docs nav link drills in; every other
|
|
46
|
+
// link navigates directly.
|
|
47
|
+
const { data: docsNavData } = useDocsNavigationData();
|
|
48
|
+
const docsSections = computed(() =>
|
|
49
|
+
getDocsNavigationSections(
|
|
50
|
+
(docsNavData.value ?? []).map((item, index) => normalizeDocsNavigationItem(item, index)),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
const docsMenuSections = computed(() => {
|
|
54
|
+
const currentPath = normalizeNavigationPath(route.path);
|
|
55
|
+
|
|
56
|
+
return docsSections.value
|
|
57
|
+
.map((section) => ({
|
|
58
|
+
id: section.id,
|
|
59
|
+
title: section.title,
|
|
60
|
+
links: section.items
|
|
61
|
+
.map((item) => ({
|
|
62
|
+
id: item.id,
|
|
63
|
+
label: item.title,
|
|
64
|
+
href: item.path ?? findFirstNavigationPage(item.children)?.path,
|
|
65
|
+
active: navigationItemContainsPath(item, currentPath),
|
|
66
|
+
}))
|
|
67
|
+
.filter((link): link is typeof link & { href: string } => Boolean(link.href)),
|
|
68
|
+
}))
|
|
69
|
+
.filter((section) => section.links.length > 0);
|
|
70
|
+
});
|
|
71
|
+
const hasDocsLevel = computed(() => docsMenuSections.value.length > 0);
|
|
29
72
|
|
|
30
|
-
const
|
|
73
|
+
const rootNavItems = computed(() =>
|
|
31
74
|
mainNav.value.map((item) => ({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
icon: mobileNavIcon(item.href),
|
|
35
|
-
children: [] as { label: string; href: string }[],
|
|
75
|
+
...item,
|
|
76
|
+
drill: hasDocsLevel.value && item.href === docsPath.value,
|
|
36
77
|
})),
|
|
37
78
|
);
|
|
38
|
-
const
|
|
79
|
+
const docsLevelTitle = computed(
|
|
80
|
+
() => rootNavItems.value.find((item) => item.drill)?.label ?? t("nav.documentation"),
|
|
81
|
+
);
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
const
|
|
83
|
+
type MenuLevel = "root" | "docs";
|
|
84
|
+
const menuLevel = ref<MenuLevel>("root");
|
|
85
|
+
const docsDrillButton = ref<HTMLButtonElement | null>(null);
|
|
86
|
+
const docsBackButton = ref<HTMLButtonElement | null>(null);
|
|
42
87
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
void nextTick(updateMobileMenuTop);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
88
|
+
function setDrillButtonRef(el: unknown) {
|
|
89
|
+
docsDrillButton.value = el instanceof HTMLButtonElement ? el : null;
|
|
90
|
+
}
|
|
49
91
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
async function drillIntoDocs() {
|
|
93
|
+
menuLevel.value = "docs";
|
|
94
|
+
await nextTick();
|
|
95
|
+
docsBackButton.value?.focus({ preventScroll: true });
|
|
53
96
|
}
|
|
54
97
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
98
|
+
async function backToRoot() {
|
|
99
|
+
menuLevel.value = "root";
|
|
100
|
+
await nextTick();
|
|
101
|
+
docsDrillButton.value?.focus({ preventScroll: true });
|
|
59
102
|
}
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
function closeMenu() {
|
|
105
|
+
isMobileMenuOpen.value = false;
|
|
106
|
+
}
|
|
63
107
|
|
|
108
|
+
// Scroll lock, Escape handling, and focus trapping come from the Sheet
|
|
109
|
+
// (reka-ui Dialog); only route-driven closing is ours.
|
|
64
110
|
function openSearch() {
|
|
65
111
|
isMobileMenuOpen.value = false;
|
|
66
112
|
openCommandCenter();
|
|
67
113
|
}
|
|
68
114
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
115
|
+
watch(isMobileMenuOpen, (open) => {
|
|
116
|
+
if (open) {
|
|
117
|
+
// Context-aware entry: docs readers land on their section links first.
|
|
118
|
+
menuLevel.value = hasDocsLevel.value && isActive(docsPath.value) ? "docs" : "root";
|
|
73
119
|
}
|
|
74
120
|
});
|
|
75
121
|
|
|
76
|
-
// Close menu on navigation
|
|
77
122
|
watch(
|
|
78
123
|
() => route.path,
|
|
79
124
|
() => {
|
|
@@ -84,7 +129,6 @@ watch(
|
|
|
84
129
|
|
|
85
130
|
<template>
|
|
86
131
|
<header
|
|
87
|
-
ref="headerElement"
|
|
88
132
|
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
133
|
>
|
|
90
134
|
<div class="mx-auto flex w-full max-w-screen-2xl flex-1 items-center gap-4 px-4 md:px-6">
|
|
@@ -120,7 +164,7 @@ watch(
|
|
|
120
164
|
>
|
|
121
165
|
<Icon name="lucide:search" class="size-4 shrink-0" aria-hidden="true" />
|
|
122
166
|
<span class="flex-1 text-left">{{ t("nav.search") }}...</span>
|
|
123
|
-
<Kbd class="h-5 bg-background text-[10px]"
|
|
167
|
+
<Kbd class="h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
|
|
124
168
|
</Button>
|
|
125
169
|
<Button
|
|
126
170
|
variant="ghost"
|
|
@@ -149,102 +193,181 @@ watch(
|
|
|
149
193
|
</ClientOnly>
|
|
150
194
|
|
|
151
195
|
<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
|
|
196
|
+
<Sheet v-model:open="isMobileMenuOpen">
|
|
197
|
+
<SheetTrigger as-child>
|
|
198
|
+
<Button
|
|
199
|
+
variant="ghost"
|
|
200
|
+
size="icon"
|
|
201
|
+
class="md:hidden"
|
|
202
|
+
:aria-label="t('nav.openMenu')"
|
|
221
203
|
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
204
|
>
|
|
225
|
-
<
|
|
226
|
-
|
|
205
|
+
<Icon name="lucide:menu" class="size-5" aria-hidden="true" />
|
|
206
|
+
</Button>
|
|
207
|
+
</SheetTrigger>
|
|
208
|
+
<SheetContent
|
|
209
|
+
side="top"
|
|
210
|
+
class="inset-0 h-dvh gap-0 overflow-hidden border-b-0 bg-background/95 p-0 backdrop-blur-md md:hidden"
|
|
211
|
+
>
|
|
212
|
+
<SheetTitle class="sr-only">{{ t("nav.mobile") }}</SheetTitle>
|
|
213
|
+
<SheetDescription class="sr-only">{{ t("nav.main") }}</SheetDescription>
|
|
214
|
+
<div class="flex h-full min-h-0 flex-col pt-16">
|
|
215
|
+
<div
|
|
216
|
+
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"
|
|
217
|
+
:class="menuLevel === 'docs' ? '-translate-x-1/2' : ''"
|
|
227
218
|
>
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
219
|
+
<nav
|
|
220
|
+
class="flex min-h-0 w-1/2 flex-col overflow-x-hidden overflow-y-auto px-6"
|
|
221
|
+
:aria-label="t('nav.mobile')"
|
|
222
|
+
:inert="menuLevel !== 'root'"
|
|
223
|
+
>
|
|
224
|
+
<div class="m-auto w-full space-y-8 py-6">
|
|
225
|
+
<template v-for="item in rootNavItems" :key="item.href">
|
|
226
|
+
<button
|
|
227
|
+
v-if="item.drill"
|
|
228
|
+
:ref="setDrillButtonRef"
|
|
229
|
+
type="button"
|
|
230
|
+
class="group block w-full text-start"
|
|
231
|
+
@click="drillIntoDocs"
|
|
232
|
+
>
|
|
233
|
+
<span
|
|
234
|
+
class="flex items-center gap-2.5 text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
|
|
235
|
+
:class="
|
|
236
|
+
isActive(item.href)
|
|
237
|
+
? 'text-foreground'
|
|
238
|
+
: 'text-foreground/70 transition-colors group-hover:text-foreground'
|
|
239
|
+
"
|
|
240
|
+
>
|
|
241
|
+
{{ item.label }}
|
|
242
|
+
<Icon
|
|
243
|
+
name="lucide:chevron-right"
|
|
244
|
+
class="size-6 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
|
245
|
+
aria-hidden="true"
|
|
246
|
+
/>
|
|
247
|
+
</span>
|
|
248
|
+
<span
|
|
249
|
+
v-if="item.description"
|
|
250
|
+
class="mt-1.5 block text-sm text-muted-foreground"
|
|
251
|
+
>
|
|
252
|
+
{{ item.description }}
|
|
253
|
+
</span>
|
|
254
|
+
</button>
|
|
255
|
+
<NuxtLink v-else :to="item.href" class="group block" @click="closeMenu">
|
|
256
|
+
<span
|
|
257
|
+
class="block text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
|
|
258
|
+
:class="
|
|
259
|
+
isActive(item.href)
|
|
260
|
+
? 'text-foreground'
|
|
261
|
+
: 'text-foreground/70 transition-colors group-hover:text-foreground'
|
|
262
|
+
"
|
|
263
|
+
>
|
|
264
|
+
{{ item.label }}
|
|
265
|
+
</span>
|
|
266
|
+
<span
|
|
267
|
+
v-if="item.description"
|
|
268
|
+
class="mt-1.5 block text-sm text-muted-foreground"
|
|
269
|
+
>
|
|
270
|
+
{{ item.description }}
|
|
271
|
+
</span>
|
|
272
|
+
</NuxtLink>
|
|
273
|
+
</template>
|
|
274
|
+
</div>
|
|
275
|
+
</nav>
|
|
276
|
+
|
|
277
|
+
<nav
|
|
278
|
+
v-if="hasDocsLevel"
|
|
279
|
+
class="flex min-h-0 w-1/2 flex-col px-6"
|
|
280
|
+
:aria-label="docsLevelTitle"
|
|
281
|
+
:inert="menuLevel !== 'docs'"
|
|
282
|
+
>
|
|
283
|
+
<button
|
|
284
|
+
ref="docsBackButton"
|
|
285
|
+
type="button"
|
|
286
|
+
class="flex items-center gap-1 self-start py-2 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
|
|
287
|
+
@click="backToRoot"
|
|
288
|
+
>
|
|
289
|
+
<Icon name="lucide:chevron-left" class="size-4" aria-hidden="true" />
|
|
290
|
+
{{ t("nav.menu") }}
|
|
291
|
+
</button>
|
|
292
|
+
<p
|
|
293
|
+
class="mt-1 text-[2rem] leading-[1.1] font-semibold tracking-[-0.03em] text-foreground"
|
|
294
|
+
>
|
|
295
|
+
{{ docsLevelTitle }}
|
|
296
|
+
</p>
|
|
297
|
+
<div class="-mx-2 mt-3 min-h-0 flex-1 overflow-x-hidden overflow-y-auto pb-4">
|
|
298
|
+
<template v-for="section in docsMenuSections" :key="section.id">
|
|
299
|
+
<p
|
|
300
|
+
v-if="section.title"
|
|
301
|
+
class="mt-6 mb-1 px-2 text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase first:mt-2"
|
|
302
|
+
>
|
|
303
|
+
{{ section.title }}
|
|
304
|
+
</p>
|
|
305
|
+
<NuxtLink
|
|
306
|
+
v-for="link in section.links"
|
|
307
|
+
:key="link.id"
|
|
308
|
+
:to="link.href"
|
|
309
|
+
class="flex items-center rounded-md px-2 py-2 text-lg font-semibold transition-colors"
|
|
310
|
+
:class="
|
|
311
|
+
link.active
|
|
312
|
+
? 'bg-primary/10 text-primary'
|
|
313
|
+
: 'text-foreground hover:bg-muted/60'
|
|
314
|
+
"
|
|
315
|
+
:aria-current="link.active ? 'true' : undefined"
|
|
316
|
+
@click="closeMenu"
|
|
317
|
+
>
|
|
318
|
+
{{ link.label }}
|
|
319
|
+
<span
|
|
320
|
+
v-if="link.active"
|
|
321
|
+
class="mr-1 ml-auto size-1.5 rounded-full bg-primary"
|
|
322
|
+
aria-hidden="true"
|
|
323
|
+
/>
|
|
324
|
+
</NuxtLink>
|
|
325
|
+
</template>
|
|
326
|
+
</div>
|
|
327
|
+
</nav>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="shrink-0 space-y-3.5 px-6 pt-4 pb-8">
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
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"
|
|
334
|
+
@click="openSearch"
|
|
335
|
+
>
|
|
336
|
+
<Icon name="lucide:search" class="size-4" aria-hidden="true" />
|
|
337
|
+
{{ t("nav.search") }}...
|
|
338
|
+
<Kbd class="ml-auto h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
|
|
339
|
+
</button>
|
|
340
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
341
|
+
<ClientOnly>
|
|
342
|
+
<ModeToggle variant="pill" />
|
|
343
|
+
<template #fallback>
|
|
344
|
+
<span
|
|
345
|
+
class="h-9 w-24 rounded-full border border-border bg-background"
|
|
346
|
+
aria-hidden="true"
|
|
347
|
+
/>
|
|
348
|
+
</template>
|
|
349
|
+
</ClientOnly>
|
|
350
|
+
<SiteLocaleSwitcher variant="pill" class="flex" @navigate="closeMenu" />
|
|
351
|
+
<span v-if="socialLinks.length" class="ml-auto flex items-center gap-2">
|
|
352
|
+
<NuxtLink
|
|
353
|
+
v-for="link in socialLinks"
|
|
354
|
+
:key="link.href"
|
|
355
|
+
:to="link.href"
|
|
356
|
+
target="_blank"
|
|
357
|
+
rel="noopener noreferrer"
|
|
358
|
+
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"
|
|
359
|
+
@click="closeMenu"
|
|
360
|
+
>
|
|
361
|
+
<Icon v-if="link.icon" :name="link.icon" class="size-4" aria-hidden="true" />
|
|
362
|
+
{{ link.label }}
|
|
363
|
+
</NuxtLink>
|
|
364
|
+
</span>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
244
367
|
</div>
|
|
245
|
-
</
|
|
246
|
-
</
|
|
368
|
+
</SheetContent>
|
|
369
|
+
</Sheet>
|
|
247
370
|
</div>
|
|
248
371
|
</div>
|
|
249
|
-
</
|
|
372
|
+
</header>
|
|
250
373
|
</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>
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
DropdownMenuItem,
|
|
8
8
|
DropdownMenuTrigger,
|
|
9
9
|
} from "#ginko-docs/components/ui/dropdown-menu";
|
|
10
|
-
import { cn } from "#ginko-docs/
|
|
10
|
+
import { cn } from "#ginko-docs/utils";
|
|
11
11
|
import { computed } from "vue";
|
|
12
12
|
import { useI18n } from "#imports";
|
|
13
13
|
import { useLocalizedRouteSwitch } from "#ginko-docs/composables/useLocalizedRouteSwitch";
|
|
14
14
|
import { locales as configuredLocales } from "../../../i18n/locales";
|
|
15
15
|
|
|
16
|
-
type LocaleSwitcherVariant = "dropdown" | "menu-row" | "menu-tile" | "segmented";
|
|
16
|
+
type LocaleSwitcherVariant = "dropdown" | "menu-row" | "menu-tile" | "segmented" | "pill";
|
|
17
17
|
|
|
18
18
|
type LocaleEntry = {
|
|
19
19
|
code: string;
|
|
@@ -160,12 +160,30 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
160
160
|
</div>
|
|
161
161
|
|
|
162
162
|
<div
|
|
163
|
-
v-else-if="
|
|
163
|
+
v-else-if="
|
|
164
|
+
localeLinks.length > 1 &&
|
|
165
|
+
(variant === 'menu-row' || variant === 'menu-tile' || variant === 'pill')
|
|
166
|
+
"
|
|
164
167
|
:class="cn('items-center', props.class)"
|
|
165
168
|
>
|
|
166
169
|
<DropdownMenu>
|
|
167
170
|
<DropdownMenuTrigger as-child>
|
|
168
171
|
<Button
|
|
172
|
+
v-if="variant === 'pill'"
|
|
173
|
+
variant="outline"
|
|
174
|
+
class="h-9 gap-2 rounded-full px-4 text-sm font-semibold"
|
|
175
|
+
:aria-label="t('nav.language')"
|
|
176
|
+
>
|
|
177
|
+
<Icon
|
|
178
|
+
v-if="currentLocale?.flagIcon"
|
|
179
|
+
:name="currentLocale.flagIcon"
|
|
180
|
+
class="size-4 rounded-full"
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
/>
|
|
183
|
+
<span>{{ currentLocale?.shortLabel }}</span>
|
|
184
|
+
</Button>
|
|
185
|
+
<Button
|
|
186
|
+
v-else
|
|
169
187
|
variant="ghost"
|
|
170
188
|
:class="
|
|
171
189
|
cn(
|
|
@@ -204,7 +222,7 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
204
222
|
/>
|
|
205
223
|
</Button>
|
|
206
224
|
</DropdownMenuTrigger>
|
|
207
|
-
<DropdownMenuContent align="end" class="w-44">
|
|
225
|
+
<DropdownMenuContent align="end" class="w-44" portal-disabled>
|
|
208
226
|
<DropdownMenuItem v-for="entry in localeLinks" :key="entry.code" as-child>
|
|
209
227
|
<NuxtLink
|
|
210
228
|
:to="entry.to"
|