@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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/app/app.config.ts +49 -0
  4. package/app/app.vue +71 -0
  5. package/app/assets/css/prose.css +1154 -0
  6. package/app/assets/css/tailwind.css +365 -0
  7. package/app/components/OgImage/GinkoDocs.satori.vue +117 -0
  8. package/app/components/content/DocumentPageShell.vue +55 -0
  9. package/app/components/content/Feedback.vue +97 -0
  10. package/app/components/content/PageMarkdownCopy.vue +102 -0
  11. package/app/components/mdc/MdcAccordion.vue +64 -0
  12. package/app/components/mdc/MdcAccordionItem.vue +51 -0
  13. package/app/components/mdc/MdcAlert.vue +34 -0
  14. package/app/components/mdc/MdcAside.vue +27 -0
  15. package/app/components/mdc/MdcCallout.vue +62 -0
  16. package/app/components/mdc/MdcCard.vue +186 -0
  17. package/app/components/mdc/MdcCardGroup.vue +47 -0
  18. package/app/components/mdc/MdcCenter.vue +71 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +193 -0
  20. package/app/components/mdc/MdcColumn.vue +70 -0
  21. package/app/components/mdc/MdcDocImg.vue +25 -0
  22. package/app/components/mdc/MdcDropcap.vue +84 -0
  23. package/app/components/mdc/MdcError.vue +16 -0
  24. package/app/components/mdc/MdcField.vue +62 -0
  25. package/app/components/mdc/MdcFieldGroup.vue +18 -0
  26. package/app/components/mdc/MdcFigure.vue +51 -0
  27. package/app/components/mdc/MdcIdea.vue +16 -0
  28. package/app/components/mdc/MdcInfo.vue +16 -0
  29. package/app/components/mdc/MdcLayout.vue +57 -0
  30. package/app/components/mdc/MdcNote.vue +16 -0
  31. package/app/components/mdc/MdcPassage.vue +32 -0
  32. package/app/components/mdc/MdcQuiz.vue +281 -0
  33. package/app/components/mdc/MdcQuizOption.vue +91 -0
  34. package/app/components/mdc/MdcQuizQuestion.vue +226 -0
  35. package/app/components/mdc/MdcReadMore.vue +90 -0
  36. package/app/components/mdc/MdcReadMoreGroup.vue +18 -0
  37. package/app/components/mdc/MdcShortcut.vue +46 -0
  38. package/app/components/mdc/MdcStep.vue +42 -0
  39. package/app/components/mdc/MdcSteps.vue +25 -0
  40. package/app/components/mdc/MdcSuccess.vue +16 -0
  41. package/app/components/mdc/MdcTab.vue +17 -0
  42. package/app/components/mdc/MdcTabs.vue +132 -0
  43. package/app/components/mdc/MdcTimeline.vue +14 -0
  44. package/app/components/mdc/MdcTimelineItem.vue +49 -0
  45. package/app/components/mdc/MdcWarning.vue +16 -0
  46. package/app/components/mdc/icons.ts +10 -0
  47. package/app/components/prose/ProseA.vue +27 -0
  48. package/app/components/prose/ProseBlockquote.vue +14 -0
  49. package/app/components/prose/ProseCode.vue +14 -0
  50. package/app/components/prose/ProseEm.vue +14 -0
  51. package/app/components/prose/ProseH1.vue +18 -0
  52. package/app/components/prose/ProseH2.vue +24 -0
  53. package/app/components/prose/ProseH3.vue +24 -0
  54. package/app/components/prose/ProseH4.vue +14 -0
  55. package/app/components/prose/ProseHr.vue +12 -0
  56. package/app/components/prose/ProseImg.vue +120 -0
  57. package/app/components/prose/ProseLi.vue +14 -0
  58. package/app/components/prose/ProseOl.vue +14 -0
  59. package/app/components/prose/ProseP.vue +14 -0
  60. package/app/components/prose/ProsePre.vue +217 -0
  61. package/app/components/prose/ProseStrong.vue +14 -0
  62. package/app/components/prose/ProseTable.vue +16 -0
  63. package/app/components/prose/ProseTbody.vue +14 -0
  64. package/app/components/prose/ProseTd.vue +14 -0
  65. package/app/components/prose/ProseTh.vue +14 -0
  66. package/app/components/prose/ProseThead.vue +14 -0
  67. package/app/components/prose/ProseTr.vue +14 -0
  68. package/app/components/prose/ProseUl.vue +14 -0
  69. package/app/components/site/ModeToggle.vue +123 -0
  70. package/app/components/site/SiteBanner.vue +56 -0
  71. package/app/components/site/SiteFooter.vue +80 -0
  72. package/app/components/site/SiteHeader.vue +250 -0
  73. package/app/components/site/SiteInteractionLayer.vue +35 -0
  74. package/app/components/site/SiteLocaleSwitcher.vue +268 -0
  75. package/app/components/site/SiteLogoMark.vue +12 -0
  76. package/app/components/site/SiteSkipLink.vue +14 -0
  77. package/app/components/ui/accordion/Accordion.vue +15 -0
  78. package/app/components/ui/accordion/AccordionContent.vue +23 -0
  79. package/app/components/ui/accordion/AccordionItem.vue +24 -0
  80. package/app/components/ui/accordion/AccordionTrigger.vue +35 -0
  81. package/app/components/ui/accordion/index.ts +4 -0
  82. package/app/components/ui/alert/Alert.vue +21 -0
  83. package/app/components/ui/alert/AlertDescription.vue +22 -0
  84. package/app/components/ui/alert/AlertTitle.vue +22 -0
  85. package/app/components/ui/alert/index.ts +34 -0
  86. package/app/components/ui/badge/Badge.vue +28 -0
  87. package/app/components/ui/badge/index.ts +24 -0
  88. package/app/components/ui/button/Button.vue +31 -0
  89. package/app/components/ui/button/index.ts +35 -0
  90. package/app/components/ui/card/Card.vue +16 -0
  91. package/app/components/ui/card/CardAction.vue +17 -0
  92. package/app/components/ui/card/CardContent.vue +14 -0
  93. package/app/components/ui/card/CardDescription.vue +14 -0
  94. package/app/components/ui/card/CardFooter.vue +14 -0
  95. package/app/components/ui/card/CardHeader.vue +22 -0
  96. package/app/components/ui/card/CardTitle.vue +14 -0
  97. package/app/components/ui/card/card-variants.ts +15 -0
  98. package/app/components/ui/card/index.ts +8 -0
  99. package/app/components/ui/checkbox/Checkbox.vue +35 -0
  100. package/app/components/ui/checkbox/index.ts +1 -0
  101. package/app/components/ui/collapsible/Collapsible.vue +15 -0
  102. package/app/components/ui/collapsible/CollapsibleContent.vue +25 -0
  103. package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  104. package/app/components/ui/collapsible/index.ts +3 -0
  105. package/app/components/ui/dialog/Dialog.vue +15 -0
  106. package/app/components/ui/dialog/DialogClose.vue +12 -0
  107. package/app/components/ui/dialog/DialogContent.vue +69 -0
  108. package/app/components/ui/dialog/DialogDescription.vue +21 -0
  109. package/app/components/ui/dialog/DialogFooter.vue +15 -0
  110. package/app/components/ui/dialog/DialogHeader.vue +12 -0
  111. package/app/components/ui/dialog/DialogOverlay.vue +26 -0
  112. package/app/components/ui/dialog/DialogPortal.vue +12 -0
  113. package/app/components/ui/dialog/DialogTitle.vue +21 -0
  114. package/app/components/ui/dialog/DialogTrigger.vue +12 -0
  115. package/app/components/ui/dialog/index.ts +10 -0
  116. package/app/components/ui/dropdown-menu/DropdownMenu.vue +15 -0
  117. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +36 -0
  118. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +40 -0
  119. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  120. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  121. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +25 -0
  122. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +15 -0
  123. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +37 -0
  124. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  125. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  126. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +15 -0
  127. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +29 -0
  128. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  129. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +14 -0
  130. package/app/components/ui/dropdown-menu/index.ts +16 -0
  131. package/app/components/ui/input/Input.vue +23 -0
  132. package/app/components/ui/input/index.ts +1 -0
  133. package/app/components/ui/kbd/Kbd.vue +23 -0
  134. package/app/components/ui/kbd/KbdGroup.vue +14 -0
  135. package/app/components/ui/kbd/index.ts +22 -0
  136. package/app/components/ui/label/Label.vue +14 -0
  137. package/app/components/ui/label/index.ts +1 -0
  138. package/app/components/ui/scroll-area/ScrollArea.vue +39 -0
  139. package/app/components/ui/scroll-area/ScrollBar.vue +33 -0
  140. package/app/components/ui/scroll-area/index.ts +2 -0
  141. package/app/components/ui/separator/Separator.vue +27 -0
  142. package/app/components/ui/separator/index.ts +1 -0
  143. package/app/components/ui/sheet/Sheet.vue +15 -0
  144. package/app/components/ui/sheet/SheetClose.vue +12 -0
  145. package/app/components/ui/sheet/SheetContent.vue +59 -0
  146. package/app/components/ui/sheet/SheetDescription.vue +21 -0
  147. package/app/components/ui/sheet/SheetFooter.vue +12 -0
  148. package/app/components/ui/sheet/SheetHeader.vue +12 -0
  149. package/app/components/ui/sheet/SheetOverlay.vue +26 -0
  150. package/app/components/ui/sheet/SheetTitle.vue +21 -0
  151. package/app/components/ui/sheet/SheetTrigger.vue +12 -0
  152. package/app/components/ui/sheet/index.ts +8 -0
  153. package/app/components/ui/switch/Switch.vue +39 -0
  154. package/app/components/ui/switch/index.ts +1 -0
  155. package/app/components/ui/tabs/Tabs.vue +24 -0
  156. package/app/components/ui/tabs/TabsContent.vue +21 -0
  157. package/app/components/ui/tabs/TabsList.vue +26 -0
  158. package/app/components/ui/tabs/TabsTrigger.vue +28 -0
  159. package/app/components/ui/tabs/index.ts +4 -0
  160. package/app/components/ui/textarea/Textarea.vue +31 -0
  161. package/app/components/ui/textarea/index.ts +1 -0
  162. package/app/composables/site-navigation.utils.ts +80 -0
  163. package/app/composables/useCanonicalUrl.ts +9 -0
  164. package/app/composables/useContentRouteAlternates.ts +43 -0
  165. package/app/composables/useGinkoAnalytics.ts +37 -0
  166. package/app/composables/useGinkoDocsConfig.ts +6 -0
  167. package/app/composables/useGinkoOgImage.ts +20 -0
  168. package/app/composables/useLocalizedPath.ts +12 -0
  169. package/app/composables/useLocalizedRouteSwitch.ts +31 -0
  170. package/app/composables/usePageMarkdownActions.ts +79 -0
  171. package/app/composables/useSchemaJsonLd.ts +25 -0
  172. package/app/composables/useSiteNavigation.ts +86 -0
  173. package/app/config/site.utils.ts +36 -0
  174. package/app/error.vue +125 -0
  175. package/app/features/docs/components/DocsContributeLinks.vue +65 -0
  176. package/app/features/docs/components/DocsMobileToc.vue +104 -0
  177. package/app/features/docs/components/DocsPageContent.vue +188 -0
  178. package/app/features/docs/components/DocsPageNav.vue +57 -0
  179. package/app/features/docs/components/DocsSidebar.vue +129 -0
  180. package/app/features/docs/components/DocsSidebarDropdown.vue +70 -0
  181. package/app/features/docs/components/DocsSidebarItem.vue +169 -0
  182. package/app/features/docs/components/DocsSidebarList.vue +35 -0
  183. package/app/features/docs/components/DocsSidebarTabs.vue +35 -0
  184. package/app/features/docs/components/DocsToc.vue +60 -0
  185. package/app/features/docs/composables/useDocsEntryPath.ts +13 -0
  186. package/app/features/docs/composables/useDocsNavigation.ts +29 -0
  187. package/app/features/docs/composables/useDocsNavigationData.ts +15 -0
  188. package/app/features/docs/composables/useScrollspy.ts +67 -0
  189. package/app/features/docs/docs-navigation.ts +170 -0
  190. package/app/features/search/command-center.ts +156 -0
  191. package/app/features/search/components/SiteCommandCenter.vue +323 -0
  192. package/app/features/search/components/SiteSearchPageHighlight.vue +124 -0
  193. package/app/features/search/useCommandCenter.ts +300 -0
  194. package/app/layouts/blog.vue +15 -0
  195. package/app/layouts/default.vue +12 -0
  196. package/app/layouts/docs.vue +20 -0
  197. package/app/lib/errors.ts +84 -0
  198. package/app/lib/schema-org.ts +38 -0
  199. package/app/lib/utils.ts +7 -0
  200. package/app/pages/blog/[slug].vue +211 -0
  201. package/app/pages/blog/index.vue +106 -0
  202. package/app/pages/docs/[...slug].vue +10 -0
  203. package/app/pages/docs/index.vue +23 -0
  204. package/app/pages/index.vue +140 -0
  205. package/app/plugins/ssr-width.ts +6 -0
  206. package/app/utils/content.ts +56 -0
  207. package/app/utils/index.ts +1 -0
  208. package/components.ts +9 -0
  209. package/content.js +175 -0
  210. package/content.ts +148 -0
  211. package/i18n/i18n.config.ts +10 -0
  212. package/i18n/locales.ts +81 -0
  213. package/i18n/messages/global/banner.ts +8 -0
  214. package/i18n/messages/global/blog.ts +14 -0
  215. package/i18n/messages/global/command.ts +44 -0
  216. package/i18n/messages/global/docs.ts +32 -0
  217. package/i18n/messages/global/errors.ts +36 -0
  218. package/i18n/messages/global/feedback.ts +11 -0
  219. package/i18n/messages/global/index.ts +21 -0
  220. package/i18n/messages/global/nav.ts +18 -0
  221. package/i18n/messages/global/site.ts +4 -0
  222. package/i18n/messages/global/theme.ts +7 -0
  223. package/i18n/messages/index.ts +20 -0
  224. package/i18n/messages/localize.ts +35 -0
  225. package/i18n/messages/source.ts +5 -0
  226. package/i18n/routes.ts +33 -0
  227. package/icon-bundle.ts +141 -0
  228. package/index.d.ts +13 -0
  229. package/modules/feature-routing.ts +32 -0
  230. package/nuxt.config.ts +194 -0
  231. package/package.json +91 -0
  232. package/runtime/markdown/inline-code-highlight.ts +155 -0
  233. package/runtime/server/agent-markdown.ts +21 -0
  234. package/runtime/utils.ts +7 -0
  235. package/server/mcp/tools/get-page.ts +14 -0
  236. package/server/mcp/tools/list-pages.ts +10 -0
  237. package/server/plugins/agent-markdown.ts +6 -0
  238. package/shared/route-slugs.ts +5 -0
  239. package/shared/types/app-config.ts +100 -0
  240. 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>