@lupinum/ginko-docs 0.2.2 → 0.2.5

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 (206) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +15 -13
  3. package/app/app.vue +4 -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 +34 -99
  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 +266 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +25 -9
  59. package/app/components/site/SiteSocialLinks.vue +31 -0
  60. package/app/components/ui/badge/Badge.vue +1 -1
  61. package/app/components/ui/button/Button.vue +1 -1
  62. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  64. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  66. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  67. package/app/components/ui/dropdown-menu/index.ts +0 -9
  68. package/app/components/ui/kbd/Kbd.vue +1 -1
  69. package/app/components/ui/kbd/index.ts +0 -1
  70. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  71. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  72. package/app/components/ui/separator/Separator.vue +1 -1
  73. package/app/components/ui/sheet/SheetContent.vue +1 -1
  74. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  75. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  76. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  77. package/app/components/ui/sheet/index.ts +0 -3
  78. package/app/components/ui/tabs/Tabs.vue +1 -1
  79. package/app/components/ui/tabs/TabsList.vue +1 -1
  80. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  81. package/app/components/ui/tabs/index.ts +0 -1
  82. package/app/composables/site-navigation.utils.ts +69 -12
  83. package/app/composables/useContentRouteAlternates.ts +13 -21
  84. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  85. package/app/composables/useMetaKey.ts +18 -0
  86. package/app/composables/useProseAppearance.ts +24 -0
  87. package/app/composables/useSiteNavigation.ts +10 -40
  88. package/app/config/site.utils.ts +1 -1
  89. package/app/error.vue +1 -5
  90. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  91. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  92. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  93. package/app/features/docs/components/DocsPageContent.vue +93 -47
  94. package/app/features/docs/components/DocsPageNav.vue +1 -1
  95. package/app/features/docs/components/DocsSidebar.vue +37 -21
  96. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  97. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  98. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  99. package/app/features/docs/components/DocsToc.vue +80 -34
  100. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  101. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  102. package/app/features/docs/composables/useRevealActive.ts +58 -0
  103. package/app/features/docs/composables/useScrollspy.ts +87 -19
  104. package/app/features/docs/docs-navigation.ts +17 -30
  105. package/app/features/docs/toc-context.ts +6 -0
  106. package/app/features/search/command-center.ts +11 -90
  107. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  108. package/app/features/search/useCommandCenter.ts +48 -48
  109. package/app/layouts/docs.vue +1 -1
  110. package/app/lib/errors.ts +2 -49
  111. package/app/pages/blog/[slug].vue +26 -9
  112. package/app/pages/blog/index.vue +38 -14
  113. package/app/pages/docs/index.vue +10 -14
  114. package/app/pages/index.vue +180 -61
  115. package/app/plugins/ginko-docs-theme.ts +12 -0
  116. package/app/utils/content.ts +11 -0
  117. package/app/utils/file-icons.ts +90 -0
  118. package/app/utils/index.ts +7 -1
  119. package/app/utils/repository.ts +20 -0
  120. package/content.js +68 -50
  121. package/content.ts +51 -32
  122. package/i18n/messages/global/blog.ts +4 -0
  123. package/i18n/messages/global/command.ts +6 -2
  124. package/i18n/messages/global/docs.ts +4 -0
  125. package/i18n/messages/global/nav.ts +6 -3
  126. package/i18n/messages/index.ts +3 -3
  127. package/icon-bundle.ts +9 -56
  128. package/index.d.ts +7 -1
  129. package/modules/feature-routing.ts +15 -0
  130. package/nuxt.config.ts +21 -7
  131. package/package.json +29 -26
  132. package/runtime/server/agent-markdown.ts +0 -2
  133. package/server/api/_ginko-docs/redirects.get.ts +28 -0
  134. package/server/middleware/redirects.ts +22 -0
  135. package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
  136. package/server/routes/blog/rss.xml.get.ts +5 -0
  137. package/server/utils/blog-feed.ts +49 -0
  138. package/server/utils/feed.ts +70 -0
  139. package/server/utils/redirects.ts +38 -0
  140. package/server/utils/redirects.utils.ts +58 -0
  141. package/shared/theme-palettes.ts +37 -0
  142. package/shared/types/app-config.ts +75 -10
  143. package/tags.ts +47 -59
  144. package/app/components/mdc/MdcAlert.vue +0 -34
  145. package/app/components/mdc/MdcCardGroup.vue +0 -47
  146. package/app/components/mdc/MdcDocImg.vue +0 -25
  147. package/app/components/mdc/MdcField.vue +0 -62
  148. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  149. package/app/components/mdc/MdcPassage.vue +0 -32
  150. package/app/components/mdc/MdcQuizOption.vue +0 -91
  151. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  152. package/app/components/mdc/MdcShortcut.vue +0 -46
  153. package/app/components/mdc/MdcStep.vue +0 -42
  154. package/app/components/ui/accordion/Accordion.vue +0 -15
  155. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  156. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  157. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  158. package/app/components/ui/accordion/index.ts +0 -4
  159. package/app/components/ui/alert/Alert.vue +0 -21
  160. package/app/components/ui/alert/AlertDescription.vue +0 -22
  161. package/app/components/ui/alert/AlertTitle.vue +0 -22
  162. package/app/components/ui/alert/index.ts +0 -34
  163. package/app/components/ui/card/Card.vue +0 -16
  164. package/app/components/ui/card/CardAction.vue +0 -17
  165. package/app/components/ui/card/CardContent.vue +0 -14
  166. package/app/components/ui/card/CardDescription.vue +0 -14
  167. package/app/components/ui/card/CardFooter.vue +0 -14
  168. package/app/components/ui/card/CardHeader.vue +0 -22
  169. package/app/components/ui/card/card-variants.ts +0 -15
  170. package/app/components/ui/card/index.ts +0 -8
  171. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  172. package/app/components/ui/checkbox/index.ts +0 -1
  173. package/app/components/ui/dialog/Dialog.vue +0 -15
  174. package/app/components/ui/dialog/DialogClose.vue +0 -12
  175. package/app/components/ui/dialog/DialogContent.vue +0 -69
  176. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  177. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  178. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  179. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  180. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  181. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  182. package/app/components/ui/dialog/index.ts +0 -10
  183. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  184. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  185. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  186. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  187. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  188. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  189. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  190. package/app/components/ui/input/Input.vue +0 -23
  191. package/app/components/ui/input/index.ts +0 -1
  192. package/app/components/ui/label/Label.vue +0 -14
  193. package/app/components/ui/label/index.ts +0 -1
  194. package/app/components/ui/sheet/SheetClose.vue +0 -12
  195. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  196. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  197. package/app/components/ui/switch/Switch.vue +0 -39
  198. package/app/components/ui/switch/index.ts +0 -1
  199. package/app/components/ui/tabs/TabsContent.vue +0 -21
  200. package/app/components/ui/textarea/Textarea.vue +0 -31
  201. package/app/components/ui/textarea/index.ts +0 -1
  202. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  203. package/app/lib/utils.ts +0 -7
  204. package/i18n/messages/source.ts +0 -5
  205. package/runtime/markdown/inline-code-highlight.ts +0 -155
  206. package/runtime/utils.ts +0 -7
