@lupinum/ginko-docs 0.4.0-rc.8 → 0.4.0

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 (296) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +1 -1
  3. package/README.md +206 -54
  4. package/app/app.config.ts +42 -55
  5. package/app/assets/docs.css +555 -0
  6. package/app/components/DocsBanner.vue +29 -0
  7. package/app/components/DocsExample.vue +43 -0
  8. package/app/components/DocsFooter.vue +19 -0
  9. package/app/components/DocsHeader.vue +112 -0
  10. package/app/components/DocsLanding.vue +55 -0
  11. package/app/components/DocsLanguage.vue +29 -0
  12. package/app/components/DocsNavTree.vue +38 -0
  13. package/app/components/DocsPage.vue +84 -0
  14. package/app/components/DocsPageActions.vue +47 -0
  15. package/app/components/DocsPost.vue +58 -0
  16. package/app/components/DocsSearch.vue +134 -0
  17. package/app/components/DocsShell.vue +20 -0
  18. package/app/components/DocsSidebar.vue +18 -0
  19. package/app/components/DocsTable.vue +11 -0
  20. package/app/components/DocsThemeToggle.vue +45 -0
  21. package/app/components/DocsToc.vue +73 -0
  22. package/app/composables/useDocs.ts +52 -0
  23. package/app/composables/useDocsBlog.ts +32 -0
  24. package/app/composables/useDocsCopy.ts +17 -0
  25. package/app/composables/useDocsPopover.ts +36 -0
  26. package/app/composables/useDocsSite.ts +10 -0
  27. package/app/docs-pages/blog.vue +47 -0
  28. package/app/docs-pages/changelog.vue +27 -0
  29. package/app/docs-pages/doc.vue +14 -0
  30. package/app/docs-pages/docs.vue +9 -0
  31. package/app/docs-pages/index.vue +12 -0
  32. package/app/docs-pages/post.vue +8 -0
  33. package/app/error.vue +23 -115
  34. package/app/layouts/default.vue +25 -7
  35. package/app/lib/blogTypes.ts +8 -0
  36. package/app/lib/docsFirst.ts +11 -0
  37. package/app/lib/docsInit.ts +18 -0
  38. package/app/lib/docsNav.ts +36 -0
  39. package/app/lib/docsPage.ts +44 -0
  40. package/app/lib/markdownComponents.ts +16 -0
  41. package/app/lib/previewToc.ts +18 -0
  42. package/dist/config.d.mts +232 -0
  43. package/dist/config.d.ts +232 -0
  44. package/dist/config.mjs +114 -0
  45. package/dist/pages.d.mts +5 -0
  46. package/dist/pages.d.ts +5 -0
  47. package/dist/pages.mjs +36 -0
  48. package/dist/shared/ginko-docs.BwbF7JBR.mjs +4 -0
  49. package/i18n/de.json +1 -0
  50. package/i18n/en.json +1 -0
  51. package/icons/README.md +2 -0
  52. package/icons/bluesky.svg +1 -0
  53. package/icons/claude.svg +1 -0
  54. package/icons/discord.svg +1 -0
  55. package/icons/github.svg +1 -0
  56. package/icons/linkedin.svg +1 -0
  57. package/icons/mastodon.svg +1 -0
  58. package/icons/openai.svg +1 -0
  59. package/icons/x.svg +1 -0
  60. package/icons/youtube.svg +1 -0
  61. package/nuxt.config.ts +34 -209
  62. package/package.json +43 -76
  63. package/public/_docs/fonts/LICENSE +188 -0
  64. package/public/_docs/fonts/inter-latin-wght-normal.woff2 +0 -0
  65. package/public/_docs/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  66. package/public/_docs/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  67. package/server/feeds/blog-locale.get.ts +9 -0
  68. package/server/feeds/blog.get.ts +4 -0
  69. package/server/lib/blogFeed.ts +35 -0
  70. package/server/lib/rss.ts +57 -0
  71. package/server/mcp/handler.ts +108 -0
  72. package/server/routes/__ginko/docs-mcp.ts +35 -0
  73. package/shared/paths.ts +4 -0
  74. package/app/app.vue +0 -98
  75. package/app/assets/css/prose.css +0 -3240
  76. package/app/assets/css/tailwind.css +0 -394
  77. package/app/assets/css/theme-palettes.css +0 -309
  78. package/app/assets/css/theme-presets.css +0 -141
  79. package/app/components/OgImage/GinkoDocs.satori.vue +0 -238
  80. package/app/components/content/DocumentPageShell.vue +0 -55
  81. package/app/components/content/Feedback.vue +0 -102
  82. package/app/components/content/ImageZoomDialog.vue +0 -84
  83. package/app/components/content/PageMarkdownCopy.vue +0 -104
  84. package/app/components/content/imageZoom.ts +0 -25
  85. package/app/components/mdc/MdcAccordion.vue +0 -67
  86. package/app/components/mdc/MdcAccordionItem.vue +0 -42
  87. package/app/components/mdc/MdcApi.vue +0 -159
  88. package/app/components/mdc/MdcAside.vue +0 -23
  89. package/app/components/mdc/MdcCallout.vue +0 -61
  90. package/app/components/mdc/MdcCard.vue +0 -175
  91. package/app/components/mdc/MdcCards.vue +0 -30
  92. package/app/components/mdc/MdcCenter.vue +0 -71
  93. package/app/components/mdc/MdcCodeGroup.vue +0 -197
  94. package/app/components/mdc/MdcCodeTree.vue +0 -234
  95. package/app/components/mdc/MdcCollapse.vue +0 -61
  96. package/app/components/mdc/MdcColumn.vue +0 -70
  97. package/app/components/mdc/MdcDropcap.vue +0 -84
  98. package/app/components/mdc/MdcError.vue +0 -23
  99. package/app/components/mdc/MdcExcerpt.vue +0 -26
  100. package/app/components/mdc/MdcFigure.vue +0 -85
  101. package/app/components/mdc/MdcFiles.vue +0 -120
  102. package/app/components/mdc/MdcIdea.vue +0 -17
  103. package/app/components/mdc/MdcInfo.vue +0 -17
  104. package/app/components/mdc/MdcInlineToc.vue +0 -51
  105. package/app/components/mdc/MdcKbd.vue +0 -10
  106. package/app/components/mdc/MdcLayout.vue +0 -57
  107. package/app/components/mdc/MdcNote.vue +0 -17
  108. package/app/components/mdc/MdcQuiz.vue +0 -241
  109. package/app/components/mdc/MdcQuizQuestion.vue +0 -221
  110. package/app/components/mdc/MdcReadMore.vue +0 -70
  111. package/app/components/mdc/MdcSteps.vue +0 -59
  112. package/app/components/mdc/MdcSuccess.vue +0 -23
  113. package/app/components/mdc/MdcTab.vue +0 -17
  114. package/app/components/mdc/MdcTabs.vue +0 -150
  115. package/app/components/mdc/MdcTimeline.vue +0 -17
  116. package/app/components/mdc/MdcTimelineItem.vue +0 -40
  117. package/app/components/mdc/MdcWarning.vue +0 -23
  118. package/app/components/mdc/api.utils.ts +0 -132
  119. package/app/components/mdc/code-tree.utils.ts +0 -22
  120. package/app/components/mdc/icons.ts +0 -10
  121. package/app/components/mdc/quiz.utils.ts +0 -38
  122. package/app/components/prose/ProseA.vue +0 -27
  123. package/app/components/prose/ProseBlockquote.vue +0 -30
  124. package/app/components/prose/ProseCode.vue +0 -14
  125. package/app/components/prose/ProseEm.vue +0 -14
  126. package/app/components/prose/ProseH1.vue +0 -18
  127. package/app/components/prose/ProseH2.vue +0 -29
  128. package/app/components/prose/ProseH3.vue +0 -29
  129. package/app/components/prose/ProseH4.vue +0 -29
  130. package/app/components/prose/ProseH5.vue +0 -15
  131. package/app/components/prose/ProseH6.vue +0 -15
  132. package/app/components/prose/ProseHr.vue +0 -12
  133. package/app/components/prose/ProseImg.vue +0 -55
  134. package/app/components/prose/ProseLi.vue +0 -14
  135. package/app/components/prose/ProseOl.vue +0 -14
  136. package/app/components/prose/ProseP.vue +0 -14
  137. package/app/components/prose/ProsePre.vue +0 -180
  138. package/app/components/prose/ProseStrong.vue +0 -14
  139. package/app/components/prose/ProseTable.vue +0 -18
  140. package/app/components/prose/ProseTbody.vue +0 -14
  141. package/app/components/prose/ProseTd.vue +0 -14
  142. package/app/components/prose/ProseTh.vue +0 -14
  143. package/app/components/prose/ProseThead.vue +0 -14
  144. package/app/components/prose/ProseTr.vue +0 -14
  145. package/app/components/prose/ProseUl.vue +0 -14
  146. package/app/components/site/ModeToggle.vue +0 -81
  147. package/app/components/site/SiteBanner.vue +0 -97
  148. package/app/components/site/SiteFooter.vue +0 -90
  149. package/app/components/site/SiteHeader.vue +0 -371
  150. package/app/components/site/SiteHeroCode.vue +0 -173
  151. package/app/components/site/SiteInteractionLayer.vue +0 -42
  152. package/app/components/site/SiteLocaleSwitcher.vue +0 -190
  153. package/app/components/site/SiteLogoMark.vue +0 -12
  154. package/app/components/site/SiteSkipLink.vue +0 -14
  155. package/app/components/site/SiteSocialLinks.vue +0 -33
  156. package/app/components/site/header-utils.ts +0 -3
  157. package/app/components/site/mode-toggle.utils.ts +0 -18
  158. package/app/components/ui/badge/Badge.vue +0 -28
  159. package/app/components/ui/badge/index.ts +0 -24
  160. package/app/components/ui/button/Button.vue +0 -31
  161. package/app/components/ui/button/index.ts +0 -35
  162. package/app/components/ui/collapsible/Collapsible.vue +0 -15
  163. package/app/components/ui/collapsible/CollapsibleContent.vue +0 -25
  164. package/app/components/ui/collapsible/CollapsibleTrigger.vue +0 -12
  165. package/app/components/ui/collapsible/index.ts +0 -3
  166. package/app/components/ui/dropdown-menu/DropdownMenu.vue +0 -15
  167. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +0 -50
  168. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +0 -41
  169. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +0 -15
  170. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +0 -37
  171. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +0 -23
  172. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +0 -14
  173. package/app/components/ui/dropdown-menu/index.ts +0 -7
  174. package/app/components/ui/kbd/Kbd.vue +0 -23
  175. package/app/components/ui/kbd/index.ts +0 -21
  176. package/app/components/ui/scroll-area/ScrollArea.vue +0 -39
  177. package/app/components/ui/scroll-area/ScrollBar.vue +0 -33
  178. package/app/components/ui/scroll-area/index.ts +0 -2
  179. package/app/components/ui/separator/Separator.vue +0 -27
  180. package/app/components/ui/separator/index.ts +0 -1
  181. package/app/components/ui/sheet/Sheet.vue +0 -15
  182. package/app/components/ui/sheet/SheetContent.vue +0 -59
  183. package/app/components/ui/sheet/SheetDescription.vue +0 -21
  184. package/app/components/ui/sheet/SheetOverlay.vue +0 -26
  185. package/app/components/ui/sheet/SheetTitle.vue +0 -21
  186. package/app/components/ui/sheet/SheetTrigger.vue +0 -12
  187. package/app/components/ui/sheet/index.ts +0 -5
  188. package/app/components/ui/tabs/Tabs.vue +0 -24
  189. package/app/components/ui/tabs/TabsList.vue +0 -26
  190. package/app/components/ui/tabs/TabsTrigger.vue +0 -28
  191. package/app/components/ui/tabs/index.ts +0 -3
  192. package/app/composables/site-navigation.utils.ts +0 -133
  193. package/app/composables/useCanonicalUrl.ts +0 -9
  194. package/app/composables/useContentRouteAlternates.ts +0 -35
  195. package/app/composables/useGinkoAnalytics.ts +0 -52
  196. package/app/composables/useGinkoDocsConfig.ts +0 -6
  197. package/app/composables/useGinkoOgImage.ts +0 -20
  198. package/app/composables/useLocalizedPath.ts +0 -12
  199. package/app/composables/useLocalizedRouteSwitch.ts +0 -31
  200. package/app/composables/useMetaKey.ts +0 -18
  201. package/app/composables/usePageMarkdownActions.ts +0 -79
  202. package/app/composables/useProseAppearance.ts +0 -24
  203. package/app/composables/useSchemaJsonLd.ts +0 -25
  204. package/app/composables/useSiteNavigation.ts +0 -64
  205. package/app/config/site.utils.ts +0 -36
  206. package/app/features/docs/components/DocsBreadcrumb.vue +0 -37
  207. package/app/features/docs/components/DocsContributeLinks.vue +0 -75
  208. package/app/features/docs/components/DocsMobileToc.vue +0 -108
  209. package/app/features/docs/components/DocsPageContent.vue +0 -234
  210. package/app/features/docs/components/DocsPageNav.vue +0 -57
  211. package/app/features/docs/components/DocsSidebar.vue +0 -145
  212. package/app/features/docs/components/DocsSidebarDropdown.vue +0 -70
  213. package/app/features/docs/components/DocsSidebarItem.vue +0 -122
  214. package/app/features/docs/components/DocsSidebarList.vue +0 -35
  215. package/app/features/docs/components/DocsSidebarRow.vue +0 -27
  216. package/app/features/docs/components/DocsSidebarTabs.vue +0 -59
  217. package/app/features/docs/components/DocsToc.vue +0 -106
  218. package/app/features/docs/composables/useDocsNavigation.ts +0 -29
  219. package/app/features/docs/composables/useDocsNavigationData.ts +0 -18
  220. package/app/features/docs/composables/useRevealActive.ts +0 -58
  221. package/app/features/docs/composables/useScrollspy.ts +0 -135
  222. package/app/features/docs/docs-navigation.ts +0 -157
  223. package/app/features/docs/toc-context.ts +0 -6
  224. package/app/features/search/command-center.ts +0 -110
  225. package/app/features/search/components/SiteCommandCenter.vue +0 -359
  226. package/app/features/search/components/SiteSearchPageHighlight.vue +0 -126
  227. package/app/features/search/useCommandCenter.ts +0 -292
  228. package/app/layouts/blog.vue +0 -15
  229. package/app/layouts/docs.vue +0 -20
  230. package/app/lib/errors.ts +0 -37
  231. package/app/lib/schema-org.ts +0 -38
  232. package/app/pages/blog/[slug].vue +0 -228
  233. package/app/pages/blog/index.vue +0 -130
  234. package/app/pages/docs/[...slug].vue +0 -10
  235. package/app/pages/docs/index.vue +0 -19
  236. package/app/pages/index.vue +0 -278
  237. package/app/plugins/ginko-docs-theme.ts +0 -14
  238. package/app/plugins/ssr-width.ts +0 -6
  239. package/app/utils/content.ts +0 -67
  240. package/app/utils/file-icons.ts +0 -90
  241. package/app/utils/index.ts +0 -7
  242. package/app/utils/repository.ts +0 -20
  243. package/components.ts +0 -9
  244. package/content-collections.ts +0 -89
  245. package/content.js +0 -200
  246. package/content.ts +0 -104
  247. package/i18n/i18n.config.ts +0 -10
  248. package/i18n/locales.ts +0 -81
  249. package/i18n/messages/global/banner.ts +0 -8
  250. package/i18n/messages/global/blog.ts +0 -18
  251. package/i18n/messages/global/command.ts +0 -49
  252. package/i18n/messages/global/docs.ts +0 -36
  253. package/i18n/messages/global/errors.ts +0 -36
  254. package/i18n/messages/global/feedback.ts +0 -11
  255. package/i18n/messages/global/index.ts +0 -21
  256. package/i18n/messages/global/nav.ts +0 -26
  257. package/i18n/messages/global/site.ts +0 -4
  258. package/i18n/messages/global/theme.ts +0 -9
  259. package/i18n/messages/index.ts +0 -20
  260. package/i18n/messages/localize.ts +0 -35
  261. package/i18n/routes.ts +0 -33
  262. package/icon-bundle.ts +0 -116
  263. package/index.d.ts +0 -37
  264. package/modules/analytics-boundary.ts +0 -23
  265. package/modules/feature-routing.ts +0 -68
  266. package/modules/syntax-highlighting.ts +0 -120
  267. package/public/fonts/public-sans/LICENSE +0 -93
  268. package/public/fonts/public-sans/README.md +0 -12
  269. package/public/fonts/public-sans/public-sans-400-normal-latin.woff +0 -0
  270. package/public/fonts/public-sans/public-sans-500-normal-latin.woff +0 -0
  271. package/public/fonts/public-sans/public-sans-600-normal-latin.woff +0 -0
  272. package/public/fonts/public-sans/public-sans-700-normal-latin.woff +0 -0
  273. package/runtime/generate-favicons.mjs +0 -124
  274. package/runtime/server/agent-markdown.ts +0 -19
  275. package/server/api/_ginko-docs/redirects.get.ts +0 -28
  276. package/server/mcp/tools/get-page.ts +0 -14
  277. package/server/mcp/tools/list-pages.ts +0 -10
  278. package/server/middleware/redirects.ts +0 -22
  279. package/server/plugins/agent-markdown.ts +0 -6
  280. package/server/plugins/sitemap-policy.ts +0 -12
  281. package/server/routes/.well-known/ai-catalog.json.get.ts +0 -10
  282. package/server/routes/[locale]/blog/rss.xml.get.ts +0 -13
  283. package/server/routes/blog/rss.xml.get.ts +0 -5
  284. package/server/routes/mcp/server-card.get.ts +0 -14
  285. package/server/utils/blog-feed.ts +0 -51
  286. package/server/utils/feed.ts +0 -70
  287. package/server/utils/mcp-server-card.ts +0 -45
  288. package/server/utils/redirects.ts +0 -38
  289. package/server/utils/redirects.utils.ts +0 -58
  290. package/server/utils/sitemap.ts +0 -45
  291. package/shared/route-slugs.ts +0 -5
  292. package/shared/theme-palettes.ts +0 -37
  293. package/shared/theme-presets.ts +0 -3
  294. package/shared/types/app-config.ts +0 -172
  295. package/shared/types/nuxt-config.ts +0 -14
  296. package/tags.ts +0 -192
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ // The docs body: the navigation on the side and the page next to it. On phones the navigation lives in the
3
+ // header's menu (DocsHeader); the bar above the page holds the page's own controls (slot `bar`, the table of
4
+ // contents). It needs no header of its own, so it works in the standalone frame and inside a site's layout alike.
5
+ const { t } = useI18n()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="docs-shell">
10
+ <nav class="docs-sidebar" :aria-label="t('docs.nav')">
11
+ <DocsSidebar />
12
+ </nav>
13
+ <div id="docs-content" class="docs-content" tabindex="-1">
14
+ <div class="docs-bar">
15
+ <slot name="bar" />
16
+ </div>
17
+ <slot />
18
+ </div>
19
+ </div>
20
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ // The side navigation: the section tabs (when the docs have several), then the pages of the active tab in their
3
+ // sections. Used in the sidebar and in the phone menu.
4
+ const { t } = useI18n()
5
+ const { tabs, activeTab, sections } = useDocs()
6
+ </script>
7
+
8
+ <template>
9
+ <div>
10
+ <nav v-if="tabs.length" class="docs-tabs" :aria-label="t('docs.sections')">
11
+ <NuxtLink v-for="tab in tabs" :key="tab.title" :to="tab.path" :aria-current="tab.title === activeTab ? 'true' : undefined">{{ tab.title }}</NuxtLink>
12
+ </nav>
13
+ <section v-for="(s, i) in sections" :key="i" class="docs-nav-section">
14
+ <h2 v-if="s.title" class="docs-nav-heading">{{ s.title }}</h2>
15
+ <DocsNavTree :nodes="s.nodes" />
16
+ </section>
17
+ </div>
18
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ // Markdown tables scroll sideways on a phone. The wrapper is the scroll box: keyboard users can focus it and scroll with the arrow keys.
3
+ // role=group, not region: a page can hold several tables, and landmarks with the same name are an error.
4
+ const { t } = useI18n()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="docs-table" role="group" tabindex="0" :aria-label="t('docs.table')">
9
+ <table><slot /></table>
10
+ </div>
11
+ </template>
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ // Auto, light or dark. Auto follows the system and is the default; a choice lives in localStorage and
3
+ // <html data-theme>, and the head script applies it before paint. Both icons are in the HTML and CSS shows the
4
+ // right one, so the server render never guesses the reader's scheme.
5
+ import { COLOR_KEY, colorInitScript } from '../lib/docsInit'
6
+
7
+ const { t } = useI18n()
8
+ useHead({ script: [{ key: 'docs-color-init', innerHTML: colorInitScript(), tagPriority: 'critical' }] })
9
+
10
+ // `segmented` is the phone menu's Auto/Light/Dark switch: the same states and the same storage.
11
+ const props = defineProps<{ segmented?: boolean }>()
12
+ type Choice = 'auto' | 'light' | 'dark'
13
+ const system = (): 'light' | 'dark' => (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
14
+ const current = (): Choice => (document.documentElement.getAttribute('data-theme') as Choice | null) ?? 'auto'
15
+ // Only read when the switch is on screen (the phone menu renders on the client), so SSR never guesses.
16
+ const choice = ref<Choice>(props.segmented && import.meta.client ? current() : 'auto')
17
+
18
+ function choose(next: Choice) {
19
+ const html = document.documentElement
20
+ if (next === 'auto') html.removeAttribute('data-theme')
21
+ else html.setAttribute('data-theme', next)
22
+ choice.value = next
23
+ try {
24
+ if (next === 'auto') localStorage.removeItem(COLOR_KEY)
25
+ else localStorage.setItem(COLOR_KEY, next)
26
+ } catch { /* storage blocked: the choice lasts until reload */ }
27
+ }
28
+ // The header button flips what the reader sees; flipping back to the system's scheme returns to Auto.
29
+ function toggle() {
30
+ const shown = current() === 'auto' ? system() : current()
31
+ const next = shown === 'dark' ? 'light' : 'dark'
32
+ choose(next === system() ? 'auto' : next)
33
+ }
34
+ const OPTIONS = ['auto', 'light', 'dark'] as const
35
+ </script>
36
+
37
+ <template>
38
+ <div v-if="props.segmented" class="docs-segmented" role="group" :aria-label="t('docs.theme.label')">
39
+ <button v-for="o in OPTIONS" :key="o" type="button" :aria-pressed="choice === o" @click="choose(o)">{{ t(`docs.theme.${o}`) }}</button>
40
+ </div>
41
+ <button v-else type="button" class="docs-icon-button" :aria-label="t('docs.theme.toggle')" :title="t('docs.theme.toggle')" @click="toggle">
42
+ <Icon name="lucide:moon" class="docs-icon docs-when-light" />
43
+ <Icon name="lucide:sun" class="docs-icon docs-when-dark" />
44
+ </button>
45
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ // The table of contents of a page: a rail beside the text on wide screens (`aside`), a menu in the docs bar
3
+ // on narrower ones (`menu`). The current section is the last heading that has passed the top of the screen.
4
+ interface TocLink { id: string, text: string, children?: TocLink[] }
5
+ const props = defineProps<{ links: TocLink[], variant: 'aside' | 'menu' }>()
6
+ const { t } = useI18n()
7
+ const { open, toggle } = useDocsPopover()
8
+
9
+ const flat = computed(() => props.links.flatMap((l) => [{ ...l, sub: false }, ...(l.children ?? []).map((c) => ({ ...c, sub: true }))]))
10
+ const current = ref<string>()
11
+
12
+ // A scroll listener instead of an IntersectionObserver: the text may render after this component on client
13
+ // navigation, and looking the headings up on each (throttled) scroll needs no bookkeeping for that.
14
+ let frame = 0
15
+ function spy() {
16
+ cancelAnimationFrame(frame)
17
+ frame = requestAnimationFrame(() => {
18
+ const top = Number.parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 96
19
+ let id: string | undefined
20
+ for (const l of flat.value) {
21
+ const el = document.getElementById(l.id)
22
+ if (el && el.getBoundingClientRect().top <= top + 8) id = l.id
23
+ }
24
+ current.value = id ?? flat.value[0]?.id
25
+ })
26
+ }
27
+ onMounted(() => {
28
+ spy()
29
+ window.addEventListener('scroll', spy, { passive: true })
30
+ })
31
+ onBeforeUnmount(() => {
32
+ cancelAnimationFrame(frame)
33
+ window.removeEventListener('scroll', spy)
34
+ })
35
+ watch(() => props.links, () => nextTick(spy))
36
+ // A long table of contents scrolls in its own box: keep the current entry in view (the box only, not the page).
37
+ const aside = ref<HTMLElement | null>(null)
38
+ watch(current, (id) => {
39
+ const box = aside.value
40
+ const link = id ? box?.querySelector<HTMLElement>(`a[href="#${CSS.escape(id)}"]`) : undefined
41
+ if (!box || !link || box.scrollHeight <= box.clientHeight) return
42
+ const top = link.getBoundingClientRect().top - box.getBoundingClientRect().top + box.scrollTop
43
+ if (top < box.scrollTop + 32 || top > box.scrollTop + box.clientHeight - 48) box.scrollTop = top - box.clientHeight / 3
44
+ })
45
+ const title = computed(() => flat.value.find((l) => l.id === current.value)?.text)
46
+ </script>
47
+
48
+ <template>
49
+ <nav v-if="props.variant === 'aside'" ref="aside" class="docs-toc" :aria-label="t('docs.onThisPage')">
50
+ <p class="docs-toc-title">
51
+ <Icon name="lucide:text-align-start" class="docs-icon" />{{ t('docs.onThisPage') }}
52
+ </p>
53
+ <ul class="docs-toc-list">
54
+ <li v-for="l in flat" :key="l.id">
55
+ <a :href="`#${l.id}`" :class="{ 'is-sub': l.sub }" :aria-current="current === l.id ? 'location' : undefined">{{ l.text }}</a>
56
+ </li>
57
+ </ul>
58
+ <slot />
59
+ </nav>
60
+ <div v-else ref="popover" class="docs-toc-menu">
61
+ <button type="button" :aria-expanded="open" aria-controls="docs-toc-menu" @click="toggle">
62
+ <span>{{ title && flat.length > 1 ? title : t('docs.onThisPage') }}</span>
63
+ <Icon name="lucide:chevron-down" class="docs-icon" />
64
+ </button>
65
+ <nav v-if="open" id="docs-toc-menu" class="docs-popover" :aria-label="t('docs.onThisPage')">
66
+ <ul class="docs-toc-list">
67
+ <li v-for="l in flat" :key="l.id">
68
+ <a :href="`#${l.id}`" :class="{ 'is-sub': l.sub }" :aria-current="current === l.id ? 'location' : undefined" @click="toggle">{{ l.text }}</a>
69
+ </li>
70
+ </ul>
71
+ </nav>
72
+ </div>
73
+ </template>
@@ -0,0 +1,52 @@
1
+ // What is docs-specific on top of Ginko's navigation: the tree comes from `useGinkoNav`, the
2
+ // breadcrumb chain from `useGinkoBreadcrumbs`; this adds tabs and sections from the entries' fields.
3
+ import schema from '#ginko/schema'
4
+ import { enrichNav, type DocNode, type DocTab } from '../lib/docsNav'
5
+
6
+ export function useDocs() {
7
+ const items = useGinko('docs')
8
+ const tree = useGinkoNav(['docs'])
9
+ const chain = useGinkoBreadcrumbs(['docs'])
10
+
11
+ // Ginko's nodes carry title and path; badge, icon, section and tab live on the entry (rules: enrichNav).
12
+ const nodes = computed<DocNode[]>(() => {
13
+ const byId = new Map(items.value.items.map((d) => [d.id, d.data]))
14
+ return enrichNav(tree.value, (id) => byId.get(id))
15
+ })
16
+
17
+ const tabs = computed<DocTab[]>(() => {
18
+ const out = new Map<string, DocTab>()
19
+ for (const n of nodes.value) if (n.tab && !out.has(n.tab)) out.set(n.tab, { title: n.tab, path: n.path })
20
+ return out.size > 1 ? [...out.values()] : []
21
+ })
22
+ const activeTab = computed(() => (tabs.value.length ? nodes.value.find((n) => n.path === chain.value[0]?.path)?.tab ?? tabs.value[0]!.title : undefined))
23
+
24
+ // Roots of the active tab, grouped by `section` (a section is a run of neighbours with the same one).
25
+ const sections = computed(() => {
26
+ const out: { title?: string; nodes: DocNode[] }[] = []
27
+ for (const node of nodes.value) {
28
+ if (tabs.value.length && (node.tab ?? tabs.value[0]!.title) !== activeTab.value) continue
29
+ const last = out.at(-1)
30
+ if (last && last.title === node.section) last.nodes.push(node)
31
+ else out.push({ title: node.section, nodes: [node] })
32
+ }
33
+ return out
34
+ })
35
+
36
+ // Tab (when tabs are shown), then the parent chain down to this page.
37
+ const crumbs = computed<{ title: string; path: string }[]>(() => {
38
+ const tab = tabs.value.find((t) => t.title === activeTab.value)
39
+ return [...(tab ? [tab] : []), ...chain.value.map(({ title, path }) => ({ title, path }))]
40
+ })
41
+
42
+ /** The first page of the docs in this language: where `/docs` (and `/` without a start page) lead. */
43
+ const first = computed(() => nodes.value[0]?.path)
44
+
45
+ /** Where the docs live in this language (docsCollection's `base`, `/docs` by default), e.g. `/de/docs`. */
46
+ const { localePath } = useI18n()
47
+ const { mainLocale } = useGinkoSite()
48
+ const route = (schema.bricks.docs as { route?: Record<string, string> } | undefined)?.route?.[mainLocale]
49
+ const base = computed(() => localePath(route?.replace(/\/\[slug\]$/, '') ?? '/docs'))
50
+
51
+ return { nodes, tabs, activeTab, sections, crumbs, first, base }
52
+ }
@@ -0,0 +1,32 @@
1
+ // The optional blog and changelog: whether the site has them, where they live in this language, and typed
2
+ // reads. The layer cannot know the site's bricks at compile time, so the reads go through one cast here.
3
+ import schema from '#ginko/schema'
4
+ import { BLOG_PATH, CHANGELOG_PATH } from '../../shared/paths'
5
+ import type { Author, ChangelogEntry, Post } from '../lib/blogTypes'
6
+
7
+
8
+ const has = (brick: string) => Object.hasOwn(schema.bricks, brick)
9
+ const read = <T>(brick: string) => (useGinko as unknown as (b: string) => ComputedRef<{ items: T[] }>)(brick)
10
+
11
+ export function useDocsBlog() {
12
+ const { localePath } = useI18n()
13
+ const hasPosts = has('posts')
14
+ const hasChangelog = has('changelog')
15
+
16
+ const authors = hasPosts && has('authors') ? read<Author>('authors') : undefined
17
+ const authorsOf = (post: Post) => (post.data.authors ?? []).flatMap((id) => authors?.value.items.find((a) => a.id === id) ?? [])
18
+ const { format } = useI18n()
19
+ const formatDate = (iso: string) => format.date(iso, 'long')
20
+
21
+ // `draft` is a reserved content key, no schema field; it reaches the bundle only in dev, where drafts show.
22
+ const isDraft = (post: Post) => 'draft' in post.data && post.data.draft === true
23
+ return {
24
+ hasPosts, hasChangelog, authorsOf, isDraft, formatDate,
25
+ blogPath: computed(() => localePath(BLOG_PATH)),
26
+ feedPath: computed(() => localePath(`${BLOG_PATH}/rss.xml`)),
27
+ changelogPath: computed(() => localePath(CHANGELOG_PATH)),
28
+ posts: () => read<Post>('posts'),
29
+ changelog: () => read<ChangelogEntry>('changelog'),
30
+ post: (slug: string) => (useGinkoEntry as unknown as (b: string, s: string) => ComputedRef<Post>)('posts', slug),
31
+ }
32
+ }
@@ -0,0 +1,17 @@
1
+ // Copies text and shows the result for two seconds: `copied`, or `failed` when the clipboard is blocked.
2
+ export function useDocsCopy() {
3
+ const state = ref<'idle' | 'copied' | 'failed'>('idle')
4
+ let timer: ReturnType<typeof setTimeout> | undefined
5
+ async function copy(text: string | (() => Promise<string>)) {
6
+ try {
7
+ await navigator.clipboard.writeText(typeof text === 'string' ? text : await text())
8
+ state.value = 'copied'
9
+ } catch {
10
+ state.value = 'failed'
11
+ }
12
+ clearTimeout(timer)
13
+ timer = setTimeout(() => { state.value = 'idle' }, 2000)
14
+ }
15
+ onBeforeUnmount(() => clearTimeout(timer))
16
+ return { state, copy }
17
+ }
@@ -0,0 +1,36 @@
1
+ // A small disclosure popover (language menu, page actions, phone table of contents): a button toggles a panel;
2
+ // a click outside, Escape or a page change closes it, and Escape returns focus to the button.
3
+ // The component puts `ref="popover"` on the element that holds both the button and the panel, and binds `toggle`
4
+ // to the button's click (Escape returns focus to that button).
5
+ export function useDocsPopover() {
6
+ const open = ref(false)
7
+ const root = useTemplateRef<HTMLElement>('popover')
8
+ const route = useRoute()
9
+ let trigger: HTMLElement | undefined
10
+
11
+ function close(refocus = false) {
12
+ if (!open.value) return
13
+ open.value = false
14
+ if (refocus) (trigger ?? root.value?.querySelector<HTMLElement>('[aria-expanded]'))?.focus()
15
+ }
16
+ const onClick = (e: MouseEvent) => { if (!root.value?.contains(e.target as Node)) close() }
17
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close(true) }
18
+ function listen(on: boolean) {
19
+ if (on) {
20
+ document.addEventListener('click', onClick, true)
21
+ document.addEventListener('keydown', onKey)
22
+ } else {
23
+ document.removeEventListener('click', onClick, true)
24
+ document.removeEventListener('keydown', onKey)
25
+ }
26
+ }
27
+
28
+ watch(open, (value) => listen(value))
29
+ watch(() => route.fullPath, () => close())
30
+ onBeforeUnmount(() => listen(false))
31
+ function toggle(event?: Event) {
32
+ if (event?.currentTarget instanceof HTMLElement) trigger = event.currentTarget
33
+ open.value = !open.value
34
+ }
35
+ return { open, toggle, close }
36
+ }
@@ -0,0 +1,10 @@
1
+ // The site's own entries in app.config (nav, footer and banner links): their texts and their addresses.
2
+ export function useDocsSite() {
3
+ const { t, localePath } = useI18n()
4
+ return {
5
+ /** A message key the site defines (`docs.nav.<id>`, `docs.footer.<id>`, `docs.banner.<id>`); the layer's types do not know them. */
6
+ text: t as (key: string) => string,
7
+ /** An internal path in the reader's language; an absolute URL, a mail link or an anchor stays as written. */
8
+ href: (raw: string) => (raw.startsWith('/') && !raw.startsWith('//') ? localePath(raw) : raw),
9
+ }
10
+ }
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ // The blog: all posts, newest first (the collection's order).
3
+ const { t } = useI18n()
4
+ const blog = useDocsBlog()
5
+ const posts = blog.posts()
6
+ useGinkoSeo(() => ({ title: t('docs.blog'), description: t('docs.blogLead') }))
7
+ useHead(() => ({ link: [{ rel: 'alternate', type: 'application/rss+xml', title: t('docs.blog'), href: blog.feedPath.value }] }))
8
+ // Nitro's crawler only follows links to pages (no `.xml`), but it also reads this header, so `nuxt generate` writes the feed.
9
+ if (import.meta.server) useResponseHeader('x-nitro-prerender').value = blog.feedPath.value
10
+ </script>
11
+
12
+ <template>
13
+ <div class="docs-blog">
14
+ <div class="docs-blog-panel">
15
+ <header class="docs-blog-head">
16
+ <div>
17
+ <h1>{{ t('docs.blog') }}</h1>
18
+ <p class="docs-lead">{{ t('docs.blogLead') }}</p>
19
+ </div>
20
+ <a :href="blog.feedPath.value" type="application/rss+xml"><Icon name="lucide:rss" class="docs-icon" />{{ t('docs.rss') }}</a>
21
+ </header>
22
+ <ul class="docs-posts">
23
+ <li v-for="post in posts.items" :key="post.id">
24
+ <div class="docs-post-side">
25
+ <p class="docs-meta"><time :datetime="post.data.date">{{ blog.formatDate(post.data.date) }}</time></p>
26
+ <p v-if="blog.authorsOf(post).length" class="docs-authors">
27
+ <template v-for="a in blog.authorsOf(post)" :key="a.id">
28
+ <GinkoImage v-if="a.data.avatar" :image="a.data.avatar" class="docs-avatar" />
29
+ </template>
30
+ {{ blog.authorsOf(post).map((a) => a.data.name).join(', ') }}
31
+ </p>
32
+ </div>
33
+ <div class="docs-post-main">
34
+ <h2>
35
+ <NuxtLink :to="post.url">{{ post.data.title }}</NuxtLink>
36
+ <span v-if="blog.isDraft(post)" class="docs-tag docs-tag-draft">{{ t('docs.draft') }}</span>
37
+ </h2>
38
+ <p v-if="post.data.description" class="docs-post-teaser">{{ post.data.description }}</p>
39
+ <p v-if="post.data.tags?.length" class="docs-tags">
40
+ <span v-for="tag in post.data.tags" :key="tag.tag" class="docs-tag">{{ tag.tag }}</span>
41
+ </p>
42
+ </div>
43
+ </li>
44
+ </ul>
45
+ </div>
46
+ </div>
47
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { markdownComponents } from '../lib/markdownComponents'
3
+ // The changelog: one page, one section per release, newest first (the collection's order). Each version has an anchor.
4
+ const { t } = useI18n()
5
+ const blog = useDocsBlog()
6
+ const entries = blog.changelog()
7
+ useGinkoSeo(() => ({ title: t('docs.changelog') }))
8
+ // `1.2.0` -> `v1-2-0`, a valid id and a stable address for the release.
9
+ const anchor = (version: string) => `v${version.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`
10
+ </script>
11
+
12
+ <template>
13
+ <div class="docs-blog">
14
+ <div class="docs-blog-panel">
15
+ <header class="docs-blog-head">
16
+ <h1>{{ t('docs.changelog') }}</h1>
17
+ </header>
18
+ <section v-for="entry in entries.items" :id="anchor(entry.data.version)" :key="entry.id" class="docs-release">
19
+ <div class="docs-release-meta">
20
+ <h2><a :href="`#${anchor(entry.data.version)}`">{{ entry.data.version }}</a></h2>
21
+ <p class="docs-meta"><time :datetime="entry.data.date">{{ blog.formatDate(entry.data.date) }}</time></p>
22
+ </div>
23
+ <GinkoMarkdown :value="entry.data.body" :components="markdownComponents" class="docs-prose" />
24
+ </section>
25
+ </div>
26
+ </div>
27
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ const route = useRoute()
3
+ const doc = useGinkoEntry('docs', () => route.params.slug)
4
+ const toc = computed(() => doc.value.data.body.toc?.links ?? [])
5
+ </script>
6
+
7
+ <template>
8
+ <DocsShell>
9
+ <template v-if="toc.length" #bar>
10
+ <DocsToc :links="toc" variant="menu" />
11
+ </template>
12
+ <DocsPage :doc="doc" />
13
+ </DocsShell>
14
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ import { toFirstDoc } from '../lib/docsFirst'
3
+ // The docs address itself (`/docs`) has no page of its own: it leads to the first docs page of the language.
4
+ await toFirstDoc()
5
+ </script>
6
+
7
+ <template>
8
+ <div />
9
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts">
2
+ import { toFirstDoc } from '../lib/docsFirst'
3
+ // The start page: the site's `home` brick (docsHome) when it has one, otherwise the first docs page.
4
+ import schema from '#ginko/schema'
5
+
6
+ const hasHome = Object.hasOwn(schema.bricks, 'home')
7
+ if (!hasHome) await toFirstDoc()
8
+ </script>
9
+
10
+ <template>
11
+ <DocsLanding v-if="hasHome" />
12
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ const blog = useDocsBlog()
3
+ const post = blog.post(String(useRoute().params.slug))
4
+ </script>
5
+
6
+ <template>
7
+ <DocsPost :post="post" />
8
+ </template>
package/app/error.vue CHANGED
@@ -1,121 +1,29 @@
1
1
  <script setup lang="ts">
