@lupinum/ginko-docs 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +14 -12
  3. package/app/app.vue +2 -2
  4. package/app/assets/css/prose.css +2203 -149
  5. package/app/assets/css/tailwind.css +60 -38
  6. package/app/assets/css/theme-palettes.css +309 -0
  7. package/app/components/content/DocumentPageShell.vue +1 -1
  8. package/app/components/content/Feedback.vue +12 -11
  9. package/app/components/content/ImageZoomDialog.vue +84 -0
  10. package/app/components/content/PageMarkdownCopy.vue +2 -0
  11. package/app/components/content/imageZoom.ts +25 -0
  12. package/app/components/mdc/MdcAccordion.vue +6 -3
  13. package/app/components/mdc/MdcAccordionItem.vue +7 -16
  14. package/app/components/mdc/MdcApi.vue +159 -0
  15. package/app/components/mdc/MdcAside.vue +6 -10
  16. package/app/components/mdc/MdcCallout.vue +9 -10
  17. package/app/components/mdc/MdcCard.vue +36 -47
  18. package/app/components/mdc/MdcCards.vue +30 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +4 -0
  20. package/app/components/mdc/MdcCodeTree.vue +234 -0
  21. package/app/components/mdc/MdcCollapse.vue +61 -0
  22. package/app/components/mdc/MdcError.vue +8 -1
  23. package/app/components/mdc/MdcExcerpt.vue +26 -0
  24. package/app/components/mdc/MdcFigure.vue +54 -20
  25. package/app/components/mdc/MdcFiles.vue +120 -0
  26. package/app/components/mdc/MdcIdea.vue +2 -1
  27. package/app/components/mdc/MdcInfo.vue +2 -1
  28. package/app/components/mdc/MdcInlineToc.vue +51 -0
  29. package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
  30. package/app/components/mdc/MdcNote.vue +2 -1
  31. package/app/components/mdc/MdcQuiz.vue +43 -83
  32. package/app/components/mdc/MdcQuizQuestion.vue +104 -109
  33. package/app/components/mdc/MdcReadMore.vue +57 -77
  34. package/app/components/mdc/MdcSteps.vue +45 -11
  35. package/app/components/mdc/MdcSuccess.vue +8 -1
  36. package/app/components/mdc/MdcTabs.vue +44 -26
  37. package/app/components/mdc/MdcTimeline.vue +4 -1
  38. package/app/components/mdc/MdcTimelineItem.vue +18 -27
  39. package/app/components/mdc/MdcWarning.vue +8 -1
  40. package/app/components/mdc/api.utils.ts +132 -0
  41. package/app/components/mdc/code-tree.utils.ts +22 -0
  42. package/app/components/mdc/quiz.utils.ts +38 -0
  43. package/app/components/prose/ProseBlockquote.vue +17 -1
  44. package/app/components/prose/ProseH1.vue +1 -1
  45. package/app/components/prose/ProseH2.vue +7 -2
  46. package/app/components/prose/ProseH3.vue +7 -2
  47. package/app/components/prose/ProseH4.vue +17 -2
  48. package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
  49. package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
  50. package/app/components/prose/ProseImg.vue +19 -112
  51. package/app/components/prose/ProsePre.vue +63 -100
  52. package/app/components/prose/ProseTable.vue +5 -3
  53. package/app/components/site/ModeToggle.vue +12 -3
  54. package/app/components/site/SiteBanner.vue +43 -2
  55. package/app/components/site/SiteFooter.vue +3 -5
  56. package/app/components/site/SiteHeader.vue +255 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +22 -4
  59. package/app/components/ui/badge/Badge.vue +1 -1
  60. package/app/components/ui/button/Button.vue +1 -1
  61. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  63. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  64. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  66. package/app/components/ui/dropdown-menu/index.ts +0 -9
  67. package/app/components/ui/kbd/Kbd.vue +1 -1
  68. package/app/components/ui/kbd/index.ts +0 -1
  69. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  70. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  71. package/app/components/ui/separator/Separator.vue +1 -1
  72. package/app/components/ui/sheet/SheetContent.vue +1 -1
  73. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  74. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  75. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  76. package/app/components/ui/sheet/index.ts +0 -3
  77. package/app/components/ui/tabs/Tabs.vue +1 -1
  78. package/app/components/ui/tabs/TabsList.vue +1 -1
  79. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  80. package/app/components/ui/tabs/index.ts +0 -1
  81. package/app/composables/site-navigation.utils.ts +29 -11
  82. package/app/composables/useContentRouteAlternates.ts +13 -21
  83. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  84. package/app/composables/useMetaKey.ts +18 -0
  85. package/app/composables/useProseAppearance.ts +24 -0
  86. package/app/composables/useSiteNavigation.ts +3 -18
  87. package/app/config/site.utils.ts +1 -1
  88. package/app/error.vue +1 -5
  89. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  90. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  91. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  92. package/app/features/docs/components/DocsPageContent.vue +84 -47
  93. package/app/features/docs/components/DocsPageNav.vue +1 -1
  94. package/app/features/docs/components/DocsSidebar.vue +30 -20
  95. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  96. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  97. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  98. package/app/features/docs/components/DocsToc.vue +80 -34
  99. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  100. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  101. package/app/features/docs/composables/useRevealActive.ts +58 -0
  102. package/app/features/docs/composables/useScrollspy.ts +87 -19
  103. package/app/features/docs/docs-navigation.ts +17 -30
  104. package/app/features/docs/toc-context.ts +6 -0
  105. package/app/features/search/command-center.ts +11 -90
  106. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  107. package/app/features/search/useCommandCenter.ts +46 -46
  108. package/app/layouts/docs.vue +1 -1
  109. package/app/lib/errors.ts +2 -49
  110. package/app/pages/blog/[slug].vue +16 -8
  111. package/app/pages/blog/index.vue +26 -13
  112. package/app/pages/docs/index.vue +10 -14
  113. package/app/pages/index.vue +174 -59
  114. package/app/plugins/ginko-docs-theme.ts +12 -0
  115. package/app/utils/content.ts +11 -0
  116. package/app/utils/file-icons.ts +90 -0
  117. package/app/utils/index.ts +7 -1
  118. package/app/utils/repository.ts +20 -0
  119. package/content.js +23 -26
  120. package/content.ts +30 -28
  121. package/i18n/messages/global/blog.ts +4 -0
  122. package/i18n/messages/global/command.ts +6 -2
  123. package/i18n/messages/global/docs.ts +3 -0
  124. package/i18n/messages/global/nav.ts +6 -2
  125. package/i18n/messages/index.ts +3 -3
  126. package/icon-bundle.ts +5 -0
  127. package/index.d.ts +7 -1
  128. package/nuxt.config.ts +2 -2
  129. package/package.json +9 -6
  130. package/runtime/server/agent-markdown.ts +0 -2
  131. package/shared/theme-palettes.ts +37 -0
  132. package/shared/types/app-config.ts +63 -6
  133. package/tags.ts +47 -59
  134. package/app/components/mdc/MdcAlert.vue +0 -34
  135. package/app/components/mdc/MdcCardGroup.vue +0 -47
  136. package/app/components/mdc/MdcDocImg.vue +0 -25
  137. package/app/components/mdc/MdcField.vue +0 -62
  138. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  139. package/app/components/mdc/MdcPassage.vue +0 -32
  140. package/app/components/mdc/MdcQuizOption.vue +0 -91
  141. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  142. package/app/components/mdc/MdcShortcut.vue +0 -46
  143. package/app/components/mdc/MdcStep.vue +0 -42
  144. package/app/components/ui/accordion/Accordion.vue +0 -15
  145. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  146. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  147. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  148. package/app/components/ui/accordion/index.ts +0 -4
  149. package/app/components/ui/alert/Alert.vue +0 -21
  150. package/app/components/ui/alert/AlertDescription.vue +0 -22
  151. package/app/components/ui/alert/AlertTitle.vue +0 -22
  152. package/app/components/ui/alert/index.ts +0 -34
  153. package/app/components/ui/card/Card.vue +0 -16
  154. package/app/components/ui/card/CardAction.vue +0 -17
  155. package/app/components/ui/card/CardContent.vue +0 -14
  156. package/app/components/ui/card/CardDescription.vue +0 -14
  157. package/app/components/ui/card/CardFooter.vue +0 -14
  158. package/app/components/ui/card/CardHeader.vue +0 -22
  159. package/app/components/ui/card/card-variants.ts +0 -15
  160. package/app/components/ui/card/index.ts +0 -8
  161. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  162. package/app/components/ui/checkbox/index.ts +0 -1
  163. package/app/components/ui/dialog/Dialog.vue +0 -15
  164. package/app/components/ui/dialog/DialogClose.vue +0 -12
  165. package/app/components/ui/dialog/DialogContent.vue +0 -69
  166. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  167. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  168. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  169. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  170. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  171. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  172. package/app/components/ui/dialog/index.ts +0 -10
  173. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  174. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  175. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  176. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  177. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  178. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  179. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  180. package/app/components/ui/input/Input.vue +0 -23
  181. package/app/components/ui/input/index.ts +0 -1
  182. package/app/components/ui/label/Label.vue +0 -14
  183. package/app/components/ui/label/index.ts +0 -1
  184. package/app/components/ui/sheet/SheetClose.vue +0 -12
  185. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  186. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  187. package/app/components/ui/switch/Switch.vue +0 -39
  188. package/app/components/ui/switch/index.ts +0 -1
  189. package/app/components/ui/tabs/TabsContent.vue +0 -21
  190. package/app/components/ui/textarea/Textarea.vue +0 -31
  191. package/app/components/ui/textarea/index.ts +0 -1
  192. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  193. package/app/lib/utils.ts +0 -7
  194. package/i18n/messages/source.ts +0 -5
  195. package/runtime/markdown/inline-code-highlight.ts +0 -155
  196. package/runtime/utils.ts +0 -7