@@ -1,42 +1,110 @@
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
+ }
36
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
+ }
100
+
101
+ const landingTitle = computed(() => localize(config.site.name));
102
+ const landingDescription = computed(() => localize(config.site.description));
37
103
  useSeoMeta({
38
- title: computed(() => localize(config.site.name)),
39
- description: computed(() => localize(config.site.description)),
104
+ title: landingTitle,
105
+ description: landingDescription,
106
+ ogTitle: landingTitle,
107
+ ogDescription: landingDescription,
40
108
  });
41
109
  </script>
42
110
 
@@ -47,9 +115,6 @@ useSeoMeta({
47
115
  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
116
  >
49
117
  <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
118
  <h1
54
119
  class="max-w-4xl text-5xl leading-[0.98] font-semibold tracking-[-0.035em] text-balance text-foreground sm:text-7xl"
55
120
  :class="landing.heroMedia ? 'lg:text-6xl' : 'lg:text-[5.75rem]'"
@@ -75,66 +140,120 @@ useSeoMeta({
75
140
  {{ landing.secondary.label }}
76
141
  </NuxtLink>
77
142
  </div>
143
+ <div v-if="landing.install" class="mt-5">
144
+ <div
145
+ 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"
146
+ >
147
+ <span class="text-muted-foreground select-none" aria-hidden="true">$</span>
148
+ <span class="truncate">{{ landing.install.command }}</span>
149
+ <button
150
+ type="button"
151
+ class="content-codeblock-copy-button"
152
+ :aria-label="copied ? t('docs.copiedText') : t('docs.copyText')"
153
+ @click="copyInstallCommand"
154
+ >
155
+ <Icon
156
+ :name="copied ? 'lucide:check' : 'lucide:clipboard'"
157
+ class="size-3.5"
158
+ aria-hidden="true"
159
+ />
160
+ </button>
161
+ </div>
162
+ </div>
78
163
  </div>
79
164
 
80
- <div v-if="landing.heroMedia" class="hidden w-[26rem] max-w-full shrink-0 lg:block">
165
+ <div
166
+ v-if="landing.heroMedia"
167
+ class="w-full min-w-0 max-w-2xl shrink-0 lg:w-[30rem] lg:max-w-full xl:w-[36rem]"
168
+ >
81
169
  <NuxtImg
82
170
  v-if="landing.heroMedia.type === 'image'"
83
171
  :src="landing.heroMedia.src"
84
172
  :alt="landing.heroMedia.alt"
85
173
  class="w-full rounded-xl border border-border object-cover shadow-sm"
86
- width="416"
174
+ width="576"
87
175
  loading="lazy"
88
176
  />
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>
177
+ <SiteHeroCode v-else-if="heroCodeTabs.length" :tabs="heroCodeTabs" />
113
178
  </div>
114
179
  </div>
115
180
  </section>
116
181
 
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
- >
182
+ <section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-24">
183
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
121
184
  <article
122
- v-for="feature in landing.features"
185
+ v-for="(feature, index) in landing.features"
123
186
  :key="feature.title"
124
- class="min-w-0 md:px-9 md:first:pl-0 md:last:pr-0"
187
+ class="min-w-0 rounded-lg border border-border bg-card p-6"
125
188
  >
126
- <Icon
189
+ <span
127
190
  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">
191
+ class="flex size-10 items-center justify-center rounded-md"
192
+ :class="tint(index).tile"
193
+ >
194
+ <Icon
195
+ :name="feature.icon"
196
+ class="size-5"
197
+ :class="tint(index).icon"
198
+ aria-hidden="true"
199
+ />
200
+ </span>
201
+ <h2 class="mt-5 text-base font-semibold tracking-tight text-foreground">
133
202
  {{ feature.title }}
134
203
  </h2>
135
- <p class="mt-3 leading-7 text-muted-foreground">{{ feature.description }}</p>
204
+ <p class="mt-2 text-sm leading-6 text-muted-foreground">{{ feature.description }}</p>
136
205
  </article>
137
206
  </div>
138
207
  </section>
208
+
209
+ <section v-if="landing.agent" class="border-y border-border bg-agent-background">
210
+ <div
211
+ 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"
212
+ >
213
+ <div class="min-w-0">
214
+ <h2
215
+ class="text-3xl font-semibold tracking-[-0.03em] text-balance text-agent-foreground sm:text-4xl"
216
+ >
217
+ {{ landing.agent.title }}
218
+ </h2>
219
+ <p class="mt-4 max-w-xl text-base leading-7 text-agent-muted">
220
+ {{ landing.agent.description }}
221
+ </p>
222
+ </div>
223
+ <pre
224
+ class="min-w-0 overflow-x-auto rounded-lg border border-white/10 bg-black/30 p-5 font-mono text-[13px] leading-7"
225
+ ><code><span
226
+ v-for="(line, index) in agentTerminalLines"
227
+ :key="index"
228
+ class="block min-h-[1.75rem] whitespace-pre"
229
+ ><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>
230
+ </div>
231
+ </section>
232
+
233
+ <section v-if="landing.cta">
234
+ <div class="mx-auto max-w-6xl px-5 py-20 text-center sm:px-8 sm:py-28">
235
+ <h2
236
+ class="text-3xl font-semibold tracking-[-0.03em] text-balance text-foreground sm:text-5xl"
237
+ >
238
+ {{ landing.cta.title }}
239
+ </h2>
240
+ <div class="mt-9 flex flex-wrap items-center justify-center gap-3">
241
+ <NuxtLink
242
+ :to="landing.cta.primary.to"
243
+ 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"
244
+ >
245
+ {{ landing.cta.primary.label }}
246
+ <Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
247
+ </NuxtLink>
248
+ <NuxtLink
249
+ v-if="landing.cta.secondary"
250
+ :to="landing.cta.secondary.to"
251
+ 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"
252
+ >
253
+ {{ landing.cta.secondary.label }}
254
+ </NuxtLink>
255
+ </div>
256
+ </div>
257
+ </section>
139
258
  </div>
140
259
  </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,31 +23,54 @@ const routeSlugs = {
24
23
  };
25
24
  //#endregion
26
25
  //#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
- });
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);
28
+ /** Former public URLs of a page, as served: locale prefix and translated slugs included. */
29
+ const redirectFrom = z
30
+ .array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
31
+ .optional();
32
+ /**
33
+ * Derives the sitemap lastmod from the authored date so it has one source of
34
+ * truth. Route records require normalized UTC ISO values.
35
+ */
36
+ const withSitemapLastmod = (data, lastmod) =>
37
+ lastmod
38
+ ? {
39
+ ...data,
40
+ sitemap: { lastmod: `${lastmod}T00:00:00.000Z` },
41
+ }
42
+ : data;
43
+ const docsSchemaWithLastmod = z
44
+ .object({
45
+ title: z.string(),
46
+ description: z.string(),
47
+ icon: z.string().optional(),
48
+ badge: z.string().optional(),
49
+ updated: isoDate.optional(),
50
+ redirectFrom,
51
+ sidebar: z.enum(["section", "group"]).optional(),
52
+ navigation: z
53
+ .object({
54
+ title: z.string().optional(),
55
+ icon: z.string().optional(),
56
+ badge: z.string().optional(),
57
+ sidebar: z.enum(["section", "group"]).optional(),
58
+ })
59
+ .optional(),
60
+ })
61
+ .transform((data) => withSitemapLastmod(data, data.updated));
62
+ const blogSchemaWithLastmod = z
63
+ .object({
64
+ title: z.string(),
65
+ description: z.string(),
66
+ badge: z.string().optional(),
67
+ date: isoDate,
68
+ readingTime: nonEmptyString,
69
+ author: reference("authors"),
70
+ image: z.string().optional(),
71
+ redirectFrom,
72
+ })
73
+ .transform((data) => withSitemapLastmod(data, data.date));
52
74
  const authorsSchema = z.object({
53
75
  slug: z.string(),
54
76
  name: z.string(),
@@ -66,10 +88,13 @@ const authorsSchema = z.object({
66
88
  });
67
89
  function defineGinkoDocsConfig(options) {
68
90
  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]]));