2
- import { localizedRoutes } from "../i18n/routes";
3
- import { messages } from "../i18n/messages/index";
4
- import { normalizeAppError } from "./lib/errors";
5
- import { computed } from "vue";
6
- import { clearError, useHead } from "#imports";
7
- import { localeFromPath, localizedPath } from "../i18n/locales";
8
-
9
- type NuxtErrorLike = {
10
- message?: string;
11
- status?: number;
12
- statusCode?: number;
13
- statusMessage?: string;
14
- statusText?: string;
15
- url?: string;
16
- };
17
-
18
- const props = defineProps<{
19
- error: NuxtErrorLike;
20
- }>();
21
-
22
- const normalized = computed(() => normalizeAppError(props.error));
23
- const requestPath = computed(
24
- () => props.error?.url || (import.meta.client ? window.location.pathname : "/"),
25
- );
26
- const locale = computed(() => localeFromPath(requestPath.value));
27
- const copy = computed(() => {
28
- const catalog = messages[locale.value].errors;
29
- return catalog[normalized.value.kind] ?? catalog.unavailable;
30
- });
31
- const homePath = computed(() => localizedPath(locale.value, localizedRoutes[locale.value].home));
32
- const docsPath = computed(() => localizedPath(locale.value, localizedRoutes[locale.value].docs));
33
- const title = computed(() => `${normalized.value.statusCode} - ${copy.value.title}`);
34
- // Keep the full site chrome for content-level errors (404 and friends); hard
35
- // server failures fall back to the bare page so the error screen cannot crash.
36
- const showChrome = computed(() => normalized.value.kind !== "server");
37
-
38
- useHead(() => ({
39
- title: title.value,
40
- meta: [{ name: "robots", content: "noindex, nofollow" }],
41
- }));
42
-
43
- const handleError = () => clearError({ redirect: homePath.value });
2
+ // Not found and other errors, inside the site's frame (the layer's layout, or the site's own). A site with its own
3
+ // error.vue replaces this one.
4
+ import type { NuxtError } from '#app'
5
+
6
+ const props = defineProps<{ error: NuxtError }>()
7
+ const { t, localePath } = useI18n()
8
+ const docs = useDocs()
9
+ const notFound = computed(() => props.error.statusCode === 404)
10
+ useGinkoSeo(() => ({ title: notFound.value ? t('docs.notFound.title') : t('docs.error.title'), noindex: true }))
11
+ // Links work without JavaScript; with it, clearError leaves the error state and navigates.
12
+ const leave = (to: string) => clearError({ redirect: to })
44
13
  </script>