@@ -1,38 +1,102 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+ import { useClipboard } from "@vueuse/core";
3
4
  import { useI18n, useSeoMeta } from "#imports";
4
5
  import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
5
6
  import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
7
+ import SiteHeroCode from "#ginko-docs/components/site/SiteHeroCode.vue";
6
8
 
7
9
  const config = useGinkoDocsConfig();
8
- const { locale } = useI18n();
9
- const localize = (value: string | { en: string; de?: string }) =>
10
- getLocalizedSiteText(value, locale.value);
10
+ const { locale, t } = useI18n();
11
+
12
+ type LocalizableText = string | { en: string; de?: string };
13
+ const localize = (value: LocalizableText) => getLocalizedSiteText(value, locale.value);
14
+ const localizeLink = (link: { label: LocalizableText; to: LocalizableText }) => ({
15
+ label: localize(link.label),
16
+ to: localize(link.to),
17
+ });
18
+
11
19
  const landing = computed(() => ({
12
- eyebrow: config.landing.eyebrow ? localize(config.landing.eyebrow) : undefined,
13
20
  title: localize(config.landing.title),
14
21
  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,
22
+ primary: localizeLink(config.landing.primary),
23
+ secondary: config.landing.secondary ? localizeLink(config.landing.secondary) : undefined,
24
+ install: config.landing.install,
25
25
  features: config.landing.features.map((feature) => ({
26
26
  ...feature,
27
27
  title: localize(feature.title),
28
28
  description: localize(feature.description),
29
29
  })),
30
30
  heroMedia: config.landing.hero?.media,
31
+ agent: config.landing.agent
32
+ ? {
33
+ title: localize(config.landing.agent.title),
34
+ description: localize(config.landing.agent.description),
35
+ code: config.landing.agent.code,
36
+ }
37
+ : undefined,
38
+ cta: config.landing.cta
39
+ ? {
40
+ title: localize(config.landing.cta.title),
41
+ primary: config.landing.cta.primary
42
+ ? localizeLink(config.landing.cta.primary)
43
+ : localizeLink(config.landing.primary),
44
+ secondary: config.landing.cta.secondary
45
+ ? localizeLink(config.landing.cta.secondary)
46
+ : undefined,
47
+ }
48
+ : undefined,
31
49
  }));