91
+ if (
92
+ !(locales.length === 1
93
+ ? locales[0] === "en"
94
+ : locales.length === 2 && locales[0] === "en" && locales[1] === "de")
95
+ )
96
+ throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
97
+ const i18n = locales.length === 2;
73
98
  const metadata = defineAgentMetadataFields([
74
99
  "title",
75
100
  "description",
@@ -85,25 +110,25 @@ function defineGinkoDocsConfig(options) {
85
110
  type: "page",
86
111
  source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
87
112
  i18n: i18n ? true : void 0,
88
- route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
113
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
89
114
  agent: {
90
115
  section: "optional",
91
116
  markdown: true,
92
117
  },
93
118
  strict: true,
94
- schema: docsSchema,
119
+ schema: docsSchemaWithLastmod,
95
120
  });
96
121
  const blog = defineCollection({
97
122
  type: "page",
98
- source: "2.blog/**/*.md",
123
+ source: "2.blog/*.md",
99
124
  i18n: i18n ? true : void 0,
100
- route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
125
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
101
126
  agent: {
102
127
  section: "blog",
103
128
  markdown: true,
104
129
  },
105
130
  strict: true,
106
- schema: blogSchema,
131
+ schema: blogSchemaWithLastmod,
107
132
  });
108
133
  const authors = defineCollection({
109
134
  type: "data",
@@ -131,30 +156,23 @@ function defineGinkoDocsConfig(options) {
131
156
  ? [
132
157
  defineAgentSection({
133
158
  id: "blog",
134
- title: "Blog",
159
+ title: {
160
+ en: "Blog",
161
+ de: "Blog",
162
+ },
135
163
  order: 40,
136
164
  }),
137
165
  ]
138
166
  : []),
139
167
  defineAgentSection({
140
168
  id: "optional",
141
- title: "Documentation",
169
+ title: {
170
+ en: "Documentation",
171
+ de: "Dokumentation",
172
+ },
142
173
  order: 100,
143
174
  }),
144
175
  ],
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
176
  },
159
177
  };
160
178
  if (options.blog)