45
14
 
46
15
  <template>
47
- <div class="flex min-h-dvh flex-col bg-background text-foreground">
48
- <template v-if="showChrome">
49
- <SiteSkipLink />
50
- <SiteHeader />
51
- </template>
52
- <main id="main-content" class="flex flex-1 items-center justify-center px-4 py-24">
53
- <section class="w-full max-w-xl text-center">
54
- <p
55
- class="mb-6 text-8xl leading-none font-bold text-primary/20 select-none"
56
- aria-hidden="true"
57
- >
58
- {{ normalized.statusCode }}
59
- </p>
60
-
61
- <h1 class="mb-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
62
- {{ copy.title }}
63
- </h1>
64
- <p class="mx-auto mb-8 max-w-md leading-7 text-muted-foreground">
65
- {{ copy.description }}
66
- </p>
67
-
68
- <div class="flex flex-col items-center justify-center gap-3 sm:flex-row">
69
- <button class="error-action error-action-primary" type="button" @click="handleError">
70
- {{ messages[locale].errors.actions.home }}
71
- </button>
72
- <a class="error-action error-action-secondary" :href="docsPath">
73
- {{ messages[locale].errors.actions.docs }}
74
- </a>
16
+ <NuxtLayout>
17
+ <div class="docs-error">
18
+ <div>
19
+ <p class="docs-error-code">{{ error.statusCode }}</p>
20
+ <h1>{{ notFound ? t('docs.notFound.title') : t('docs.error.title') }}</h1>
21
+ <p>{{ notFound ? t('docs.notFound.text') : t('docs.error.text') }}</p>
22
+ <div class="docs-error-actions">
23
+ <a :href="localePath('/')" class="docs-button is-large is-primary" @click.prevent="leave(localePath('/'))">{{ t('docs.notFound.home') }}</a>
24
+ <a v-if="docs.first.value" :href="docs.first.value" class="docs-button is-large is-soft" @click.prevent="leave(docs.first.value)">{{ t('docs.docs') }}</a>
75
25
  </div>