32
50
 
33
- const heroCodeLines = computed(() =>
34
- landing.value.heroMedia?.type === "code" ? landing.value.heroMedia.code.split("\n") : [],
35
- );
51
+ const heroCodeTabs = computed(() => {
52
+ const media = landing.value.heroMedia;
53
+
54
+ if (media?.type === "code-tabs") {
55
+ return media.tabs.map((tab) => ({ ...tab, label: localize(tab.label) }));
56
+ }
57
+
58
+ if (media?.type === "code") {
59
+ return [
60
+ {
61
+ label: media.filename ?? media.language ?? "Code",
62
+ filename: media.filename,
63
+ language: media.language,
64
+ code: media.code,
65
+ },
66
+ ];
67
+ }
68
+
69
+ return [];
70
+ });
71
+
72
+ // Feature icon tiles cycle through the theme-adaptive hero accent pairs.
73
+ const FEATURE_TINTS = [
74
+ { tile: "bg-(--hero-blue-muted)", icon: "text-(--hero-blue-dark)" },
75
+ { tile: "bg-(--hero-mint-muted)", icon: "text-(--hero-mint-text)" },
76
+ { tile: "bg-(--hero-yellow-muted)", icon: "text-(--hero-yellow-dark)" },
77
+ { tile: "bg-(--hero-coral-muted)", icon: "text-(--hero-coral-text)" },
78
+ ];
79
+ const tint = (index: number) => FEATURE_TINTS[index % FEATURE_TINTS.length]!;
80
+
81
+ type TerminalLine = { kind: "command" | "comment" | "output"; text: string };
82
+ const agentTerminalLines = computed<TerminalLine[]>(() => {
83
+ const code = landing.value.agent?.code;
84
+ if (!code) return [];
85
+
86
+ return code.split("\n").map((line) => ({
87
+ kind: line.startsWith("$")
88
+ ? "command"
89
+ : line.trimStart().startsWith("#")
90
+ ? "comment"
91
+ : "output",
92
+ text: line,
93
+ }));
94
+ });
95
+
96
+ const { copy, copied } = useClipboard();
97
+ async function copyInstallCommand() {
98
+ await copy(landing.value.install?.command ?? "");
99
+ }
36
100
 
