@lupinum/ginko-docs 0.2.2
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 +112 -0
- package/app/app.config.ts +49 -0
- package/app/app.vue +71 -0
- package/app/assets/css/prose.css +1154 -0
- package/app/assets/css/tailwind.css +365 -0
- package/app/components/OgImage/GinkoDocs.satori.vue +117 -0
- package/app/components/content/DocumentPageShell.vue +55 -0
- package/app/components/content/Feedback.vue +97 -0
- package/app/components/content/PageMarkdownCopy.vue +102 -0
- package/app/components/mdc/MdcAccordion.vue +64 -0
- package/app/components/mdc/MdcAccordionItem.vue +51 -0
- package/app/components/mdc/MdcAlert.vue +34 -0
- package/app/components/mdc/MdcAside.vue +27 -0
- package/app/components/mdc/MdcCallout.vue +62 -0
- package/app/components/mdc/MdcCard.vue +186 -0
- package/app/components/mdc/MdcCardGroup.vue +47 -0
- package/app/components/mdc/MdcCenter.vue +71 -0
- package/app/components/mdc/MdcCodeGroup.vue +193 -0
- package/app/components/mdc/MdcColumn.vue +70 -0
- package/app/components/mdc/MdcDocImg.vue +25 -0
- package/app/components/mdc/MdcDropcap.vue +84 -0
- package/app/components/mdc/MdcError.vue +16 -0
- package/app/components/mdc/MdcField.vue +62 -0
- package/app/components/mdc/MdcFieldGroup.vue +18 -0
- package/app/components/mdc/MdcFigure.vue +51 -0
- package/app/components/mdc/MdcIdea.vue +16 -0
- package/app/components/mdc/MdcInfo.vue +16 -0
- package/app/components/mdc/MdcLayout.vue +57 -0
- package/app/components/mdc/MdcNote.vue +16 -0
- package/app/components/mdc/MdcPassage.vue +32 -0
- package/app/components/mdc/MdcQuiz.vue +281 -0
- package/app/components/mdc/MdcQuizOption.vue +91 -0
- package/app/components/mdc/MdcQuizQuestion.vue +226 -0
- package/app/components/mdc/MdcReadMore.vue +90 -0
- package/app/components/mdc/MdcReadMoreGroup.vue +18 -0
- package/app/components/mdc/MdcShortcut.vue +46 -0
- package/app/components/mdc/MdcStep.vue +42 -0
- package/app/components/mdc/MdcSteps.vue +25 -0
- package/app/components/mdc/MdcSuccess.vue +16 -0
- package/app/components/mdc/MdcTab.vue +17 -0
- package/app/components/mdc/MdcTabs.vue +132 -0
- package/app/components/mdc/MdcTimeline.vue +14 -0
- package/app/components/mdc/MdcTimelineItem.vue +49 -0
- package/app/components/mdc/MdcWarning.vue +16 -0
- package/app/components/mdc/icons.ts +10 -0
- package/app/components/prose/ProseA.vue +27 -0
- package/app/components/prose/ProseBlockquote.vue +14 -0
- package/app/components/prose/ProseCode.vue +14 -0
- package/app/components/prose/ProseEm.vue +14 -0
- package/app/components/prose/ProseH1.vue +18 -0
- package/app/components/prose/ProseH2.vue +24 -0
- package/app/components/prose/ProseH3.vue +24 -0
- package/app/components/prose/ProseH4.vue +14 -0
- package/app/components/prose/ProseHr.vue +12 -0
- package/app/components/prose/ProseImg.vue +120 -0
- package/app/components/prose/ProseLi.vue +14 -0
- package/app/components/prose/ProseOl.vue +14 -0
- package/app/components/prose/ProseP.vue +14 -0
- package/app/components/prose/ProsePre.vue +217 -0
- package/app/components/prose/ProseStrong.vue +14 -0
- package/app/components/prose/ProseTable.vue +16 -0
- package/app/components/prose/ProseTbody.vue +14 -0
- package/app/components/prose/ProseTd.vue +14 -0
- package/app/components/prose/ProseTh.vue +14 -0
- package/app/components/prose/ProseThead.vue +14 -0
- package/app/components/prose/ProseTr.vue +14 -0
- package/app/components/prose/ProseUl.vue +14 -0
- package/app/components/site/ModeToggle.vue +123 -0
- package/app/components/site/SiteBanner.vue +56 -0
- package/app/components/site/SiteFooter.vue +80 -0
- package/app/components/site/SiteHeader.vue +250 -0
- package/app/components/site/SiteInteractionLayer.vue +35 -0
- package/app/components/site/SiteLocaleSwitcher.vue +268 -0
- package/app/components/site/SiteLogoMark.vue +12 -0
- package/app/components/site/SiteSkipLink.vue +14 -0
- package/app/components/ui/accordion/Accordion.vue +15 -0
- package/app/components/ui/accordion/AccordionContent.vue +23 -0
- package/app/components/ui/accordion/AccordionItem.vue +24 -0
- package/app/components/ui/accordion/AccordionTrigger.vue +35 -0
- package/app/components/ui/accordion/index.ts +4 -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 +34 -0
- package/app/components/ui/badge/Badge.vue +28 -0
- package/app/components/ui/badge/index.ts +24 -0
- package/app/components/ui/button/Button.vue +31 -0
- package/app/components/ui/button/index.ts +35 -0
- package/app/components/ui/card/Card.vue +16 -0
- package/app/components/ui/card/CardAction.vue +17 -0
- package/app/components/ui/card/CardContent.vue +14 -0
- package/app/components/ui/card/CardDescription.vue +14 -0
- package/app/components/ui/card/CardFooter.vue +14 -0
- package/app/components/ui/card/CardHeader.vue +22 -0
- package/app/components/ui/card/CardTitle.vue +14 -0
- package/app/components/ui/card/card-variants.ts +15 -0
- package/app/components/ui/card/index.ts +8 -0
- package/app/components/ui/checkbox/Checkbox.vue +35 -0
- package/app/components/ui/checkbox/index.ts +1 -0
- package/app/components/ui/collapsible/Collapsible.vue +15 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +25 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/app/components/ui/collapsible/index.ts +3 -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 +69 -0
- package/app/components/ui/dialog/DialogDescription.vue +21 -0
- package/app/components/ui/dialog/DialogFooter.vue +15 -0
- package/app/components/ui/dialog/DialogHeader.vue +12 -0
- package/app/components/ui/dialog/DialogOverlay.vue +26 -0
- package/app/components/ui/dialog/DialogPortal.vue +12 -0
- package/app/components/ui/dialog/DialogTitle.vue +21 -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 +15 -0
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +36 -0
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -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 +25 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +15 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +37 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +15 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +29 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
- package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +14 -0
- package/app/components/ui/dropdown-menu/index.ts +16 -0
- package/app/components/ui/input/Input.vue +23 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/kbd/Kbd.vue +23 -0
- package/app/components/ui/kbd/KbdGroup.vue +14 -0
- package/app/components/ui/kbd/index.ts +22 -0
- package/app/components/ui/label/Label.vue +14 -0
- package/app/components/ui/label/index.ts +1 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +39 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +33 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/separator/Separator.vue +27 -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 +59 -0
- package/app/components/ui/sheet/SheetDescription.vue +21 -0
- package/app/components/ui/sheet/SheetFooter.vue +12 -0
- package/app/components/ui/sheet/SheetHeader.vue +12 -0
- package/app/components/ui/sheet/SheetOverlay.vue +26 -0
- package/app/components/ui/sheet/SheetTitle.vue +21 -0
- package/app/components/ui/sheet/SheetTrigger.vue +12 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/switch/Switch.vue +39 -0
- package/app/components/ui/switch/index.ts +1 -0
- package/app/components/ui/tabs/Tabs.vue +24 -0
- package/app/components/ui/tabs/TabsContent.vue +21 -0
- package/app/components/ui/tabs/TabsList.vue +26 -0
- package/app/components/ui/tabs/TabsTrigger.vue +28 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/textarea/Textarea.vue +31 -0
- package/app/components/ui/textarea/index.ts +1 -0
- package/app/composables/site-navigation.utils.ts +80 -0
- package/app/composables/useCanonicalUrl.ts +9 -0
- package/app/composables/useContentRouteAlternates.ts +43 -0
- package/app/composables/useGinkoAnalytics.ts +37 -0
- package/app/composables/useGinkoDocsConfig.ts +6 -0
- package/app/composables/useGinkoOgImage.ts +20 -0
- package/app/composables/useLocalizedPath.ts +12 -0
- package/app/composables/useLocalizedRouteSwitch.ts +31 -0
- package/app/composables/usePageMarkdownActions.ts +79 -0
- package/app/composables/useSchemaJsonLd.ts +25 -0
- package/app/composables/useSiteNavigation.ts +86 -0
- package/app/config/site.utils.ts +36 -0
- package/app/error.vue +125 -0
- package/app/features/docs/components/DocsContributeLinks.vue +65 -0
- package/app/features/docs/components/DocsMobileToc.vue +104 -0
- package/app/features/docs/components/DocsPageContent.vue +188 -0
- package/app/features/docs/components/DocsPageNav.vue +57 -0
- package/app/features/docs/components/DocsSidebar.vue +129 -0
- package/app/features/docs/components/DocsSidebarDropdown.vue +70 -0
- package/app/features/docs/components/DocsSidebarItem.vue +169 -0
- package/app/features/docs/components/DocsSidebarList.vue +35 -0
- package/app/features/docs/components/DocsSidebarTabs.vue +35 -0
- package/app/features/docs/components/DocsToc.vue +60 -0
- package/app/features/docs/composables/useDocsEntryPath.ts +13 -0
- package/app/features/docs/composables/useDocsNavigation.ts +29 -0
- package/app/features/docs/composables/useDocsNavigationData.ts +15 -0
- package/app/features/docs/composables/useScrollspy.ts +67 -0
- package/app/features/docs/docs-navigation.ts +170 -0
- package/app/features/search/command-center.ts +156 -0
- package/app/features/search/components/SiteCommandCenter.vue +323 -0
- package/app/features/search/components/SiteSearchPageHighlight.vue +124 -0
- package/app/features/search/useCommandCenter.ts +300 -0
- package/app/layouts/blog.vue +15 -0
- package/app/layouts/default.vue +12 -0
- package/app/layouts/docs.vue +20 -0
- package/app/lib/errors.ts +84 -0
- package/app/lib/schema-org.ts +38 -0
- package/app/lib/utils.ts +7 -0
- package/app/pages/blog/[slug].vue +211 -0
- package/app/pages/blog/index.vue +106 -0
- package/app/pages/docs/[...slug].vue +10 -0
- package/app/pages/docs/index.vue +23 -0
- package/app/pages/index.vue +140 -0
- package/app/plugins/ssr-width.ts +6 -0
- package/app/utils/content.ts +56 -0
- package/app/utils/index.ts +1 -0
- package/components.ts +9 -0
- package/content.js +175 -0
- package/content.ts +148 -0
- package/i18n/i18n.config.ts +10 -0
- package/i18n/locales.ts +81 -0
- package/i18n/messages/global/banner.ts +8 -0
- package/i18n/messages/global/blog.ts +14 -0
- package/i18n/messages/global/command.ts +44 -0
- package/i18n/messages/global/docs.ts +32 -0
- package/i18n/messages/global/errors.ts +36 -0
- package/i18n/messages/global/feedback.ts +11 -0
- package/i18n/messages/global/index.ts +21 -0
- package/i18n/messages/global/nav.ts +18 -0
- package/i18n/messages/global/site.ts +4 -0
- package/i18n/messages/global/theme.ts +7 -0
- package/i18n/messages/index.ts +20 -0
- package/i18n/messages/localize.ts +35 -0
- package/i18n/messages/source.ts +5 -0
- package/i18n/routes.ts +33 -0
- package/icon-bundle.ts +141 -0
- package/index.d.ts +13 -0
- package/modules/feature-routing.ts +32 -0
- package/nuxt.config.ts +194 -0
- package/package.json +91 -0
- package/runtime/markdown/inline-code-highlight.ts +155 -0
- package/runtime/server/agent-markdown.ts +21 -0
- package/runtime/utils.ts +7 -0
- package/server/mcp/tools/get-page.ts +14 -0
- package/server/mcp/tools/list-pages.ts +10 -0
- package/server/plugins/agent-markdown.ts +6 -0
- package/shared/route-slugs.ts +5 -0
- package/shared/types/app-config.ts +100 -0
- package/tags.ts +204 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createContentNotFoundError } from "#ginko-docs/lib/errors";
|
|
3
|
+
import { filterTocByDepth, flattenTocLinks } from "#ginko-docs/utils/content";
|
|
4
|
+
import ContentFeedback from "#ginko-docs/components/content/Feedback.vue";
|
|
5
|
+
import DocsMobileToc from "./DocsMobileToc.vue";
|
|
6
|
+
import DocsPageNav from "./DocsPageNav.vue";
|
|
7
|
+
import DocsToc from "./DocsToc.vue";
|
|
8
|
+
import DocsContributeLinks from "./DocsContributeLinks.vue";
|
|
9
|
+
import { computed, watch } from "vue";
|
|
10
|
+
import { useAppConfig, useContentPage, useHead, useI18n, useRoute, useSeoMeta } from "#imports";
|
|
11
|
+
import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
|
|
12
|
+
import { useGinkoOgImage } from "#ginko-docs/composables/useGinkoOgImage";
|
|
13
|
+
import { useScrollspy } from "#ginko-docs/features/docs/composables/useScrollspy";
|
|
14
|
+
import PageMarkdownCopy from "#ginko-docs/components/content/PageMarkdownCopy.vue";
|
|
15
|
+
import { Separator } from "#ginko-docs/components/ui/separator";
|
|
16
|
+
import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
|
|
17
|
+
import { useSchemaJsonLd } from "#ginko-docs/composables/useSchemaJsonLd";
|
|
18
|
+
import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
|
|
19
|
+
import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
|
|
20
|
+
import { syncContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
|
|
21
|
+
|
|
22
|
+
type DocsNavLink = {
|
|
23
|
+
path: string;
|
|
24
|
+
title: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function toDocsNavLink(
|
|
28
|
+
item: { path?: string; title?: string } | null | undefined,
|
|
29
|
+
): DocsNavLink | null {
|
|
30
|
+
return item?.path && item?.title ? { path: item.path, title: item.title } : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { locale, t } = useI18n();
|
|
34
|
+
const config = useAppConfig().ginkoDocs;
|
|
35
|
+
const route = useRoute();
|
|
36
|
+
const localizedPath = useLocalizedPath();
|
|
37
|
+
const {
|
|
38
|
+
page,
|
|
39
|
+
previous,
|
|
40
|
+
next: nextContent,
|
|
41
|
+
} = await useContentPage("docs", {
|
|
42
|
+
locale: () => locale.value,
|
|
43
|
+
fallback: true,
|
|
44
|
+
surround: true,
|
|
45
|
+
});
|
|
46
|
+
if (!page.value) throw createContentNotFoundError();
|
|
47
|
+
syncContentRouteAlternates(page);
|
|
48
|
+
const { trail } = await useDocsNavigation();
|
|
49
|
+
|
|
50
|
+
const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
|
|
51
|
+
const pageTitle = computed(() => page.value?.title ?? t("docs.fallbackTitle"));
|
|
52
|
+
const pageDescription = computed(() => page.value?.description ?? t("docs.fallbackDescription"));
|
|
53
|
+
const canonicalUrl = useCanonicalUrl();
|
|
54
|
+
const tocItems = computed(() =>
|
|
55
|
+
filterTocByDepth(flattenTocLinks(page.value?.body?.toc?.links), config.toc?.depth ?? 3),
|
|
56
|
+
);
|
|
57
|
+
const prev = computed(() => toDocsNavLink(previous.value));
|
|
58
|
+
const next = computed(() => toDocsNavLink(nextContent.value));
|
|
59
|
+
const visibleTrail = computed(() => trail.value.slice(0, -1));
|
|
60
|
+
const schemaBreadcrumbs = computed(() => [
|
|
61
|
+
{ name: siteName.value, path: localizedPath("home") },
|
|
62
|
+
...trail.value
|
|
63
|
+
.filter((item) => item.path)
|
|
64
|
+
.map((item) => ({ name: item.title, path: item.path! })),
|
|
65
|
+
]);
|
|
66
|
+
const { activeId, refresh } = useScrollspy(computed(() => tocItems.value.map((item) => item.id)));
|
|
67
|
+
|
|
68
|
+
watch(tocItems, () => {
|
|
69
|
+
void refresh();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useSeoMeta({
|
|
73
|
+
title: computed(() => `${pageTitle.value} - ${siteName.value}`),
|
|
74
|
+
description: pageDescription,
|
|
75
|
+
ogTitle: computed(() => `${pageTitle.value} - ${siteName.value}`),
|
|
76
|
+
ogDescription: pageDescription,
|
|
77
|
+
ogUrl: canonicalUrl,
|
|
78
|
+
twitterCard: "summary_large_image",
|
|
79
|
+
twitterTitle: computed(() => `${pageTitle.value} - ${siteName.value}`),
|
|
80
|
+
twitterDescription: pageDescription,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Social card with the raw page title (no site-name suffix baked in).
|
|
84
|
+
useGinkoOgImage({
|
|
85
|
+
title: pageTitle.value,
|
|
86
|
+
description: pageDescription.value,
|
|
87
|
+
locale: locale.value,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
useSchemaJsonLd(() => [
|
|
91
|
+
{
|
|
92
|
+
"@type": "TechArticle",
|
|
93
|
+
headline: pageTitle.value,
|
|
94
|
+
description: pageDescription.value,
|
|
95
|
+
url: canonicalUrl.value,
|
|
96
|
+
inLanguage: locale.value,
|
|
97
|
+
dateModified: page.value?.updated,
|
|
98
|
+
isPartOf: { "@type": "WebSite", name: siteName.value, url: config.site.url },
|
|
99
|
+
publisher: { "@type": "Organization", name: siteName.value, url: config.site.url },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"@type": "BreadcrumbList",
|
|
103
|
+
itemListElement: schemaBreadcrumbs.value.map((item, index) => ({
|
|
104
|
+
"@type": "ListItem",
|
|
105
|
+
position: index + 1,
|
|
106
|
+
name: item.name,
|
|
107
|
+
item: new URL(item.path, config.site.url).toString(),
|
|
108
|
+
})),
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
useHead(() => ({
|
|
113
|
+
link: [{ key: "canonical", rel: "canonical", href: canonicalUrl.value }],
|
|
114
|
+
meta: [{ property: "og:type", content: "article" }],
|
|
115
|
+
}));
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<template>
|
|
119
|
+
<div class="flex min-w-0 flex-1 flex-col xl:flex-row">
|
|
120
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
121
|
+
<DocsMobileToc
|
|
122
|
+
class="order-1"
|
|
123
|
+
:items="tocItems"
|
|
124
|
+
:active-id="activeId"
|
|
125
|
+
:page-title="pageTitle"
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<main
|
|
129
|
+
id="main-content"
|
|
130
|
+
class="order-2 mx-auto w-full max-w-3xl flex-1 px-4 py-8 md:px-6 xl:py-14"
|
|
131
|
+
>
|
|
132
|
+
<article v-if="page">
|
|
133
|
+
<div class="flex items-start justify-between gap-4 sm:gap-6">
|
|
134
|
+
<div class="min-w-0">
|
|
135
|
+
<nav
|
|
136
|
+
v-if="visibleTrail.length"
|
|
137
|
+
class="mb-2 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
|
|
138
|
+
:aria-label="t('docs.breadcrumbs')"
|
|
139
|
+
>
|
|
140
|
+
<template v-for="(item, index) in visibleTrail" :key="item.id">
|
|
141
|
+
<Icon
|
|
142
|
+
v-if="index > 0"
|
|
143
|
+
name="lucide:chevron-right"
|
|
144
|
+
class="size-3.5 text-border"
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
/>
|
|
147
|
+
<NuxtLink
|
|
148
|
+
v-if="item.path"
|
|
149
|
+
:to="item.path"
|
|
150
|
+
class="rounded-sm transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
151
|
+
>
|
|
152
|
+
{{ item.title }}
|
|
153
|
+
</NuxtLink>
|
|
154
|
+
<span v-else>{{ item.title }}</span>
|
|
155
|
+
</template>
|
|
156
|
+
</nav>
|
|
157
|
+
<h1
|
|
158
|
+
class="font-heading text-[2rem] leading-[1.15] font-semibold tracking-[-0.025em] text-balance text-foreground sm:text-[2.25rem]"
|
|
159
|
+
>
|
|
160
|
+
{{ page.title }}
|
|
161
|
+
</h1>
|
|
162
|
+
<p v-if="page.description" class="mt-2 text-lg leading-7 text-muted-foreground">
|
|
163
|
+
{{ page.description }}
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
<PageMarkdownCopy />
|
|
167
|
+
</div>
|
|
168
|
+
<Separator class="my-8" />
|
|
169
|
+
|
|
170
|
+
<div class="content-prose content-prose-docs">
|
|
171
|
+
<ContentRenderer :value="page" />
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<DocsContributeLinks :stem="page.stem" :extension="page.extension" :title="pageTitle" />
|
|
175
|
+
<DocsPageNav :prev="prev" :next="next" />
|
|
176
|
+
<ContentFeedback :label="t('feedback.label')" />
|
|
177
|
+
</article>
|
|
178
|
+
</main>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<aside
|
|
182
|
+
class="sticky top-[var(--site-header-height)] hidden h-[calc(100vh-var(--site-header-height))] w-[var(--docs-toc-width)] shrink-0 flex-col overflow-y-auto pt-10 pr-4 pb-6 xl:flex"
|
|
183
|
+
:aria-label="t('docs.toc')"
|
|
184
|
+
>
|
|
185
|
+
<DocsToc :items="tocItems" :active-id="activeId" />
|
|
186
|
+
</aside>
|
|
187
|
+
</div>
|
|
188
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from "#imports";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
prev?: { path?: string; title?: string } | null;
|
|
6
|
+
next?: { path?: string; title?: string } | null;
|
|
7
|
+
}>();
|
|
8
|
+
const { t } = useI18n();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<nav
|
|
13
|
+
v-if="prev || next"
|
|
14
|
+
class="mt-12 grid grid-cols-1 gap-3 border-t border-border pt-8 sm:grid-cols-2"
|
|
15
|
+
:aria-label="t('docs.pageNavigation')"
|
|
16
|
+
>
|
|
17
|
+
<!-- Previous -->
|
|
18
|
+
<NuxtLink
|
|
19
|
+
v-if="prev"
|
|
20
|
+
:to="prev.path"
|
|
21
|
+
class="group flex min-h-20 flex-col gap-1.5 rounded-xl border border-border bg-card px-5 py-4 transition-[border-color,background-color] duration-200 hover:border-foreground/20 hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:col-start-1"
|
|
22
|
+
>
|
|
23
|
+
<span
|
|
24
|
+
class="flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors group-hover:text-foreground"
|
|
25
|
+
>
|
|
26
|
+
<Icon
|
|
27
|
+
name="lucide:chevron-left"
|
|
28
|
+
class="size-3.5 transition-transform group-hover:-translate-x-0.5"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
/>
|
|
31
|
+
{{ t("docs.previous") }}
|
|
32
|
+
</span>
|
|
33
|
+
<span class="text-sm leading-snug font-semibold text-foreground">{{ prev.title }}</span>
|
|
34
|
+
</NuxtLink>
|
|
35
|
+
<div v-else class="hidden sm:block" />
|
|
36
|
+
|
|
37
|
+
<!-- Next -->
|
|
38
|
+
<NuxtLink
|
|
39
|
+
v-if="next"
|
|
40
|
+
:to="next.path"
|
|
41
|
+
class="group flex min-h-20 flex-col items-end gap-1.5 rounded-xl border border-border bg-card px-5 py-4 text-right transition-[border-color,background-color] duration-200 hover:border-foreground/20 hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:col-start-2"
|
|
42
|
+
>
|
|
43
|
+
<span
|
|
44
|
+
class="flex items-center gap-1 text-xs font-medium text-muted-foreground transition-colors group-hover:text-foreground"
|
|
45
|
+
>
|
|
46
|
+
{{ t("docs.next") }}
|
|
47
|
+
<Icon
|
|
48
|
+
name="lucide:chevron-right"
|
|
49
|
+
class="size-3.5 transition-transform group-hover:translate-x-0.5"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
/>
|
|
52
|
+
</span>
|
|
53
|
+
<span class="text-sm leading-snug font-semibold text-foreground">{{ next.title }}</span>
|
|
54
|
+
</NuxtLink>
|
|
55
|
+
<div v-else class="hidden sm:block" />
|
|
56
|
+
</nav>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { cn } from "#ginko-docs/lib/utils";
|
|
3
|
+
import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
|
|
4
|
+
import { findFirstNavigationPage } from "@lupinum/ginko-content/client";
|
|
5
|
+
import {
|
|
6
|
+
docsNavigationSectionContainsPath,
|
|
7
|
+
getDocsNavigationGroups,
|
|
8
|
+
type DocsNavigationSection,
|
|
9
|
+
} from "#ginko-docs/features/docs/docs-navigation";
|
|
10
|
+
import DocsSidebarItem from "./DocsSidebarItem.vue";
|
|
11
|
+
import DocsSidebarDropdown from "./DocsSidebarDropdown.vue";
|
|
12
|
+
import DocsSidebarList from "./DocsSidebarList.vue";
|
|
13
|
+
import DocsSidebarTabs from "./DocsSidebarTabs.vue";
|
|
14
|
+
import { computed, ref, watch } from "vue";
|
|
15
|
+
import { navigateTo, useAppConfig, useI18n, useRoute } from "#imports";
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
variant?: "desktop" | "drawer";
|
|
20
|
+
}>(),
|
|
21
|
+
{
|
|
22
|
+
variant: "desktop",
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
const route = useRoute();
|
|
28
|
+
const sidebarSwitcher = useAppConfig().ginkoDocs.site.docsSidebarSwitcher;
|
|
29
|
+
const { sections } = await useDocsNavigation();
|
|
30
|
+
const switcherSections = computed(() =>
|
|
31
|
+
sections.value.filter((section): section is DocsNavigationSection & { title: string } =>
|
|
32
|
+
Boolean(section.title),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
const selectedSectionId = ref("");
|
|
36
|
+
const routeSection = computed(() =>
|
|
37
|
+
sections.value.find((section) => docsNavigationSectionContainsPath(section, route.path)),
|
|
38
|
+
);
|
|
39
|
+
const activeSectionId = computed(
|
|
40
|
+
() => selectedSectionId.value || routeSection.value?.id || sections.value[0]?.id || "",
|
|
41
|
+
);
|
|
42
|
+
const activeSection = computed(
|
|
43
|
+
() => sections.value.find((section) => section.id === activeSectionId.value) ?? sections.value[0],
|
|
44
|
+
);
|
|
45
|
+
const groups = computed(() =>
|
|
46
|
+
activeSection.value ? getDocsNavigationGroups(activeSection.value) : [],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
watch(
|
|
50
|
+
() => route.path,
|
|
51
|
+
() => {
|
|
52
|
+
selectedSectionId.value = "";
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function setActiveSection(id: string) {
|
|
57
|
+
selectedSectionId.value = id;
|
|
58
|
+
const section = sections.value.find((entry) => entry.id === id);
|
|
59
|
+
// Structural sections have no page of their own — land on their first page
|
|
60
|
+
// instead of only swapping the sidebar contents.
|
|
61
|
+
const path = section?.path ?? findFirstNavigationPage(section?.items)?.path;
|
|
62
|
+
if (path) void navigateTo(path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const scrollViewportClass =
|
|
66
|
+
"min-h-0 flex-1 overflow-y-auto px-4 pb-4 overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]";
|
|
67
|
+
|
|
68
|
+
const asideClass = computed(() =>
|
|
69
|
+
cn(
|
|
70
|
+
"flex h-full w-full flex-col text-sm transition-[border-color] duration-[250ms] ease-out *:w-full",
|
|
71
|
+
props.variant === "desktop"
|
|
72
|
+
? "absolute inset-y-0 start-0 items-end border-e border-border"
|
|
73
|
+
: "relative min-h-0 items-stretch border-e-0",
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<aside
|
|
80
|
+
data-hovered="false"
|
|
81
|
+
:data-variant="variant"
|
|
82
|
+
:id="variant === 'desktop' ? 'nd-sidebar' : undefined"
|
|
83
|
+
:aria-label="t('docs.label')"
|
|
84
|
+
:class="asideClass"
|
|
85
|
+
>
|
|
86
|
+
<div v-if="switcherSections.length > 1" class="flex flex-col gap-3 p-4 pb-2">
|
|
87
|
+
<DocsSidebarTabs
|
|
88
|
+
v-if="sidebarSwitcher === 'tabs'"
|
|
89
|
+
:sections="switcherSections"
|
|
90
|
+
:active-id="activeSectionId"
|
|
91
|
+
@update:active-id="setActiveSection"
|
|
92
|
+
/>
|
|
93
|
+
<DocsSidebarDropdown
|
|
94
|
+
v-else-if="sidebarSwitcher === 'dropdown'"
|
|
95
|
+
:sections="switcherSections"
|
|
96
|
+
:active-id="activeSectionId"
|
|
97
|
+
@update:active-id="setActiveSection"
|
|
98
|
+
/>
|
|
99
|
+
<DocsSidebarList
|
|
100
|
+
v-else
|
|
101
|
+
:sections="switcherSections"
|
|
102
|
+
:active-id="activeSectionId"
|
|
103
|
+
@update:active-id="setActiveSection"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
:class="[scrollViewportClass, switcherSections.length > 1 ? 'pt-2' : 'pt-4']"
|
|
109
|
+
:data-switcher="switcherSections.length > 1"
|
|
110
|
+
>
|
|
111
|
+
<div class="flex min-w-full flex-col gap-0.5">
|
|
112
|
+
<template v-for="group in groups" :key="group.id">
|
|
113
|
+
<p
|
|
114
|
+
v-if="group.title"
|
|
115
|
+
class="mt-6 mb-1 inline-flex items-center gap-2 px-2 text-sm font-semibold text-foreground first:mt-0"
|
|
116
|
+
>
|
|
117
|
+
{{ group.title }}
|
|
118
|
+
</p>
|
|
119
|
+
<DocsSidebarItem
|
|
120
|
+
v-for="item in group.items"
|
|
121
|
+
:key="item.path ?? item.title"
|
|
122
|
+
:item="item"
|
|
123
|
+
:depth="0"
|
|
124
|
+
/>
|
|
125
|
+
</template>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</aside>
|
|
129
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import type { DocsNavigationSection } from "#ginko-docs/features/docs/docs-navigation";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "#ginko-docs/components/ui/dropdown-menu";
|
|
11
|
+
|
|
12
|
+
const defaultIcon = "lucide:book-open";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
sections: Array<DocsNavigationSection & { title: string }>;
|
|
16
|
+
activeId: string;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
"update:activeId": [id: string];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const activeSection = computed(
|
|
24
|
+
() => props.sections.find((section) => section.id === props.activeId) ?? props.sections[0],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function selectSection(value: unknown) {
|
|
28
|
+
if (typeof value === "string") emit("update:activeId", value);
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<DropdownMenu v-if="activeSection">
|
|
34
|
+
<DropdownMenuTrigger as-child>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="flex h-10 w-full items-center gap-2 rounded-lg border border-border bg-background px-3 text-start text-foreground transition-colors outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
38
|
+
data-slot="docs-sidebar-dropdown"
|
|
39
|
+
>
|
|
40
|
+
<Icon
|
|
41
|
+
:name="activeSection.icon ?? defaultIcon"
|
|
42
|
+
class="size-4 shrink-0"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
/>
|
|
45
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ activeSection.title }}</span>
|
|
46
|
+
<Icon
|
|
47
|
+
name="lucide:chevrons-up-down"
|
|
48
|
+
class="size-4 shrink-0 text-muted-foreground"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
</button>
|
|
52
|
+
</DropdownMenuTrigger>
|
|
53
|
+
<DropdownMenuContent align="start" class="w-(--reka-dropdown-menu-trigger-width)">
|
|
54
|
+
<DropdownMenuRadioGroup :model-value="activeId" @update:model-value="selectSection">
|
|
55
|
+
<DropdownMenuRadioItem
|
|
56
|
+
v-for="section in sections"
|
|
57
|
+
:key="section.id"
|
|
58
|
+
:value="section.id"
|
|
59
|
+
class="gap-2"
|
|
60
|
+
>
|
|
61
|
+
<template #indicator-icon>
|
|
62
|
+
<Icon name="lucide:check" class="size-4" aria-hidden="true" />
|
|
63
|
+
</template>
|
|
64
|
+
<Icon :name="section.icon ?? defaultIcon" class="size-4 shrink-0" aria-hidden="true" />
|
|
65
|
+
<span class="truncate">{{ section.title }}</span>
|
|
66
|
+
</DropdownMenuRadioItem>
|
|
67
|
+
</DropdownMenuRadioGroup>
|
|
68
|
+
</DropdownMenuContent>
|
|
69
|
+
</DropdownMenu>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
import {
|
|
5
|
+
docsNavigationItemContainsPath,
|
|
6
|
+
normalizeDocsNavigationPath,
|
|
7
|
+
type DocsNavigationItem,
|
|
8
|
+
} from "#ginko-docs/features/docs/docs-navigation";
|
|
9
|
+
import { Badge } from "#ginko-docs/components/ui/badge";
|
|
10
|
+
import { cn } from "#ginko-docs/lib/utils";
|
|
11
|
+
|
|
12
|
+
defineOptions({
|
|
13
|
+
name: "DocsSidebarItem",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
item: DocsNavigationItem;
|
|
18
|
+
depth: number;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const route = useRoute();
|
|
22
|
+
|
|
23
|
+
function itemIsExactActive(path: string, item: DocsNavigationItem): boolean {
|
|
24
|
+
return Boolean(
|
|
25
|
+
item.path && normalizeDocsNavigationPath(path) === normalizeDocsNavigationPath(item.path),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function folderShouldBeOpen(path: string, item: DocsNavigationItem): boolean {
|
|
30
|
+
return item.children.length > 0 && docsNavigationItemContainsPath(item, path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isExactActive = computed(() => itemIsExactActive(route.path, props.item));
|
|
34
|
+
|
|
35
|
+
const paddingClass = computed(() => {
|
|
36
|
+
const steps = ["ps-2", "ps-5", "ps-8", "ps-11"] as const;
|
|
37
|
+
return steps[Math.min(props.depth, steps.length - 1)]!;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const linkRowClass = computed(() =>
|
|
41
|
+
cn(
|
|
42
|
+
"group relative flex min-h-10 w-full flex-row items-center gap-2 rounded-lg px-2 py-2 text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
43
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
44
|
+
"hover:bg-accent/50 hover:text-accent-foreground/80 hover:transition-none",
|
|
45
|
+
"data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-foreground data-[active=true]:hover:transition-colors",
|
|
46
|
+
props.depth > 0
|
|
47
|
+
? "data-[active=true]:before:absolute data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5 data-[active=true]:before:w-px data-[active=true]:before:bg-primary data-[active=true]:before:content-['']"
|
|
48
|
+
: undefined,
|
|
49
|
+
paddingClass.value,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const splitFolderRowClass = computed(() =>
|
|
54
|
+
cn(
|
|
55
|
+
"group relative flex min-h-10 w-full flex-row items-center rounded-lg text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none",
|
|
56
|
+
"hover:bg-accent/50 hover:text-accent-foreground/80 hover:transition-none",
|
|
57
|
+
"data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-foreground data-[active=true]:hover:transition-colors",
|
|
58
|
+
props.depth > 0
|
|
59
|
+
? "data-[active=true]:before:absolute data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5 data-[active=true]:before:w-px data-[active=true]:before:bg-primary data-[active=true]:before:content-['']"
|
|
60
|
+
: undefined,
|
|
61
|
+
paddingClass.value,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const folderOpen = ref(folderShouldBeOpen(route.path, props.item));
|
|
66
|
+
|
|
67
|
+
watch(
|
|
68
|
+
() => route.path,
|
|
69
|
+
() => {
|
|
70
|
+
if (folderShouldBeOpen(route.path, props.item)) {
|
|
71
|
+
folderOpen.value = true;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const contentRailClass =
|
|
77
|
+
"relative flex flex-col gap-0.5 *:first:mt-0.5 before:absolute before:start-2.5 before:inset-y-1 before:w-px before:bg-border before:content-['']";
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<div v-if="item.children.length > 0" :data-state="folderOpen ? 'open' : 'closed'">
|
|
82
|
+
<div
|
|
83
|
+
v-if="item.path"
|
|
84
|
+
:class="splitFolderRowClass"
|
|
85
|
+
:data-active="isExactActive ? 'true' : 'false'"
|
|
86
|
+
>
|
|
87
|
+
<NuxtLink
|
|
88
|
+
:to="item.path"
|
|
89
|
+
:aria-current="isExactActive ? 'page' : undefined"
|
|
90
|
+
class="flex min-w-0 flex-1 items-center gap-2 rounded-s-lg py-2 pe-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
91
|
+
data-slot="docs-sidebar-folder-link"
|
|
92
|
+
>
|
|
93
|
+
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
|
|
94
|
+
<span class="min-w-0 truncate">{{ item.title }}</span>
|
|
95
|
+
<Badge
|
|
96
|
+
v-if="item.badge"
|
|
97
|
+
variant="secondary"
|
|
98
|
+
class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
|
|
99
|
+
>
|
|
100
|
+
{{ item.badge }}
|
|
101
|
+
</Badge>
|
|
102
|
+
</NuxtLink>
|
|
103
|
+
<button
|
|
104
|
+
class="group flex size-10 shrink-0 items-center justify-center rounded-e-lg transition-colors outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
105
|
+
data-slot="docs-sidebar-folder-toggle"
|
|
106
|
+
type="button"
|
|
107
|
+
:aria-expanded="folderOpen"
|
|
108
|
+
:data-state="folderOpen ? 'open' : 'closed'"
|
|
109
|
+
@click="folderOpen = !folderOpen"
|
|
110
|
+
>
|
|
111
|
+
<Icon
|
|
112
|
+
name="lucide:chevron-down"
|
|
113
|
+
class="size-4 shrink-0 -rotate-90 transition-transform duration-200 group-data-[state=open]:rotate-0 rtl:rotate-90"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
/>
|
|
116
|
+
<span class="sr-only">{{ item.title }}</span>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
<button
|
|
120
|
+
v-else
|
|
121
|
+
:class="linkRowClass"
|
|
122
|
+
:data-active="isExactActive ? 'true' : 'false'"
|
|
123
|
+
:data-state="folderOpen ? 'open' : 'closed'"
|
|
124
|
+
:aria-expanded="folderOpen"
|
|
125
|
+
type="button"
|
|
126
|
+
@click="folderOpen = !folderOpen"
|
|
127
|
+
>
|
|
128
|
+
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
|
|
129
|
+
<span class="min-w-0 truncate">{{ item.title }}</span>
|
|
130
|
+
<Badge
|
|
131
|
+
v-if="item.badge"
|
|
132
|
+
variant="secondary"
|
|
133
|
+
class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
|
|
134
|
+
>
|
|
135
|
+
{{ item.badge }}
|
|
136
|
+
</Badge>
|
|
137
|
+
<Icon
|
|
138
|
+
name="lucide:chevron-down"
|
|
139
|
+
class="ms-auto size-4 shrink-0 -rotate-90 transition-transform duration-200 group-data-[state=open]:rotate-0 rtl:rotate-90"
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
/>
|
|
142
|
+
</button>
|
|
143
|
+
<div v-show="folderOpen" :class="contentRailClass">
|
|
144
|
+
<DocsSidebarItem
|
|
145
|
+
v-for="child in item.children"
|
|
146
|
+
:key="child.path ?? child.title"
|
|
147
|
+
:item="child"
|
|
148
|
+
:depth="depth + 1"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<NuxtLink
|
|
153
|
+
v-else-if="item.path"
|
|
154
|
+
:to="item.path"
|
|
155
|
+
:aria-current="isExactActive ? 'page' : undefined"
|
|
156
|
+
:class="linkRowClass"
|
|
157
|
+
:data-active="isExactActive ? 'true' : 'false'"
|
|
158
|
+
>
|
|
159
|
+
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
|
|
160
|
+
<span class="min-w-0 truncate">{{ item.title }}</span>
|
|
161
|
+
<Badge
|
|
162
|
+
v-if="item.badge"
|
|
163
|
+
variant="secondary"
|
|
164
|
+
class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
|
|
165
|
+
>
|
|
166
|
+
{{ item.badge }}
|
|
167
|
+
</Badge>
|
|
168
|
+
</NuxtLink>
|
|
169
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocsNavigationSection } from "#ginko-docs/features/docs/docs-navigation";
|
|
3
|
+
|
|
4
|
+
const defaultIcon = "lucide:book-open";
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
sections: Array<DocsNavigationSection & { title: string }>;
|
|
8
|
+
activeId: string;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
"update:activeId": [id: string];
|
|
13
|
+
}>();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<ul class="flex flex-col gap-0.5" data-slot="docs-sidebar-list">
|
|
18
|
+
<li v-for="section in sections" :key="section.id">
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
class="flex min-h-9 w-full items-center gap-2 rounded-lg px-2 text-start text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
22
|
+
:class="
|
|
23
|
+
activeId === section.id
|
|
24
|
+
? 'bg-accent font-medium text-foreground'
|
|
25
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
|
26
|
+
"
|
|
27
|
+
:aria-pressed="activeId === section.id"
|
|
28
|
+
@click="emit('update:activeId', section.id)"
|
|
29
|
+
>
|
|
30
|
+
<Icon :name="section.icon ?? defaultIcon" class="size-4 shrink-0" aria-hidden="true" />
|
|
31
|
+
<span class="truncate">{{ section.title }}</span>
|
|
32
|
+
</button>
|
|
33
|
+
</li>
|
|
34
|
+
</ul>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocsNavigationSection } from "#ginko-docs/features/docs/docs-navigation";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
sections: Array<DocsNavigationSection & { title: string }>;
|
|
6
|
+
activeId: string;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
"update:activeId": [id: string];
|
|
11
|
+
}>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div
|
|
16
|
+
class="inline-flex h-9 w-full items-center justify-center overflow-hidden rounded-lg bg-muted p-[3px] text-muted-foreground"
|
|
17
|
+
role="tablist"
|
|
18
|
+
aria-orientation="horizontal"
|
|
19
|
+
data-slot="docs-sidebar-tabs"
|
|
20
|
+
>
|
|
21
|
+
<button
|
|
22
|
+
v-for="section in sections"
|
|
23
|
+
:key="section.id"
|
|
24
|
+
type="button"
|
|
25
|
+
role="tab"
|
|
26
|
+
:title="section.title"
|
|
27
|
+
:aria-selected="activeId === section.id"
|
|
28
|
+
:data-state="activeId === section.id ? 'active' : 'inactive'"
|
|
29
|
+
class="inline-flex h-[calc(100%-1px)] min-w-0 flex-1 basis-0 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring data-[state=active]:bg-background data-[state=active]:shadow-sm dark:text-muted-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground"
|
|
30
|
+
@click="emit('update:activeId', section.id)"
|
|
31
|
+
>
|
|
32
|
+
{{ section.title }}
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|