76
- </section>
77
- </main>
78
- <SiteFooter v-if="showChrome" />
79
- </div>
26
+ </div>
27
+ </div>
28
+ </NuxtLayout>
80
29
  </template>
81
-
82
- <style scoped>
83
- .error-action {
84
- align-items: center;
85
- border-radius: 0.375rem;
86
- display: inline-flex;
87
- font-size: 0.875rem;
88
- font-weight: 500;
89
- height: 2.5rem;
90
- justify-content: center;
91
- padding-inline: 1rem;
92
- transition:
93
- background-color 150ms,
94
- color 150ms,
95
- border-color 150ms;
96
- }
97
-
98
- .error-action:focus-visible {
99
- outline: 2px solid var(--ring);
100
- outline-offset: 2px;
101
- }
102
-
103
- .error-action-primary {
104
- background: var(--primary);
105
- color: var(--primary-foreground);
106
- }
107
-
108
- .error-action-primary:hover {
109
- opacity: 0.9;
110
- }
111
-
112
- .error-action-secondary {
113
- background: var(--background);
114
- border: 1px solid var(--input);
115
- }
116
-
117
- .error-action-secondary:hover {
118
- background: var(--accent);
119
- color: var(--accent-foreground);
120
- }
121
- </style>
@@ -1,12 +1,30 @@
1
+ <script setup lang="ts">
2
+ // The standalone frame: banner, header, page, footer. A site with its own `layouts/default.vue` replaces it, and
3
+ // the docs pages then sit inside the site's frame (the docs body, DocsShell, works either way).
4
+ const { t } = useI18n()
5
+ const { theme, banner } = useAppConfig().docs ?? {}
6
+ useHead({
7
+ // `docs-standalone` turns on the page background, font and color-scheme (docs.css); embedded, the site owns those.
8
+ htmlAttrs: { class: 'docs-standalone', 'data-docs-theme': theme && theme !== 'default' ? theme : undefined },
9
+ })
10
+ // "Skip to content" goes past the docs navigation to the page itself when the page has one (DocsShell), else to main.
11
+ function skip(event: MouseEvent) {
12
+ const target = document.getElementById('docs-content') ?? document.getElementById('docs-main')
13
+ if (!target) return
14
+ event.preventDefault()
15
+ target.focus()
16
+ target.scrollIntoView()
17
+ }
18
+ </script>
19
+
1
20
  <template>
