@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,106 @@
1
+ <script setup lang="ts">
2
+ import { Badge } from "#ginko-docs/components/ui/badge";
3
+ import { many } from "@lupinum/ginko-content/client";
4
+ import { formatContentDate } from "#ginko-docs/utils/content";
5
+ import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
6
+ import { computed } from "vue";
7
+ import { definePageMeta, useAppConfig, useAsyncData, useHead, useI18n, useSeoMeta } from "#imports";
8
+ import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
9
+
10
+ definePageMeta({ layout: "blog" });
11
+
12
+ const { locale, t } = useI18n();
13
+ const postsKey = computed(() => `blog-posts:${locale.value}`);
14
+ const { data: posts } = await useAsyncData(
15
+ postsKey,
16
+ () =>
17
+ many("blog", {
18
+ locale: locale.value,
19
+ fallback: true,
20
+ populate: { author: "authors" },
21
+ sort: { date: "desc" },
22
+ limit: 50,
23
+ }),
24
+ { watch: [locale] },
25
+ );
26
+ const canonicalUrl = useCanonicalUrl();
27
+ const config = useAppConfig().ginkoDocs;
28
+ const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
29
+ const fullTitle = computed(() => `${t("blog.pageTitle")} - ${siteName.value}`);
30
+
31
+ useSeoMeta({
32
+ title: fullTitle,
33
+ description: computed(() => t("blog.description")),
34
+ ogTitle: fullTitle,
35
+ ogDescription: computed(() => t("blog.description")),
36
+ ogUrl: canonicalUrl,
37
+ twitterCard: "summary_large_image",
38
+ });
39
+
40
+ useHead(() => ({
41
+ link: [{ key: "canonical", rel: "canonical", href: canonicalUrl.value }],
42
+ }));
43
+ </script>
44
+
45
+ <template>
46
+ <div class="mx-auto max-w-3xl">
47
+ <div class="mb-12">
48
+ <h1 class="text-3xl font-semibold tracking-tight text-foreground">{{ t("blog.title") }}</h1>
49
+ <p class="mt-2 text-muted-foreground">{{ t("blog.description") }}</p>
50
+ </div>
51
+
52
+ <div class="divide-y divide-border">
53
+ <article v-for="post in posts" :key="post.route.resolvedPath" class="group py-8">
54
+ <NuxtLink
55
+ :to="post.route.resolvedPath"
56
+ class="flex flex-col gap-6 sm:flex-row sm:items-start"
57
+ >
58
+ <div class="min-w-0 flex-1">
59
+ <div class="mb-3 flex items-center gap-2">
60
+ <Badge v-if="post.badge" variant="secondary" class="text-xs">{{ post.badge }}</Badge>
61
+ <span class="text-xs text-muted-foreground"
62
+ >{{ formatContentDate(post.date, locale)
63
+ }}<template v-if="post.readingTime"> · {{ post.readingTime }}</template></span
64
+ >
65
+ </div>
66
+ <div v-if="post.author" class="mb-3 flex items-center gap-2">
67
+ <img
68
+ v-if="post.author.avatar"
69
+ :src="post.author.avatar"
70
+ :alt="post.author.name"
71
+ class="size-5 shrink-0 rounded-full border border-border object-cover"
72
+ loading="lazy"
73
+ />
74
+ <p class="text-xs font-medium text-muted-foreground">
75
+ {{ post.author.name }} · {{ post.author.role }}
76
+ </p>
77
+ </div>
78
+ <h2
79
+ class="mb-2 text-xl font-semibold text-foreground transition-colors group-hover:text-primary"
80
+ >
81
+ {{ post.title }}
82
+ </h2>
83
+ <p class="text-sm leading-relaxed text-muted-foreground">{{ post.description }}</p>
84
+ <div class="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
85
+ {{ t("blog.readMore") }}
86
+ <Icon
87
+ name="lucide:arrow-right"
88
+ class="size-4 transition-transform group-hover:translate-x-0.5"
89
+ aria-hidden="true"
90
+ />
91
+ </div>
92
+ </div>
93
+ <NuxtImg
94
+ v-if="post.image"
95
+ :src="post.image"
96
+ :alt="post.title"
97
+ class="aspect-[16/10] w-full shrink-0 rounded-lg border border-border object-cover sm:w-52"
98
+ width="416"
99
+ height="260"
100
+ loading="lazy"
101
+ />
102
+ </NuxtLink>
103
+ </article>
104
+ </div>
105
+ </div>
106
+ </template>
@@ -0,0 +1,10 @@
1
+ <script setup lang="ts">
2
+ import DocsPageContent from "#ginko-docs/features/docs/components/DocsPageContent.vue";
3
+ import { definePageMeta } from "#imports";
4
+
5
+ definePageMeta({ layout: "docs" });
6
+ </script>
7
+
8
+ <template>
9
+ <DocsPageContent :key="$route.path" />
10
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import { definePageMeta, navigateTo, useRoute } from "#imports";
3
+ import { useDocsEntryPath } from "#ginko-docs/features/docs/composables/useDocsEntryPath";
4
+
5
+ definePageMeta({ layout: "docs" });
6
+
7
+ const route = useRoute();
8
+ const entryPath = await useDocsEntryPath();
9
+
10
+ // Redirect the bare docs root to the first documentation page. Guard against
11
+ // self-redirects when no docs pages exist (entry path falls back to the root).
12
+ if (entryPath.value && entryPath.value !== route.path) {
13
+ await navigateTo(entryPath.value, { redirectCode: 302, replace: true });
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <main id="main-content" class="flex-1 px-4 py-12 md:px-6">
19
+ <NuxtLink v-if="entryPath && entryPath !== route.path" :to="entryPath" class="underline">
20
+ {{ entryPath }}
21
+ </NuxtLink>
22
+ </main>
23
+ </template>
@@ -0,0 +1,140 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useI18n, useSeoMeta } from "#imports";
4
+ import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
5
+ import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
6
+
7
+ const config = useGinkoDocsConfig();
8
+ const { locale } = useI18n();
9
+ const localize = (value: string | { en: string; de?: string }) =>
10
+ getLocalizedSiteText(value, locale.value);
11
+ const landing = computed(() => ({
12
+ eyebrow: config.landing.eyebrow ? localize(config.landing.eyebrow) : undefined,
13
+ title: localize(config.landing.title),
14
+ description: localize(config.landing.description),
15
+ primary: {
16
+ label: localize(config.landing.primary.label),
17
+ to: localize(config.landing.primary.to),
18
+ },
19
+ secondary: config.landing.secondary
20
+ ? {
21
+ label: localize(config.landing.secondary.label),
22
+ to: localize(config.landing.secondary.to),
23
+ }
24
+ : undefined,
25
+ features: config.landing.features.map((feature) => ({
26
+ ...feature,
27
+ title: localize(feature.title),
28
+ description: localize(feature.description),
29
+ })),
30
+ heroMedia: config.landing.hero?.media,
31
+ }));
32
+
33
+ const heroCodeLines = computed(() =>
34
+ landing.value.heroMedia?.type === "code" ? landing.value.heroMedia.code.split("\n") : [],
35
+ );
36
+
37
+ useSeoMeta({
38
+ title: computed(() => localize(config.site.name)),
39
+ description: computed(() => localize(config.site.description)),
40
+ });
41
+ </script>
42
+
43
+ <template>
44
+ <div class="overflow-hidden">
45
+ <section class="relative border-b border-border">
46
+ <div
47
+ class="relative mx-auto flex max-w-6xl flex-col gap-14 px-5 py-24 sm:px-8 sm:py-32 lg:flex-row lg:items-center"
48
+ >
49
+ <div class="min-w-0 flex-1">
50
+ <p v-if="landing.eyebrow" class="mb-6 text-sm font-semibold tracking-wide text-primary">
51
+ {{ landing.eyebrow }}
52
+ </p>
53
+ <h1
54
+ class="max-w-4xl text-5xl leading-[0.98] font-semibold tracking-[-0.035em] text-balance text-foreground sm:text-7xl"
55
+ :class="landing.heroMedia ? 'lg:text-6xl' : 'lg:text-[5.75rem]'"
56
+ >
57
+ {{ landing.title }}
58
+ </h1>
59
+ <p class="mt-7 max-w-2xl text-lg leading-8 text-muted-foreground sm:text-xl">
60
+ {{ landing.description }}
61
+ </p>
62
+ <div class="mt-10 flex flex-wrap items-center gap-3">
63
+ <NuxtLink
64
+ :to="landing.primary.to"
65
+ class="inline-flex h-11 items-center gap-2 rounded-md bg-primary px-5 text-sm font-semibold text-primary-foreground transition-transform hover:-translate-y-0.5"
66
+ >
67
+ {{ landing.primary.label }}
68
+ <Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
69
+ </NuxtLink>
70
+ <NuxtLink
71
+ v-if="landing.secondary"
72
+ :to="landing.secondary.to"
73
+ class="inline-flex h-11 items-center gap-2 rounded-md border border-border bg-background px-5 text-sm font-semibold text-foreground transition-colors hover:bg-muted"
74
+ >
75
+ {{ landing.secondary.label }}
76
+ </NuxtLink>
77
+ </div>
78
+ </div>
79
+
80
+ <div v-if="landing.heroMedia" class="hidden w-[26rem] max-w-full shrink-0 lg:block">
81
+ <NuxtImg
82
+ v-if="landing.heroMedia.type === 'image'"
83
+ :src="landing.heroMedia.src"
84
+ :alt="landing.heroMedia.alt"
85
+ class="w-full rounded-xl border border-border object-cover shadow-sm"
86
+ width="416"
87
+ loading="lazy"
88
+ />
89
+ <div
90
+ v-else-if="landing.heroMedia.type === 'code'"
91
+ class="overflow-hidden rounded-xl border border-border bg-card shadow-sm"
92
+ >
93
+ <div
94
+ v-if="landing.heroMedia.filename"
95
+ class="flex h-9 items-center gap-2 border-b border-border bg-muted/50 px-4"
96
+ >
97
+ <span class="flex gap-1.5" aria-hidden="true">
98
+ <span class="size-2.5 rounded-full bg-border" />
99
+ <span class="size-2.5 rounded-full bg-border" />
100
+ <span class="size-2.5 rounded-full bg-border" />
101
+ </span>
102
+ <span class="text-xs font-medium text-muted-foreground">
103
+ {{ landing.heroMedia.filename }}
104
+ </span>
105
+ </div>
106
+ <pre
107
+ role="region"
108
+ tabindex="0"
109
+ :aria-label="landing.heroMedia.filename ?? 'Code'"
110
+ class="overflow-x-auto p-4 font-mono text-[13px] leading-6 text-foreground/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
111
+ ><code><span v-for="(line, index) in heroCodeLines" :key="index" class="block">{{ line }}</span></code></pre>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </section>
116
+
117
+ <section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-28">
118
+ <div
119
+ class="grid gap-12 border-t border-border pt-12 md:grid-cols-3 md:gap-0 md:divide-x md:divide-border"
120
+ >
121
+ <article
122
+ v-for="feature in landing.features"
123
+ :key="feature.title"
124
+ class="min-w-0 md:px-9 md:first:pl-0 md:last:pr-0"
125
+ >
126
+ <Icon
127
+ v-if="feature.icon"
128
+ :name="feature.icon"
129
+ class="size-6 text-primary"
130
+ aria-hidden="true"
131
+ />
132
+ <h2 class="mt-8 text-xl font-semibold tracking-tight text-foreground">
133
+ {{ feature.title }}
134
+ </h2>
135
+ <p class="mt-3 leading-7 text-muted-foreground">{{ feature.description }}</p>
136
+ </article>
137
+ </div>
138
+ </section>
139
+ </div>
140
+ </template>
@@ -0,0 +1,6 @@
1
+ import { provideSSRWidth } from "@vueuse/core";
2
+ import { defineNuxtPlugin } from "#imports";
3
+
4
+ export default defineNuxtPlugin((nuxtApp) => {
5
+ provideSSRWidth(1024, nuxtApp.vueApp);
6
+ });
@@ -0,0 +1,56 @@
1
+ type TocLinkLike = {
2
+ id?: string;
3
+ text?: string;
4
+ label?: string;
5
+ depth?: number;
6
+ children?: TocLinkLike[];
7
+ };
8
+
9
+ export type FlatTocItem = {
10
+ id: string;
11
+ label: string;
12
+ depth?: number;
13
+ };
14
+
15
+ export function formatContentDate(
16
+ value?: string | Date | null,
17
+ locale: string = defaultLocale,
18
+ ): string {
19
+ if (!value) return "";
20
+
21
+ const date = value instanceof Date ? value : new Date(value);
22
+ if (Number.isNaN(date.getTime())) return "";
23
+
24
+ return new Intl.DateTimeFormat(locale, {
25
+ month: "short",
26
+ day: "numeric",
27
+ year: "numeric",
28
+ }).format(date);
29
+ }
30
+
31
+ export function flattenTocLinks(links?: TocLinkLike[] | null): FlatTocItem[] {
32
+ const output: FlatTocItem[] = [];
33
+
34
+ function visit(items: TocLinkLike[] | undefined) {
35
+ for (const item of items ?? []) {
36
+ const label = item.text ?? item.label;
37
+ if (item.id && label) {
38
+ output.push({
39
+ id: item.id,
40
+ label,
41
+ depth: item.depth,
42
+ });
43
+ }
44
+
45
+ visit(item.children);
46
+ }
47
+ }
48
+
49
+ visit(links ?? undefined);
50
+ return output;
51
+ }
52
+
53
+ export function filterTocByDepth(items: FlatTocItem[], maxDepth: number): FlatTocItem[] {
54
+ return items.filter((item) => (item.depth ?? 2) <= maxDepth);
55
+ }
56
+ import { defaultLocale } from "../../i18n/locales";
@@ -0,0 +1 @@
1
+ export { cn } from "../../runtime/utils";
package/components.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ contentComponentNames as ginkoDocsComponentNames,
3
+ contentComponentPolicy as ginkoDocsComponentPolicy,
4
+ contentComponentTags as ginkoDocsComponentTags,
5
+ } from "./tags";
6
+ export type {
7
+ ContentComponentName as GinkoDocsComponentName,
8
+ ContentComponentTag as GinkoDocsComponentTag,
9
+ } from "./tags";
package/content.js ADDED
@@ -0,0 +1,175 @@
1
+ import {
2
+ defineAgentAppPage,
3
+ defineAgentMetadataFields,
4
+ defineAgentSection,
5
+ defineCollection,
6
+ defineContentConfig,
7
+ reference,
8
+ } from "@lupinum/ginko-content/config";
9
+ import { z } from "zod";
10
+ //#region layer/shared/route-slugs.ts
11
+ const routeSlugs = {
12
+ home: {
13
+ en: "/",
14
+ de: "/",
15
+ },
16
+ docs: {
17
+ en: "/docs",
18
+ de: "/dokumentation",
19
+ },
20
+ blog: {
21
+ en: "/blog",
22
+ de: "/blog",
23
+ },
24
+ };
25
+ //#endregion
26
+ //#region layer/content.ts
27
+ const docsSchema = z.object({
28
+ title: z.string(),
29
+ description: z.string(),
30
+ icon: z.string().optional(),
31
+ badge: z.string().optional(),
32
+ updated: z.string().optional(),
33
+ sidebar: z.enum(["section", "group"]).optional(),
34
+ navigation: z
35
+ .object({
36
+ title: z.string().optional(),
37
+ icon: z.string().optional(),
38
+ badge: z.string().optional(),
39
+ sidebar: z.enum(["section", "group"]).optional(),
40
+ })
41
+ .optional(),
42
+ });
43
+ const blogSchema = z.object({
44
+ title: z.string(),
45
+ description: z.string(),
46
+ badge: z.string().optional(),
47
+ date: z.string(),
48
+ readingTime: z.string(),
49
+ author: reference("authors"),
50
+ image: z.string().optional(),
51
+ });
52
+ const authorsSchema = z.object({
53
+ slug: z.string(),
54
+ name: z.string(),
55
+ role: z.string(),
56
+ bio: z.string(),
57
+ avatar: z.string(),
58
+ links: z
59
+ .array(
60
+ z.object({
61
+ label: z.string(),
62
+ href: z.string(),
63
+ }),
64
+ )
65
+ .optional(),
66
+ });
67
+ function defineGinkoDocsConfig(options) {
68
+ const locales = options.locales ?? ["en"];
69
+ const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
70
+ const i18n = locales.length > 1;
71
+ const routeMap = (key) =>
72
+ Object.fromEntries(locales.map((locale) => [locale, routeSlugs[key][locale]]));
73
+ const metadata = defineAgentMetadataFields([
74
+ "title",
75
+ "description",
76
+ "url",
77
+ "route",
78
+ "locale",
79
+ "section",
80
+ "collection",
81
+ "source",
82
+ "updated",
83
+ ]);
84
+ const docs = defineCollection({
85
+ type: "page",
86
+ source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
87
+ i18n: i18n ? true : void 0,
88
+ route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
89
+ agent: {
90
+ section: "optional",
91
+ markdown: true,
92
+ },
93
+ strict: true,
94
+ schema: docsSchema,
95
+ });
96
+ const blog = defineCollection({
97
+ type: "page",
98
+ source: "2.blog/**/*.md",
99
+ i18n: i18n ? true : void 0,
100
+ route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
101
+ agent: {
102
+ section: "blog",
103
+ markdown: true,
104
+ },
105
+ strict: true,
106
+ schema: blogSchema,
107
+ });
108
+ const authors = defineCollection({
109
+ type: "data",
110
+ source: "authors/**/*.json",
111
+ i18n: i18n ? true : void 0,
112
+ strict: true,
113
+ sitemap: false,
114
+ schema: authorsSchema,
115
+ });
116
+ const config = {
117
+ agent: {
118
+ site: {
119
+ title: options.site.name,
120
+ description: options.site.description,
121
+ url: options.site.url,
122
+ },
123
+ markdown: {
124
+ metadata: {
125
+ enabled: true,
126
+ defaultFields: metadata,
127
+ },
128
+ },
129
+ sections: [
130
+ ...(options.blog
131
+ ? [
132
+ defineAgentSection({
133
+ id: "blog",
134
+ title: "Blog",
135
+ order: 40,
136
+ }),
137
+ ]
138
+ : []),
139
+ defineAgentSection({
140
+ id: "optional",
141
+ title: "Documentation",
142
+ order: 100,
143
+ }),
144
+ ],
145
+ pages: [
146
+ defineAgentAppPage({
147
+ id: "home",
148
+ route: Object.fromEntries(
149
+ locales.map((locale) => [locale, locale === defaultLocale ? "/" : `/${locale}`]),
150
+ ),
151
+ section: "optional",
152
+ title: options.site.name,
153
+ description: options.site.description,
154
+ metadata,
155
+ render: () => `# ${options.site.name}\n\n> ${options.site.description}`,
156
+ }),
157
+ ],
158
+ },
159
+ };
160
+ if (options.blog)
161
+ return defineContentConfig({
162
+ ...config,
163
+ collections: {
164
+ docs,
165
+ blog,
166
+ authors,
167
+ },
168
+ });
169
+ return defineContentConfig({
170
+ ...config,
171
+ collections: { docs },
172
+ });
173
+ }
174
+ //#endregion
175
+ export { defineGinkoDocsConfig };
package/content.ts ADDED
@@ -0,0 +1,148 @@
1
+ import {
2
+ defineAgentAppPage,
3
+ defineAgentMetadataFields,
4
+ defineAgentSection,
5
+ defineCollection,
6
+ defineContentConfig,
7
+ reference,
8
+ } from "@lupinum/ginko-content/config";
9
+ import type { ContentCollectionConfig, ContentConfig } from "@lupinum/ginko-content/config";
10
+ import { z } from "zod";
11
+ import { routeSlugs } from "./shared/route-slugs";
12
+
13
+ export interface GinkoDocsContentOptions {
14
+ site: { name: string; description: string; url: string };
15
+ locales?: Array<"en" | "de">;
16
+ defaultLocale?: "en" | "de";
17
+ blog?: boolean;
18
+ }
19
+
20
+ const docsSchema = z.object({
21
+ title: z.string(),
22
+ description: z.string(),
23
+ icon: z.string().optional(),
24
+ badge: z.string().optional(),
25
+ updated: z.string().optional(),
26
+ sidebar: z.enum(["section", "group"]).optional(),
27
+ navigation: z
28
+ .object({
29
+ title: z.string().optional(),
30
+ icon: z.string().optional(),
31
+ badge: z.string().optional(),
32
+ sidebar: z.enum(["section", "group"]).optional(),
33
+ })
34
+ .optional(),
35
+ });
36
+ const blogSchema = z.object({
37
+ title: z.string(),
38
+ description: z.string(),
39
+ badge: z.string().optional(),
40
+ date: z.string(),
41
+ readingTime: z.string(),
42
+ author: reference("authors"),
43
+ image: z.string().optional(),
44
+ });
45
+ const authorsSchema = z.object({
46
+ slug: z.string(),
47
+ name: z.string(),
48
+ role: z.string(),
49
+ bio: z.string(),
50
+ avatar: z.string(),
51
+ links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
52
+ });
53
+
54
+ type DocsCollection = ContentCollectionConfig<typeof docsSchema>;
55
+ type BlogCollection = ContentCollectionConfig<typeof blogSchema>;
56
+ type AuthorsCollection = ContentCollectionConfig<typeof authorsSchema>;
57
+ type DocsContentConfig = ContentConfig<{ docs: DocsCollection }>;
58
+ type DocsBlogContentConfig = ContentConfig<{
59
+ docs: DocsCollection;
60
+ blog: BlogCollection;
61
+ authors: AuthorsCollection;
62
+ }>;
63
+
64
+ export function defineGinkoDocsConfig(
65
+ options: GinkoDocsContentOptions & { blog: true },
66
+ ): DocsBlogContentConfig;
67
+ export function defineGinkoDocsConfig(options: GinkoDocsContentOptions): DocsContentConfig;
68
+ export function defineGinkoDocsConfig(
69
+ options: GinkoDocsContentOptions,
70
+ ): DocsContentConfig | DocsBlogContentConfig {
71
+ const locales = options.locales ?? ["en"];
72
+ const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
73
+ const i18n = locales.length > 1;
74
+ const routeMap = (key: "docs" | "blog") =>
75
+ Object.fromEntries(locales.map((locale) => [locale, routeSlugs[key][locale]]));
76
+ const metadata = defineAgentMetadataFields([
77
+ "title",
78
+ "description",
79
+ "url",
80
+ "route",
81
+ "locale",
82
+ "section",
83
+ "collection",
84
+ "source",
85
+ "updated",
86
+ ]);
87
+ const docs = defineCollection({
88
+ type: "page",
89
+ source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
90
+ i18n: i18n ? true : undefined,
91
+ route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
92
+ agent: { section: "optional", markdown: true },
93
+ strict: true,
94
+ schema: docsSchema,
95
+ });
96
+ const blog = defineCollection({
97
+ type: "page",
98
+ source: "2.blog/**/*.md",
99
+ i18n: i18n ? true : undefined,
100
+ route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
101
+ agent: { section: "blog", markdown: true },
102
+ strict: true,
103
+ schema: blogSchema,
104
+ });
105
+ const authors = defineCollection({
106
+ type: "data",
107
+ source: "authors/**/*.json",
108
+ i18n: i18n ? true : undefined,
109
+ strict: true,
110
+ sitemap: false,
111
+ schema: authorsSchema,
112
+ });
113
+ const config = {
114
+ agent: {
115
+ site: {
116
+ title: options.site.name,
117
+ description: options.site.description,
118
+ url: options.site.url,
119
+ },
120
+ markdown: { metadata: { enabled: true, defaultFields: metadata } },
121
+ sections: [
122
+ ...(options.blog ? [defineAgentSection({ id: "blog", title: "Blog", order: 40 })] : []),
123
+ defineAgentSection({ id: "optional", title: "Documentation", order: 100 }),
124
+ ],
125
+ pages: [
126
+ defineAgentAppPage({
127
+ id: "home",
128
+ route: Object.fromEntries(
129
+ locales.map((locale) => [locale, locale === defaultLocale ? "/" : `/${locale}`]),
130
+ ),
131
+ section: "optional",
132
+ title: options.site.name,
133
+ description: options.site.description,
134
+ metadata,
135
+ render: () => `# ${options.site.name}\n\n> ${options.site.description}`,
136
+ }),
137
+ ],
138
+ },
139
+ };
140
+
141
+ if (options.blog) {
142
+ return defineContentConfig({
143
+ ...config,
144
+ collections: { docs, blog, authors },
145
+ }) as DocsBlogContentConfig;
146
+ }
147
+ return defineContentConfig({ ...config, collections: { docs } }) as DocsContentConfig;
148
+ }
@@ -0,0 +1,10 @@
1
+ import { defaultLocale } from "./locales";
2
+ import { messages } from "./messages/index";
3
+ import { defineI18nConfig } from "#imports";
4
+
5
+ export default defineI18nConfig(() => ({
6
+ legacy: false,
7
+ locale: defaultLocale,
8
+ fallbackLocale: defaultLocale,
9
+ messages,
10
+ }));