37
101
  useSeoMeta({
38
102
  title: computed(() => localize(config.site.name)),
@@ -47,9 +111,6 @@ useSeoMeta({
47
111
  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
112
  >
49
113
  <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
114
  <h1
54
115
  class="max-w-4xl text-5xl leading-[0.98] font-semibold tracking-[-0.035em] text-balance text-foreground sm:text-7xl"
55
116
  :class="landing.heroMedia ? 'lg:text-6xl' : 'lg:text-[5.75rem]'"
@@ -75,66 +136,120 @@ useSeoMeta({
75
136
  {{ landing.secondary.label }}
76
137
  </NuxtLink>
77
138
  </div>
139
+ <div v-if="landing.install" class="mt-5">
140
+ <div
141
+ class="inline-flex h-10 max-w-full items-center gap-3 rounded-md border border-border bg-muted/40 pr-1.5 pl-4 font-mono text-[13px] text-foreground/90"
142
+ >
143
+ <span class="text-muted-foreground select-none" aria-hidden="true">$</span>
144
+ <span class="truncate">{{ landing.install.command }}</span>
145
+ <button
146
+ type="button"
147
+ class="content-codeblock-copy-button"
148
+ :aria-label="copied ? t('docs.copiedText') : t('docs.copyText')"
149
+ @click="copyInstallCommand"
150
+ >
151
+ <Icon
152
+ :name="copied ? 'lucide:check' : 'lucide:clipboard'"
153
+ class="size-3.5"
154
+ aria-hidden="true"
155
+ />
156
+ </button>
157
+ </div>
158
+ </div>
78
159
  </div>
79
160
 
80
- <div v-if="landing.heroMedia" class="hidden w-[26rem] max-w-full shrink-0 lg:block">
161
+ <div
162
+ v-if="landing.heroMedia"
163
+ class="w-full min-w-0 max-w-2xl shrink-0 lg:w-[30rem] lg:max-w-full xl:w-[36rem]"
164
+ >
81
165
  <NuxtImg
82
166
  v-if="landing.heroMedia.type === 'image'"
83
167
  :src="landing.heroMedia.src"
84
168
  :alt="landing.heroMedia.alt"
85
169
  class="w-full rounded-xl border border-border object-cover shadow-sm"
86
- width="416"
170
+ width="576"
87
171
  loading="lazy"
88
172
  />
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>
173
+ <SiteHeroCode v-else-if="heroCodeTabs.length" :tabs="heroCodeTabs" />
113
174
  </div>
114
175
  </div>
115
176
  </section>
116
177
 
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
- >
178
+ <section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-24">
179
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
121
180
  <article
122
- v-for="feature in landing.features"
181
+ v-for="(feature, index) in landing.features"
123
182
  :key="feature.title"
124
- class="min-w-0 md:px-9 md:first:pl-0 md:last:pr-0"
183
+ class="min-w-0 rounded-lg border border-border bg-card p-6"
125
184
  >
126
- <Icon
185
+ <span
127
186
  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">
187
+ class="flex size-10 items-center justify-center rounded-md"
188
+ :class="tint(index).tile"
189
+ >
190
+ <Icon
191
+ :name="feature.icon"
192
+ class="size-5"
193
+ :class="tint(index).icon"
194
+ aria-hidden="true"
195
+ />
196
+ </span>
197
+ <h2 class="mt-5 text-base font-semibold tracking-tight text-foreground">
133
198
  {{ feature.title }}
134
199
  </h2>
135
- <p class="mt-3 leading-7 text-muted-foreground">{{ feature.description }}</p>
200
+ <p class="mt-2 text-sm leading-6 text-muted-foreground">{{ feature.description }}</p>
136
201
  </article>
137
202
  </div>
138
203
  </section>
204
+
205
+ <section v-if="landing.agent" class="border-y border-border bg-agent-background">
206
+ <div
207
+ class="mx-auto grid max-w-6xl items-center gap-10 px-5 py-16 sm:px-8 sm:py-20 lg:grid-cols-2 lg:gap-16"
208
+ >
209
+ <div class="min-w-0">
210
+ <h2
211
+ class="text-3xl font-semibold tracking-[-0.03em] text-balance text-agent-foreground sm:text-4xl"
212
+ >
213
+ {{ landing.agent.title }}
214
+ </h2>
215
+ <p class="mt-4 max-w-xl text-base leading-7 text-agent-muted">
216
+ {{ landing.agent.description }}
217
+ </p>
218
+ </div>
219
+ <pre
220
+ class="min-w-0 overflow-x-auto rounded-lg border border-white/10 bg-black/30 p-5 font-mono text-[13px] leading-7"
221
+ ><code><span
222
+ v-for="(line, index) in agentTerminalLines"
223
+ :key="index"
224
+ class="block min-h-[1.75rem] whitespace-pre"
225
+ ><template v-if="line.kind === 'command'"><span class="text-agent-muted/70 select-none">$</span><span class="text-agent-foreground">{{ line.text.slice(1) }}</span></template><span v-else-if="line.kind === 'comment'" class="text-emerald-300/90">{{ line.text }}</span><span v-else class="text-agent-muted">{{ line.text }}</span></span></code></pre>
226
+ </div>
227
+ </section>
228
+
229
+ <section v-if="landing.cta">
230
+ <div class="mx-auto max-w-6xl px-5 py-20 text-center sm:px-8 sm:py-28">
231
+ <h2
232
+ class="text-3xl font-semibold tracking-[-0.03em] text-balance text-foreground sm:text-5xl"
233
+ >
234
+ {{ landing.cta.title }}
235
+ </h2>
236
+ <div class="mt-9 flex flex-wrap items-center justify-center gap-3">
237
+ <NuxtLink
238
+ :to="landing.cta.primary.to"
239
+ 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"
240
+ >
241
+ {{ landing.cta.primary.label }}
242
+ <Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
243
+ </NuxtLink>
244
+ <NuxtLink
245
+ v-if="landing.cta.secondary"
246
+ :to="landing.cta.secondary.to"
247
+ 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"
248
+ >
249
+ {{ landing.cta.secondary.label }}
250
+ </NuxtLink>
251
+ </div>
252
+ </div>
253
+ </section>
139
254
  </div>
140
255
  </template>
@@ -0,0 +1,12 @@
1
+ import { defineNuxtPlugin, useAppConfig, useHead } from "#imports";
2
+
3
+ export default defineNuxtPlugin(() => {
4
+ const { theme } = useAppConfig().ginkoDocs;
5
+
6
+ useHead({
7
+ htmlAttrs: {
8
+ "data-neutral": theme.neutral,
9
+ "data-primary": theme.primary,
10
+ },
11
+ });
12
+ });
@@ -12,6 +12,17 @@ export type FlatTocItem = {
12
12
  depth?: number;
13
13
  };
14
14
 
15
+ export function getMarkdownTocLinks(body: unknown): TocLinkLike[] | undefined {
16
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
17
+
18
+ const root = body as { type?: unknown; children?: unknown; toc?: unknown };
19
+ if (root.type !== "root" || !Array.isArray(root.children)) return undefined;
20
+ if (!root.toc || typeof root.toc !== "object" || Array.isArray(root.toc)) return undefined;
21
+
22
+ const links = (root.toc as { links?: unknown }).links;
23
+ return Array.isArray(links) ? (links as TocLinkLike[]) : undefined;
24
+ }
25
+
15
26
  export function formatContentDate(
16
27
  value?: string | Date | null,
17
28
  locale: string = defaultLocale,
@@ -0,0 +1,90 @@
1
+ export const FALLBACK_FILE_ICON = "lucide:file";
2
+
3
+ export const FILENAME_ICONS: Record<string, string> = {
4
+ ".env": "lucide:file-key",
5
+ ".env.example": "lucide:file-key",
6
+ ".gitignore": "lucide:git-branch",
7
+ "components.json": "lucide:blocks",
8
+ "eslint.config.cjs": "lucide:badge-check",
9
+ "eslint.config.js": "lucide:badge-check",
10
+ "eslint.config.mjs": "lucide:badge-check",
11
+ "nuxt.config.js": "logos:nuxt-icon",
12
+ "nuxt.config.ts": "logos:nuxt-icon",
13
+ "package.json": "lucide:package",
14
+ "pnpm-lock.yaml": "lucide:package",
15
+ "pnpm-workspace.yaml": "lucide:package",
16
+ "tailwind.config.js": "lucide:palette",
17
+ "tailwind.config.ts": "lucide:palette",
18
+ "tsconfig.json": "lucide:file-cog",
19
+ "vite.config.js": "lucide:zap",
20
+ "vite.config.ts": "lucide:zap",
21
+ };
22
+
23
+ export const EXTENSION_ICONS: Record<string, string> = {
24
+ bash: "lucide:terminal",
25
+ cjs: "lucide:file-json",
26
+ css: "lucide:braces",
27
+ csv: "lucide:table",
28
+ htm: "lucide:code-xml",
29
+ html: "lucide:code-xml",
30
+ js: "lucide:file-json",
31
+ json: "lucide:braces",
32
+ jsx: "lucide:atom",
33
+ md: "lucide:file-text",
34
+ mdc: "lucide:file-text",
35
+ mjs: "lucide:file-json",
36
+ mts: "lucide:file-code",
37
+ scss: "lucide:braces",
38
+ sh: "lucide:terminal",
39
+ ts: "logos:typescript-icon",
40
+ tsx: "lucide:atom",
41
+ txt: "lucide:file-text",
42
+ vue: "logos:vue",
43
+ yaml: "lucide:file-cog",
44
+ yml: "lucide:file-cog",
45
+ };
46
+
47
+ export const LANGUAGE_ICONS: Record<string, string> = {
48
+ bash: "lucide:terminal",
49
+ css: "lucide:braces",
50
+ html: "lucide:code-xml",
51
+ javascript: "lucide:file-json",
52
+ js: "lucide:file-json",
53
+ json: "lucide:braces",
54
+ jsx: "lucide:atom",
55
+ markdown: "lucide:file-text",
56
+ mdc: "lucide:file-text",
57
+ shell: "lucide:terminal",
58
+ sh: "lucide:terminal",
59
+ ts: "logos:typescript-icon",
60
+ tsx: "lucide:atom",
61
+ typescript: "logos:typescript-icon",
62
+ vue: "logos:vue",
63
+ "vue-html": "logos:vue",
64
+ yaml: "lucide:file-cog",
65
+ yml: "lucide:file-cog",
66
+ };
67
+
68
+ export function normalizedFilename(filename: string | null | undefined): string | null {
69
+ return filename?.trim().replace(/^\.\//, "").toLowerCase() || null;
70
+ }
71
+
72
+ export function filenameExtension(filename: string | null | undefined): string | null {
73
+ const normalized = normalizedFilename(filename);
74
+ const basename = normalized?.split("/").pop();
75
+ const index = basename?.lastIndexOf(".") ?? -1;
76
+
77
+ if (!basename || index < 0 || index === basename.length - 1) return null;
78
+ return basename.slice(index + 1);
79
+ }
80
+
81
+ /** Best-effort icon for a file, by exact name first, then extension. */
82
+ export function resolveFileIcon(filename: string | null | undefined): string {
83
+ const normalized = normalizedFilename(filename);
84
+ if (normalized && FILENAME_ICONS[normalized]) return FILENAME_ICONS[normalized];
85
+
86
+ const extension = filenameExtension(filename);
87
+ if (extension && EXTENSION_ICONS[extension]) return EXTENSION_ICONS[extension];
88
+
89
+ return FALLBACK_FILE_ICON;
90
+ }
@@ -1 +1,7 @@
1
- export { cn } from "../../runtime/utils";
1
+ import type { ClassValue } from "clsx";
2
+ import { clsx } from "clsx";
3
+ import { twMerge } from "tailwind-merge";
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
@@ -0,0 +1,20 @@
1
+ import type { GinkoDocsAppConfig } from "../../shared/types/app-config";
2
+
3
+ type RepositoryConfig = NonNullable<GinkoDocsAppConfig["repository"]>;
4
+
5
+ const repoBase = (repository: RepositoryConfig) => repository.url.replace(/\/$/, "");
6
+
7
+ export function buildRepoIssueUrl(
8
+ repository: RepositoryConfig,
9
+ params: { title: string; body: string },
10
+ ): string {
11
+ const url = new URL(`${repoBase(repository)}/issues/new`);
12
+ url.searchParams.set("title", params.title);
13
+ url.searchParams.set("body", params.body);
14
+ return url.toString();
15
+ }
16
+
17
+ export function buildRepoEditUrl(repository: RepositoryConfig, sourcePath: string): string {
18
+ const branch = repository.branch || "main";
19
+ return `${repoBase(repository)}/edit/${encodeURIComponent(branch)}/${sourcePath}`;
20
+ }
package/content.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- defineAgentAppPage,
3
2
  defineAgentMetadataFields,
4
3
  defineAgentSection,
5
4
  defineCollection,
@@ -24,12 +23,14 @@ const routeSlugs = {
24
23
  };
25
24
  //#endregion
26
25
  //#region layer/content.ts
26
+ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
27
+ const nonEmptyString = z.string().trim().min(1);
27
28
  const docsSchema = z.object({
28
29
  title: z.string(),
29
30
  description: z.string(),
30
31
  icon: z.string().optional(),
31
32
  badge: z.string().optional(),
32
- updated: z.string().optional(),
33
+ updated: isoDate.optional(),
33
34
  sidebar: z.enum(["section", "group"]).optional(),
34
35
  navigation: z
35
36
  .object({
@@ -44,8 +45,8 @@ const blogSchema = z.object({
44
45
  title: z.string(),
45
46
  description: z.string(),
46
47
  badge: z.string().optional(),
47
- date: z.string(),
48
- readingTime: z.string(),
48
+ date: isoDate,
49
+ readingTime: nonEmptyString,
49
50
  author: reference("authors"),
50
51
  image: z.string().optional(),
51
52
  });
@@ -66,10 +67,13 @@ const authorsSchema = z.object({
66
67
  });
67
68
  function defineGinkoDocsConfig(options) {
68
69
  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]]));
70
+ if (
71
+ !(locales.length === 1
72
+ ? locales[0] === "en"
73
+ : locales.length === 2 && locales[0] === "en" && locales[1] === "de")
74
+ )
75
+ throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
76
+ const i18n = locales.length === 2;
73
77
  const metadata = defineAgentMetadataFields([
74
78
  "title",
75
79
  "description",
@@ -85,7 +89,7 @@ function defineGinkoDocsConfig(options) {
85
89
  type: "page",
86
90
  source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
87
91
  i18n: i18n ? true : void 0,
88
- route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
92
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
89
93
  agent: {
90
94
  section: "optional",
91
95
  markdown: true,
@@ -95,9 +99,9 @@ function defineGinkoDocsConfig(options) {
95
99
  });
96
100
  const blog = defineCollection({
97
101
  type: "page",
98
- source: "2.blog/**/*.md",
102
+ source: "2.blog/*.md",
99
103
  i18n: i18n ? true : void 0,
100
- route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
104
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
101
105
  agent: {
102
106
  section: "blog",
103
107
  markdown: true,
@@ -131,30 +135,23 @@ function defineGinkoDocsConfig(options) {
131
135
  ? [
132
136
  defineAgentSection({
133
137
  id: "blog",
134
- title: "Blog",
138
+ title: {
139
+ en: "Blog",
140
+ de: "Blog",
141
+ },
135
142
  order: 40,
136
143
  }),
137
144
  ]
138
145
  : []),
139
146
  defineAgentSection({
140
147
  id: "optional",
141
- title: "Documentation",
148
+ title: {
149
+ en: "Documentation",
150
+ de: "Dokumentation",
151
+ },
142
152
  order: 100,
143
153
  }),
144
154
  ],
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
155
  },
159
156
  };
160
157
  if (options.blog)
package/content.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- defineAgentAppPage,
3
2
  defineAgentMetadataFields,
4
3
  defineAgentSection,
5
4
  defineCollection,
@@ -10,10 +9,16 @@ import type { ContentCollectionConfig, ContentConfig } from "@lupinum/ginko-cont
10
9
  import { z } from "zod";
11
10
  import { routeSlugs } from "./shared/route-slugs";
12
11
 
12
+ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
13
+ const nonEmptyString = z.string().trim().min(1);
14
+
13
15
  export interface GinkoDocsContentOptions {
14
- site: { name: string; description: string; url: string };
15
- locales?: Array<"en" | "de">;
16
- defaultLocale?: "en" | "de";
16
+ site: {
17
+ name: string | { en: string; de: string };
18
+ description: string | { en: string; de: string };
19
+ url: string;
20
+ };
21
+ locales?: readonly ["en"] | readonly ["en", "de"];
17
22
  blog?: boolean;
18
23
  }
19
24
 
@@ -22,7 +27,7 @@ const docsSchema = z.object({
22
27
  description: z.string(),
23
28
  icon: z.string().optional(),
24
29
  badge: z.string().optional(),
25
- updated: z.string().optional(),
30
+ updated: isoDate.optional(),
26
31
  sidebar: z.enum(["section", "group"]).optional(),
27
32
  navigation: z
28
33
  .object({
@@ -37,8 +42,8 @@ const blogSchema = z.object({
37
42
  title: z.string(),
38
43
  description: z.string(),
39
44
  badge: z.string().optional(),
40
- date: z.string(),
41
- readingTime: z.string(),
45
+ date: isoDate,
46
+ readingTime: nonEmptyString,
42
47
  author: reference("authors"),
43
48
  image: z.string().optional(),
44
49
  });
@@ -69,10 +74,14 @@ export function defineGinkoDocsConfig(
69
74
  options: GinkoDocsContentOptions,
70
75
  ): DocsContentConfig | DocsBlogContentConfig {
71
76
  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]]));
77
+ const validLocales =
78
+ locales.length === 1
79
+ ? locales[0] === "en"
80
+ : locales.length === 2 && locales[0] === "en" && locales[1] === "de";
81
+ if (!validLocales) {
82
+ throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
83
+ }
84
+ const i18n = locales.length === 2;
76
85
  const metadata = defineAgentMetadataFields([
77
86
  "title",
78
87
  "description",
@@ -88,16 +97,16 @@ export function defineGinkoDocsConfig(
88
97
  type: "page",
89
98
  source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
90
99
  i18n: i18n ? true : undefined,
91
- route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
100
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
92
101
  agent: { section: "optional", markdown: true },
93
102
  strict: true,
94
103
  schema: docsSchema,
95
104
  });
96
105
  const blog = defineCollection({
97
106
  type: "page",
98
- source: "2.blog/**/*.md",
107
+ source: "2.blog/*.md",
99
108
  i18n: i18n ? true : undefined,
100
- route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
109
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
101
110
  agent: { section: "blog", markdown: true },
102
111
  strict: true,
103
112
  schema: blogSchema,
@@ -119,20 +128,13 @@ export function defineGinkoDocsConfig(
119
128
  },
120
129
  markdown: { metadata: { enabled: true, defaultFields: metadata } },
121
130
  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}`,
131
+ ...(options.blog
132
+ ? [defineAgentSection({ id: "blog", title: { en: "Blog", de: "Blog" }, order: 40 })]
133
+ : []),
134
+ defineAgentSection({
135
+ id: "optional",
136
+ title: { en: "Documentation", de: "Dokumentation" },
137
+ order: 100,
136
138
  }),
137
139
  ],
138
140
  },
@@ -11,4 +11,8 @@ export const blog = {
11
11
  toc: { de: "Im Artikel", en: "In this article" },
12
12
  readMore: { de: "Weiterlesen", en: "Read more" },
13
13
  moreArticles: { de: "Weitere Artikel", en: "More articles" },
14
+ empty: {
15
+ de: "Noch keine Artikel veröffentlicht.",
16
+ en: "No articles have been published yet.",
17
+ },
14
18
  } as const;