@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,250 @@
1
+ <script setup lang="ts">
2
+ import { useScrollLock, onKeyStroke } from "@vueuse/core";
3
+ import { Button } from "#ginko-docs/components/ui/button";
4
+ import { Kbd } from "#ginko-docs/components/ui/kbd";
5
+ import { Separator } from "#ginko-docs/components/ui/separator";
6
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
7
+ import { useI18n, useRoute } from "#imports";
8
+ import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
9
+ import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
10
+ import { useCommandCenterState } from "#ginko-docs/features/search/useCommandCenter";
11
+ import ModeToggle from "#ginko-docs/components/site/ModeToggle.vue";
12
+
13
+ const { mainNav } = useSiteNavigation();
14
+ const { openCommandCenter } = useCommandCenterState();
15
+ const { t } = useI18n();
16
+ const route = useRoute();
17
+ const isMobileMenuOpen = ref(false);
18
+ const headerElement = ref<HTMLElement | null>(null);
19
+ const mobileMenuTop = ref("var(--site-header-height)");
20
+ const localizedPath = useLocalizedPath();
21
+
22
+ const isActive = (href: string) => route.path === href || route.path.startsWith(href + "/");
23
+
24
+ function mobileNavIcon(href: string) {
25
+ if (href.includes("blog")) return "lucide:file-text";
26
+ if (href.includes("dok") || href.includes("doc")) return "lucide:book-open";
27
+ return "lucide:circle";
28
+ }
29
+
30
+ const mobileNavItems = computed(() =>
31
+ mainNav.value.map((item) => ({
32
+ label: item.label,
33
+ href: item.href,
34
+ icon: mobileNavIcon(item.href),
35
+ children: [] as { label: string; href: string }[],
36
+ })),
37
+ );
38
+ const homePath = computed(() => localizedPath("home"));
39
+
40
+ // Scroll lock when menu is open
41
+ const scrollLock = useScrollLock(import.meta.client ? document.body : null);
42
+
43
+ watch(isMobileMenuOpen, (open) => {
44
+ scrollLock.value = open;
45
+ if (open) {
46
+ void nextTick(updateMobileMenuTop);
47
+ }
48
+ });
49
+
50
+ function updateMobileMenuTop() {
51
+ if (!import.meta.client || !headerElement.value) return;
52
+ mobileMenuTop.value = `${Math.max(0, headerElement.value.getBoundingClientRect().bottom)}px`;
53
+ }
54
+
55
+ function handleResize() {
56
+ if (isMobileMenuOpen.value) {
57
+ updateMobileMenuTop();
58
+ }
59
+ }
60
+
61
+ onMounted(() => window.addEventListener("resize", handleResize));
62
+ onBeforeUnmount(() => window.removeEventListener("resize", handleResize));
63
+
64
+ function openSearch() {
65
+ isMobileMenuOpen.value = false;
66
+ openCommandCenter();
67
+ }
68
+
69
+ // Close on Escape
70
+ onKeyStroke("Escape", () => {
71
+ if (isMobileMenuOpen.value) {
72
+ isMobileMenuOpen.value = false;
73
+ }
74
+ });
75
+
76
+ // Close menu on navigation
77
+ watch(
78
+ () => route.path,
79
+ () => {
80
+ isMobileMenuOpen.value = false;
81
+ },
82
+ );
83
+ </script>
84
+
85
+ <template>
86
+ <header
87
+ ref="headerElement"
88
+ 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
+ >
90
+ <div class="mx-auto flex w-full max-w-screen-2xl flex-1 items-center gap-4 px-4 md:px-6">
91
+ <NuxtLink
92
+ :to="homePath"
93
+ class="flex items-center gap-2 font-semibold text-foreground"
94
+ :aria-label="t('nav.home')"
95
+ >
96
+ <SiteLogoMark />
97
+ </NuxtLink>
98
+ <nav class="ml-4 hidden items-center gap-1 md:flex" :aria-label="t('nav.main')">
99
+ <NuxtLink
100
+ v-for="item in mainNav"
101
+ :key="item.href"
102
+ :to="item.href"
103
+ class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
104
+ :class="
105
+ isActive(item.href)
106
+ ? 'bg-muted text-foreground'
107
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
108
+ "
109
+ >
110
+ {{ item.label }}
111
+ </NuxtLink>
112
+ </nav>
113
+ <div class="ml-auto flex items-center gap-1.5">
114
+ <Button
115
+ variant="outline"
116
+ class="hidden h-9 w-52 justify-start gap-2 px-3 text-muted-foreground md:flex"
117
+ :aria-label="t('nav.search')"
118
+ type="button"
119
+ @click="openSearch"
120
+ >
121
+ <Icon name="lucide:search" class="size-4 shrink-0" aria-hidden="true" />
122
+ <span class="flex-1 text-left">{{ t("nav.search") }}...</span>
123
+ <Kbd class="h-5 bg-background text-[10px]">⌘K</Kbd>
124
+ </Button>
125
+ <Button
126
+ variant="ghost"
127
+ size="icon"
128
+ class="md:hidden"
129
+ :aria-label="t('nav.search')"
130
+ type="button"
131
+ @click="openSearch"
132
+ >
133
+ <Icon name="lucide:search" class="size-5" aria-hidden="true" />
134
+ </Button>
135
+
136
+ <Separator orientation="vertical" class="mx-0.5 hidden h-5 md:block" />
137
+
138
+ <ClientOnly>
139
+ <div class="hidden md:block">
140
+ <ModeToggle />
141
+ </div>
142
+
143
+ <template #fallback>
144
+ <span
145
+ class="hidden size-9 shrink-0 rounded-md border border-border bg-background md:block"
146
+ aria-hidden="true"
147
+ />
148
+ </template>
149
+ </ClientOnly>
150
+
151
+ <SiteLocaleSwitcher class="hidden md:flex" />
152
+ <Button
153
+ variant="ghost"
154
+ size="icon"
155
+ class="md:hidden"
156
+ :aria-label="isMobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')"
157
+ :aria-expanded="isMobileMenuOpen"
158
+ type="button"
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
221
+ 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
+ >
225
+ <span
226
+ class="flex size-11 shrink-0 items-center justify-center rounded-lg bg-muted/60 text-foreground"
227
+ >
228
+ <Icon name="lucide:search" class="size-5" aria-hidden="true" />
229
+ </span>
230
+ <span class="min-w-0 flex-1 truncate">{{ t("nav.search") }}</span>
231
+ <Icon
232
+ name="lucide:chevron-right"
233
+ class="size-5 shrink-0 text-muted-foreground"
234
+ aria-hidden="true"
235
+ />
236
+ </button>
237
+ <div class="grid grid-cols-2 border-t border-border">
238
+ <ModeToggle variant="menu-tile" class="border-r border-border" />
239
+ <SiteLocaleSwitcher
240
+ variant="menu-tile"
241
+ class="flex"
242
+ @navigate="isMobileMenuOpen = false"
243
+ />
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </Transition>
250
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import { defineAsyncComponent, onBeforeUnmount, onMounted } from "vue";
3
+ import SiteSearchPageHighlight from "#ginko-docs/features/search/components/SiteSearchPageHighlight.vue";
4
+ import { useCommandCenterState } from "#ginko-docs/features/search/useCommandCenter";
5
+
6
+ const SiteCommandCenter = defineAsyncComponent(
7
+ () => import("#ginko-docs/features/search/components/SiteCommandCenter.vue"),
8
+ );
9
+ const { open, openCommandCenter } = useCommandCenterState();
10
+
11
+ function handleGlobalShortcut(event: KeyboardEvent) {
12
+ const element = event.target as HTMLElement | null;
13
+ const typing =
14
+ element instanceof HTMLElement &&
15
+ (element.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName));
16
+
17
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
18
+ event.preventDefault();
19
+ openCommandCenter();
20
+ } else if (!typing && event.key === "/") {
21
+ event.preventDefault();
22
+ openCommandCenter();
23
+ }
24
+ }
25
+
26
+ onMounted(() => window.addEventListener("keydown", handleGlobalShortcut));
27
+ onBeforeUnmount(() => window.removeEventListener("keydown", handleGlobalShortcut));
28
+ </script>
29
+
30
+ <template>
31
+ <ClientOnly>
32
+ <SiteCommandCenter v-if="open" />
33
+ <SiteSearchPageHighlight />
34
+ </ClientOnly>
35
+ </template>
@@ -0,0 +1,268 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { Button } from "#ginko-docs/components/ui/button";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "#ginko-docs/components/ui/dropdown-menu";
10
+ import { cn } from "#ginko-docs/lib/utils";
11
+ import { computed } from "vue";
12
+ import { useI18n } from "#imports";
13
+ import { useLocalizedRouteSwitch } from "#ginko-docs/composables/useLocalizedRouteSwitch";
14
+ import { locales as configuredLocales } from "../../../i18n/locales";
15
+
16
+ type LocaleSwitcherVariant = "dropdown" | "menu-row" | "menu-tile" | "segmented";
17
+
18
+ type LocaleEntry = {
19
+ code: string;
20
+ language?: string;
21
+ name?: string;
22
+ nativeName?: string;
23
+ shortLabel?: string;
24
+ flagIcon?: string;
25
+ };
26
+
27
+ const props = withDefaults(
28
+ defineProps<{
29
+ variant?: LocaleSwitcherVariant;
30
+ class?: HTMLAttributes["class"];
31
+ }>(),
32
+ {
33
+ variant: "dropdown",
34
+ },
35
+ );
36
+
37
+ const emit = defineEmits<{
38
+ navigate: [];
39
+ }>();
40
+
41
+ const { locale, locales, t } = useI18n();
42
+ const { switchPath } = useLocalizedRouteSwitch();
43
+
44
+ type ConfiguredLocale = (typeof configuredLocales)[number];
45
+
46
+ const configuredLocaleMap: ReadonlyMap<string, ConfiguredLocale> = new Map(
47
+ configuredLocales.map((entry) => [entry.code, entry]),
48
+ );
49
+
50
+ function normalizeLocale(entry: string | LocaleEntry) {
51
+ if (typeof entry === "string") {
52
+ const configured: LocaleEntry | undefined = configuredLocaleMap.get(entry);
53
+ return {
54
+ code: entry,
55
+ language: configured?.language ?? entry,
56
+ name: configured?.name ?? entry.toUpperCase(),
57
+ nativeName: configured?.nativeName ?? configured?.name ?? entry.toUpperCase(),
58
+ shortLabel: configured?.shortLabel ?? entry.toUpperCase(),
59
+ flagIcon: configured?.flagIcon,
60
+ };
61
+ }
62
+
63
+ const configured: LocaleEntry | undefined = configuredLocaleMap.get(entry.code);
64
+ return {
65
+ code: entry.code,
66
+ language: entry.language ?? configured?.language ?? entry.code,
67
+ name: entry.name ?? configured?.name ?? entry.code.toUpperCase(),
68
+ nativeName:
69
+ entry.nativeName ?? entry.name ?? configured?.nativeName ?? entry.code.toUpperCase(),
70
+ shortLabel: entry.shortLabel ?? configured?.shortLabel ?? entry.code.toUpperCase(),
71
+ flagIcon: entry.flagIcon ?? configured?.flagIcon,
72
+ };
73
+ }
74
+
75
+ const localeLinks = computed(() =>
76
+ locales.value.map((entry) => {
77
+ const normalized = normalizeLocale(entry as string | LocaleEntry);
78
+ return {
79
+ ...normalized,
80
+ to: switchPath(normalized.code) || "/",
81
+ current: locale.value === normalized.code,
82
+ };
83
+ }),
84
+ );
85
+
86
+ const currentLocale = computed(
87
+ () => localeLinks.value.find((entry) => entry.current) ?? localeLinks.value[0],
88
+ );
89
+ const currentMobileLocaleLabel = computed(() => {
90
+ const code = currentLocale.value?.code;
91
+ if (!code) return "";
92
+ return currentLocale.value?.name ?? code.toUpperCase();
93
+ });
94
+
95
+ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unknown }) {
96
+ emit("navigate");
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <div
102
+ v-if="localeLinks.length > 1 && variant === 'dropdown'"
103
+ :class="cn('items-center', props.class)"
104
+ >
105
+ <ClientOnly>
106
+ <DropdownMenu>
107
+ <DropdownMenuTrigger as-child>
108
+ <Button variant="outline" class="h-9 gap-1.5 px-2.5" :aria-label="t('nav.language')">
109
+ <Icon
110
+ v-if="currentLocale?.flagIcon"
111
+ :name="currentLocale.flagIcon"
112
+ class="size-4 rounded-full"
113
+ aria-hidden="true"
114
+ />
115
+ <span class="text-xs font-semibold">{{ currentLocale?.shortLabel }}</span>
116
+ <Icon
117
+ name="lucide:chevron-down"
118
+ class="size-3.5 text-muted-foreground"
119
+ aria-hidden="true"
120
+ />
121
+ </Button>
122
+ </DropdownMenuTrigger>
123
+ <DropdownMenuContent align="end" class="w-44">
124
+ <DropdownMenuItem v-for="entry in localeLinks" :key="entry.code" as-child>
125
+ <NuxtLink
126
+ :to="entry.to"
127
+ :hreflang="entry.language"
128
+ :lang="entry.code"
129
+ :aria-label="entry.name"
130
+ :aria-current="entry.current ? 'page' : undefined"
131
+ class="flex w-full items-center gap-2"
132
+ @click="trackLocaleNavigation(entry)"
133
+ >
134
+ <Icon
135
+ v-if="entry.flagIcon"
136
+ :name="entry.flagIcon"
137
+ class="size-4 rounded-full"
138
+ aria-hidden="true"
139
+ />
140
+ <span class="flex-1">{{ entry.nativeName }}</span>
141
+ <span class="text-xs text-muted-foreground">{{ entry.shortLabel }}</span>
142
+ <Icon
143
+ v-if="entry.current"
144
+ name="lucide:check"
145
+ class="size-4 text-foreground"
146
+ aria-hidden="true"
147
+ />
148
+ </NuxtLink>
149
+ </DropdownMenuItem>
150
+ </DropdownMenuContent>
151
+ </DropdownMenu>
152
+
153
+ <template #fallback>
154
+ <span
155
+ class="inline-flex h-9 w-[4.25rem] rounded-md border border-border bg-background"
156
+ aria-hidden="true"
157
+ />
158
+ </template>
159
+ </ClientOnly>
160
+ </div>
161
+
162
+ <div
163
+ v-else-if="localeLinks.length > 1 && (variant === 'menu-row' || variant === 'menu-tile')"
164
+ :class="cn('items-center', props.class)"
165
+ >
166
+ <DropdownMenu>
167
+ <DropdownMenuTrigger as-child>
168
+ <Button
169
+ variant="ghost"
170
+ :class="
171
+ cn(
172
+ variant === 'menu-tile'
173
+ ? 'h-14 w-full justify-between rounded-none px-3.5 text-sm font-semibold hover:bg-muted/40'
174
+ : 'h-[4.25rem] w-full justify-between rounded-none px-5 text-base font-semibold hover:bg-transparent',
175
+ )
176
+ "
177
+ :aria-label="t('nav.language')"
178
+ >
179
+ <span
180
+ class="flex min-w-0 items-center"
181
+ :class="variant === 'menu-tile' ? 'gap-3' : 'gap-4'"
182
+ >
183
+ <span
184
+ v-if="variant === 'menu-tile' && currentLocale?.flagIcon"
185
+ class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted/60"
186
+ >
187
+ <Icon :name="currentLocale.flagIcon" class="size-5 rounded-full" aria-hidden="true" />
188
+ </span>
189
+ <Icon
190
+ v-else-if="currentLocale?.flagIcon"
191
+ :name="currentLocale.flagIcon"
192
+ class="size-7 shrink-0 rounded-full"
193
+ aria-hidden="true"
194
+ />
195
+ <span class="truncate">
196
+ {{ variant === "menu-tile" ? currentMobileLocaleLabel : currentLocale?.shortLabel }}
197
+ </span>
198
+ </span>
199
+ <Icon
200
+ name="lucide:chevron-down"
201
+ class="text-muted-foreground"
202
+ :class="variant === 'menu-tile' ? 'size-4' : 'size-5'"
203
+ aria-hidden="true"
204
+ />
205
+ </Button>
206
+ </DropdownMenuTrigger>
207
+ <DropdownMenuContent align="end" class="w-44">
208
+ <DropdownMenuItem v-for="entry in localeLinks" :key="entry.code" as-child>
209
+ <NuxtLink
210
+ :to="entry.to"
211
+ :hreflang="entry.language"
212
+ :lang="entry.code"
213
+ :aria-label="entry.name"
214
+ :aria-current="entry.current ? 'page' : undefined"
215
+ class="flex w-full items-center gap-2"
216
+ @click="trackLocaleNavigation(entry)"
217
+ >
218
+ <Icon
219
+ v-if="entry.flagIcon"
220
+ :name="entry.flagIcon"
221
+ class="size-4 rounded-full"
222
+ aria-hidden="true"
223
+ />
224
+ <span class="flex-1">{{ entry.nativeName }}</span>
225
+ <span class="text-xs text-muted-foreground">{{ entry.shortLabel }}</span>
226
+ <Icon
227
+ v-if="entry.current"
228
+ name="lucide:check"
229
+ class="size-4 text-foreground"
230
+ aria-hidden="true"
231
+ />
232
+ </NuxtLink>
233
+ </DropdownMenuItem>
234
+ </DropdownMenuContent>
235
+ </DropdownMenu>
236
+ </div>
237
+
238
+ <div
239
+ v-else-if="localeLinks.length > 1"
240
+ :class="cn('items-center gap-1 rounded-md border border-border p-0.5', props.class)"
241
+ :aria-label="t('nav.language')"
242
+ >
243
+ <NuxtLink
244
+ v-for="entry in localeLinks"
245
+ :key="entry.code"
246
+ :to="entry.to"
247
+ :hreflang="entry.language"
248
+ :lang="entry.code"
249
+ :aria-label="entry.name"
250
+ :aria-current="entry.current ? 'page' : undefined"
251
+ class="inline-flex h-8 items-center gap-1.5 rounded px-2 text-xs font-medium transition-colors"
252
+ :class="
253
+ entry.current
254
+ ? 'bg-muted text-foreground'
255
+ : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
256
+ "
257
+ @click="trackLocaleNavigation(entry)"
258
+ >
259
+ <Icon
260
+ v-if="entry.flagIcon"
261
+ :name="entry.flagIcon"
262
+ class="size-4 rounded-full"
263
+ aria-hidden="true"
264
+ />
265
+ <span>{{ entry.shortLabel }}</span>
266
+ </NuxtLink>
267
+ </div>
268
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
3
+
4
+ const { site } = useSiteNavigation();
5
+ </script>
6
+
7
+ <template>
8
+ <span class="relative inline-flex h-7 shrink-0" aria-hidden="true">
9
+ <img :src="site.logo.light" alt="" class="h-7 w-auto dark:hidden" loading="eager" />
10
+ <img :src="site.logo.dark" alt="" class="hidden h-7 w-auto dark:block" loading="eager" />
11
+ </span>
12
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "#imports";
3
+
4
+ const { t } = useI18n();
5
+ </script>
6
+
7
+ <template>
8
+ <a
9
+ href="#main-content"
10
+ class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-100 focus:rounded-md focus:border focus:border-border focus:bg-background focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-md"
11
+ >
12
+ {{ t("nav.skip") }}
13
+ </a>
14
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import type { AccordionRootEmits, AccordionRootProps } from "reka-ui";
3
+ import { AccordionRoot, useForwardPropsEmits } from "reka-ui";
4
+
5
+ const props = defineProps<AccordionRootProps>();
6
+ const emits = defineEmits<AccordionRootEmits>();
7
+
8
+ const forwarded = useForwardPropsEmits(props, emits);
9
+ </script>
10
+
11
+ <template>
12
+ <AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
13
+ <slot v-bind="slotProps" />
14
+ </AccordionRoot>
15
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import type { AccordionContentProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { AccordionContent } from "reka-ui";
6
+ import { cn } from "#ginko-docs/lib/utils";
7
+
8
+ const props = defineProps<AccordionContentProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+ </script>
12
+
13
+ <template>
14
+ <AccordionContent
15
+ data-slot="accordion-content"
16
+ v-bind="delegatedProps"
17
+ class="accordion-content overflow-hidden text-sm"
18
+ >
19
+ <div :class="cn('pt-0 pb-4', props.class)">
20
+ <slot />
21
+ </div>
22
+ </AccordionContent>
23
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import type { AccordionItemProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { AccordionItem, useForwardProps } from "reka-ui";
6
+ import { cn } from "#ginko-docs/lib/utils";
7
+
8
+ const props = defineProps<AccordionItemProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps);
13
+ </script>
14
+
15
+ <template>
16
+ <AccordionItem
17
+ v-slot="slotProps"
18
+ data-slot="accordion-item"
19
+ v-bind="forwardedProps"
20
+ :class="cn('border-b last:border-b-0', props.class)"
21
+ >
22
+ <slot v-bind="slotProps" />
23
+ </AccordionItem>
24
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import type { AccordionTriggerProps } from "reka-ui";
3
+ import type { HTMLAttributes } from "vue";
4
+ import { reactiveOmit } from "@vueuse/core";
5
+ import { AccordionHeader, AccordionTrigger } from "reka-ui";
6
+ import { cn } from "#ginko-docs/lib/utils";
7
+
8
+ const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes["class"] }>();
9
+
10
+ const delegatedProps = reactiveOmit(props, "class");
11
+ </script>
12
+
13
+ <template>
14
+ <AccordionHeader class="flex">
15
+ <AccordionTrigger
16
+ data-slot="accordion-trigger"
17
+ v-bind="delegatedProps"
18
+ :class="
19
+ cn(
20
+ 'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
21
+ props.class,
22
+ )
23
+ "
24
+ >
25
+ <slot />
26
+ <slot name="icon">
27
+ <Icon
28
+ name="lucide:chevron-down"
29
+ class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200"
30
+ aria-hidden="true"
31
+ />
32
+ </slot>
33
+ </AccordionTrigger>
34
+ </AccordionHeader>
35
+ </template>