2
- <div class="flex min-h-dvh flex-col bg-background text-foreground">
3
- <SiteSkipLink />
4
- <SiteBanner landing />
5
- <SiteHeader />
6
- <main id="main-content" class="flex-1">
21
+ <div class="docs-frame">
22
+ <a class="docs-skip" href="#docs-main" @click="skip">{{ t('docs.skip') }}</a>
23
+ <DocsBanner v-if="banner" :id="banner.id" :href="banner.href" />
24
+ <DocsHeader />
25
+ <main id="docs-main" class="docs-frame-main" tabindex="-1">
7
26
  <slot />
8
27
  </main>
9
- <SiteFooter />
10
- <SiteInteractionLayer />
28
+ <DocsFooter />
11
29
  </div>
12
30
  </template>
@@ -0,0 +1,8 @@
1
+ // Types of the blog and changelog entries. Kept out of `composables/` so that they are not auto-imported into the host app.
2
+ import type { ItemOf } from '@lupinum/ginko'
3
+ import type { blogCollections, changelogCollection } from '@lupinum/ginko-docs/config'
4
+
5
+ type Blog = ReturnType<typeof blogCollections>
6
+ export type Post = ItemOf<Blog['posts']['fields']> & { url?: string }
7
+ export type Author = ItemOf<Blog['authors']['fields']>
8
+ export type ChangelogEntry = ItemOf<ReturnType<typeof changelogCollection>['fields']>