@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
@@ -8,14 +8,14 @@ import {
8
8
  SheetTitle,
9
9
  SheetTrigger,
10
10
  } from "#ginko-docs/components/ui/sheet";
11
- import { cn } from "#ginko-docs/lib/utils";
12
- import type { TocItem } from "./DocsToc.vue";
11
+ import { cn } from "#ginko-docs/utils";
12
+ import type { FlatTocItem } from "#ginko-docs/utils/content";
13
13
  import DocsToc from "./DocsToc.vue";
14
14
  import { useI18n, useRoute } from "#imports";
15
15
 
16
16
  const props = defineProps<{
17
- items: TocItem[];
18
- activeId?: string;
17
+ items: FlatTocItem[];
18
+ activeIds?: string[];
19
19
  pageTitle?: string;
20
20
  class?: HTMLAttributes["class"];
21
21
  }>();
@@ -53,7 +53,7 @@ watch(
53
53
  <SheetTrigger as-child>
54
54
  <button
55
55
  type="button"
56
- class="-ml-1 flex h-8 min-w-0 items-center gap-2 rounded-lg px-2 text-sm font-medium text-foreground transition-colors hover:bg-muted"
56
+ class="-ml-1 flex h-8 min-w-0 items-center gap-2 rounded-lg px-2 text-sm font-medium text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
57
57
  :aria-label="t('docs.pageNavigation')"
58
58
  >
59
59
  <Icon
@@ -65,7 +65,10 @@ watch(
65
65
  </button>
66
66
  </SheetTrigger>
67
67
 
68
- <SheetContent side="left" class="flex w-[20rem] max-w-[86vw] flex-col gap-0 p-0">
68
+ <SheetContent
69
+ side="left"
70
+ class="flex w-[var(--docs-sidebar-width)] max-w-[85vw] flex-col gap-0 p-0"
71
+ >
69
72
  <div class="border-b border-border px-4 py-3">
70
73
  <SheetTitle class="text-sm font-semibold">{{ t("docs.label") }}</SheetTitle>
71
74
  <SheetDescription class="sr-only">{{ t("docs.pageNavigation") }}</SheetDescription>
@@ -79,7 +82,8 @@ watch(
79
82
  <Sheet v-if="items.length" v-model:open="tocOpen">
80
83
  <SheetTrigger as-child>
81
84
  <button
82
- class="-mr-1 flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
85
+ type="button"
86
+ class="-mr-1 flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
83
87
  >
84
88
  <Icon name="lucide:list" class="size-3.5" aria-hidden="true" />
85
89
  <span>{{ t("docs.toc") }}</span>
@@ -96,7 +100,7 @@ watch(
96
100
  </div>
97
101
 
98
102
  <div class="flex-1 overflow-y-auto px-6 py-5" @click="tocOpen = false">
99
- <DocsToc :items="items" :active-id="activeId" :show-title="false" />
103
+ <DocsToc :items="items" :active-ids="activeIds" :show-title="false" />
100
104
  </div>
101
105
  </SheetContent>
102
106
  </Sheet>
@@ -1,23 +1,33 @@
1
1
  <script setup lang="ts">
2
- import { createContentNotFoundError } from "#ginko-docs/lib/errors";
3
- import { filterTocByDepth, flattenTocLinks } from "#ginko-docs/utils/content";
2
+ import { filterTocByDepth, flattenTocLinks, getMarkdownTocLinks } from "#ginko-docs/utils/content";
4
3
  import ContentFeedback from "#ginko-docs/components/content/Feedback.vue";
4
+ import DocsBreadcrumb from "./DocsBreadcrumb.vue";
5
5
  import DocsMobileToc from "./DocsMobileToc.vue";
6
6
  import DocsPageNav from "./DocsPageNav.vue";
7
7
  import DocsToc from "./DocsToc.vue";
8
8
  import DocsContributeLinks from "./DocsContributeLinks.vue";
9
- import { computed, watch } from "vue";
10
- import { useAppConfig, useContentPage, useHead, useI18n, useRoute, useSeoMeta } from "#imports";
9
+ import { computed, provide, ref, watch } from "vue";
10
+ import { docsTocKey } from "#ginko-docs/features/docs/toc-context";
11
+ import {
12
+ createError,
13
+ useAppConfig,
14
+ useContentPage,
15
+ useHead,
16
+ useI18n,
17
+ useRoute,
18
+ useSeoMeta,
19
+ } from "#imports";
11
20
  import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
12
21
  import { useGinkoOgImage } from "#ginko-docs/composables/useGinkoOgImage";
13
22
  import { useScrollspy } from "#ginko-docs/features/docs/composables/useScrollspy";
23
+ import { useRevealActive } from "#ginko-docs/features/docs/composables/useRevealActive";
14
24
  import PageMarkdownCopy from "#ginko-docs/components/content/PageMarkdownCopy.vue";
15
25
  import { Separator } from "#ginko-docs/components/ui/separator";
16
26
  import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
17
27
  import { useSchemaJsonLd } from "#ginko-docs/composables/useSchemaJsonLd";
18
28
  import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
19
29
  import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
20
- import { syncContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
30
+ import { useContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
21
31
 
22
32
  type DocsNavLink = {
23
33
  path: string;
@@ -34,25 +44,26 @@ const { locale, t } = useI18n();
34
44
  const config = useAppConfig().ginkoDocs;
35
45
  const route = useRoute();
36
46
  const localizedPath = useLocalizedPath();
37
- const {
38
- page,
39
- previous,
40
- next: nextContent,
41
- } = await useContentPage("docs", {
42
- locale: () => locale.value,
47
+ const canonicalUrl = useCanonicalUrl();
48
+ const { sync: syncContentRouteAlternates } = useContentRouteAlternates();
49
+ const contentPageResult = useContentPage("docs", {
43
50
  fallback: true,
44
51
  surround: true,
45
52
  });
46
- if (!page.value) throw createContentNotFoundError();
53
+ const docsNavigationResult = useDocsNavigation();
54
+ const [{ page, previous, next: nextContent, error }, { trail }] = await Promise.all([
55
+ contentPageResult,
56
+ docsNavigationResult,
57
+ ]);
58
+ if (error.value) throw error.value;
59
+ if (!page.value) throw createError({ statusCode: 404, statusMessage: "Not Found" });
47
60
  syncContentRouteAlternates(page);
48
- const { trail } = await useDocsNavigation();
49
61
 
50
62
  const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
51
63
  const pageTitle = computed(() => page.value?.title ?? t("docs.fallbackTitle"));
52
64
  const pageDescription = computed(() => page.value?.description ?? t("docs.fallbackDescription"));
53
- const canonicalUrl = useCanonicalUrl();
54
65
  const tocItems = computed(() =>
55
- filterTocByDepth(flattenTocLinks(page.value?.body?.toc?.links), config.toc?.depth ?? 3),
66
+ filterTocByDepth(flattenTocLinks(getMarkdownTocLinks(page.value?.body)), config.toc?.depth ?? 3),
56
67
  );
57
68
  const prev = computed(() => toDocsNavLink(previous.value));
58
69
  const next = computed(() => toDocsNavLink(nextContent.value));
@@ -63,12 +74,24 @@ const schemaBreadcrumbs = computed(() => [
63
74
  .filter((item) => item.path)
64
75
  .map((item) => ({ name: item.title, path: item.path! })),
65
76
  ]);
66
- const { activeId, refresh } = useScrollspy(computed(() => tocItems.value.map((item) => item.id)));
77
+ provide(docsTocKey, tocItems);
78
+ const { activeIds, refresh } = useScrollspy(computed(() => tocItems.value.map((item) => item.id)));
67
79
 
68
80
  watch(tocItems, () => {
69
81
  void refresh();
70
82
  });
71
83
 
84
+ // Keep the active TOC region visible when a long TOC overflows the sticky
85
+ // aside.
86
+ const tocAside = ref<HTMLElement | null>(null);
87
+ const { reveal: revealActiveTocLink } = useRevealActive(tocAside, "[data-toc-active]", {
88
+ topPad: 16,
89
+ bottomPad: 44,
90
+ });
91
+ watch(activeIds, () => {
92
+ requestAnimationFrame(revealActiveTocLink);
93
+ });
94
+
72
95
  useSeoMeta({
73
96
  title: computed(() => `${pageTitle.value} - ${siteName.value}`),
74
97
  description: pageDescription,
@@ -113,6 +136,11 @@ useHead(() => ({
113
136
  link: [{ key: "canonical", rel: "canonical", href: canonicalUrl.value }],
114
137
  meta: [{ property: "og:type", content: "article" }],
115
138
  }));
139
+
140
+ function scrollToTop() {
141
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
142
+ window.scrollTo({ top: 0, behavior: reducedMotion ? "auto" : "smooth" });
143
+ }
116
144
  </script>
117
145
 
118
146
  <template>
@@ -121,45 +149,27 @@ useHead(() => ({
121
149
  <DocsMobileToc
122
150
  class="order-1"
123
151
  :items="tocItems"
124
- :active-id="activeId"
152
+ :active-ids="activeIds"
125
153
  :page-title="pageTitle"
126
154
  />
127
155
 
128
156
  <main
129
157
  id="main-content"
130
- class="order-2 mx-auto w-full max-w-3xl flex-1 px-4 py-8 md:px-6 xl:py-14"
158
+ class="order-2 mx-auto w-full max-w-[72ch] flex-1 px-4 py-8 md:px-6 xl:py-14"
131
159
  >
132
160
  <article v-if="page">
133
161
  <div class="flex items-start justify-between gap-4 sm:gap-6">
134
162
  <div class="min-w-0">
135
- <nav
136
- v-if="visibleTrail.length"
137
- class="mb-2 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
138
- :aria-label="t('docs.breadcrumbs')"
139
- >
140
- <template v-for="(item, index) in visibleTrail" :key="item.id">
141
- <Icon
142
- v-if="index > 0"
143
- name="lucide:chevron-right"
144
- class="size-3.5 text-border"
145
- aria-hidden="true"
146
- />
147
- <NuxtLink
148
- v-if="item.path"
149
- :to="item.path"
150
- class="rounded-sm transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
151
- >
152
- {{ item.title }}
153
- </NuxtLink>
154
- <span v-else>{{ item.title }}</span>
155
- </template>
156
- </nav>
163
+ <DocsBreadcrumb class="mb-2" :items="visibleTrail" />
157
164
  <h1
158
- class="font-heading text-[2rem] leading-[1.15] font-semibold tracking-[-0.025em] text-balance text-foreground sm:text-[2.25rem]"
165
+ class="font-heading text-[27px] leading-[1.15] font-semibold tracking-[-0.02em] text-balance text-foreground"
159
166
  >
160
167
  {{ page.title }}
161
168
  </h1>
162
- <p v-if="page.description" class="mt-2 text-lg leading-7 text-muted-foreground">
169
+ <p
170
+ v-if="page.description"
171
+ class="mt-2 text-base leading-[1.55] text-muted-foreground"
172
+ >
163
173
  {{ page.description }}
164
174
  </p>
165
175
  </div>
@@ -171,18 +181,45 @@ useHead(() => ({
171
181
  <ContentRenderer :value="page" />
172
182
  </div>
173
183
 
174
- <DocsContributeLinks :stem="page.stem" :extension="page.extension" :title="pageTitle" />
175
- <DocsPageNav :prev="prev" :next="next" />
176
- <ContentFeedback :label="t('feedback.label')" />
184
+ <footer class="mt-12 border-t border-border pt-6">
185
+ <div class="flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
186
+ <ContentFeedback :label="t('feedback.label')" />
187
+ <DocsContributeLinks
188
+ class="xl:hidden"
189
+ :stem="page.stem"
190
+ :extension="page.extension"
191
+ :title="pageTitle"
192
+ />
193
+ </div>
194
+ <DocsPageNav class="mt-8" :prev="prev" :next="next" />
195
+ </footer>
177
196
  </article>
178
197
  </main>
179
198
  </div>
180
199
 
181
200
  <aside
182
- class="sticky top-[var(--site-header-height)] hidden h-[calc(100vh-var(--site-header-height))] w-[var(--docs-toc-width)] shrink-0 flex-col overflow-y-auto pt-10 pr-4 pb-6 xl:flex"
201
+ ref="tocAside"
202
+ class="sticky top-[var(--site-header-height)] hidden h-[calc(100dvh-var(--site-header-height)-var(--site-banner-height,0px))] w-[var(--docs-toc-width)] shrink-0 flex-col overflow-y-auto pt-10 pr-4 pb-6 xl:flex"
183
203
  :aria-label="t('docs.toc')"
184
204
  >
185
- <DocsToc :items="tocItems" :active-id="activeId" />
205
+ <DocsToc class="mb-6" :items="tocItems" :active-ids="activeIds" />
206
+
207
+ <div class="mt-auto flex flex-col gap-2.5 border-t border-border/70 pt-5">
208
+ <DocsContributeLinks
209
+ variant="rail"
210
+ :stem="page?.stem"
211
+ :extension="page?.extension"
212
+ :title="pageTitle"
213
+ />
214
+ <button
215
+ type="button"
216
+ class="inline-flex items-center gap-1.5 self-start text-sm font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
217
+ @click="scrollToTop"
218
+ >
219
+ <Icon name="lucide:arrow-up" class="size-4" aria-hidden="true" />
220
+ {{ t("docs.backToTop") }}
221
+ </button>
222
+ </div>
186
223
  </aside>
187
224
  </div>
188
225
  </template>
@@ -11,7 +11,7 @@ const { t } = useI18n();
11
11
  <template>
12
12
  <nav
13
13
  v-if="prev || next"
14
- class="mt-12 grid grid-cols-1 gap-3 border-t border-border pt-8 sm:grid-cols-2"
14
+ class="grid grid-cols-1 gap-3 sm:grid-cols-2"
15
15
  :aria-label="t('docs.pageNavigation')"
16
16
  >
17
17
  <!-- Previous -->
@@ -1,17 +1,19 @@
1
1
  <script setup lang="ts">
2
- import { cn } from "#ginko-docs/lib/utils";
2
+ import { cn } from "#ginko-docs/utils";
3
3
  import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
4
- import { findFirstNavigationPage } from "@lupinum/ginko-content/client";
5
4
  import {
6
5
  docsNavigationSectionContainsPath,
7
6
  getDocsNavigationGroups,
7
+ resolveDocsSectionTargetPath,
8
8
  type DocsNavigationSection,
9
9
  } from "#ginko-docs/features/docs/docs-navigation";
10
+ import { ScrollArea } from "#ginko-docs/components/ui/scroll-area";
11
+ import { useRevealActive } from "#ginko-docs/features/docs/composables/useRevealActive";
10
12
  import DocsSidebarItem from "./DocsSidebarItem.vue";
11
13
  import DocsSidebarDropdown from "./DocsSidebarDropdown.vue";
12
14
  import DocsSidebarList from "./DocsSidebarList.vue";
13
15
  import DocsSidebarTabs from "./DocsSidebarTabs.vue";
14
- import { computed, ref, watch } from "vue";
16
+ import { computed, nextTick, onMounted, ref, watch } from "vue";
15
17
  import { navigateTo, useAppConfig, useI18n, useRoute } from "#imports";
16
18
 
17
19
  const props = withDefaults(
@@ -54,16 +56,32 @@ watch(
54
56
  );
55
57
 
56
58
  function setActiveSection(id: string) {
59
+ // Re-selecting the active section must not navigate away from the current page.
60
+ if (id === activeSectionId.value) return;
57
61
  selectedSectionId.value = id;
58
62
  const section = sections.value.find((entry) => entry.id === id);
59
- // Structural sections have no page of their own — land on their first page
60
- // instead of only swapping the sidebar contents.
61
- const path = section?.path ?? findFirstNavigationPage(section?.items)?.path;
63
+ const path = section ? resolveDocsSectionTargetPath(section) : undefined;
62
64
  if (path) void navigateTo(path);
63
65
  }
64
66
 
65
67
  const scrollViewportClass =
66
- "min-h-0 flex-1 overflow-y-auto px-4 pb-4 overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]";
68
+ "size-full rounded-[inherit] p-4 pt-2 overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]";
69
+
70
+ // Deep links can land on an item far outside the visible band — center it
71
+ // once on mount and on section switches.
72
+ const scrollArea = ref<{ $el?: HTMLElement } | null>(null);
73
+
74
+ const { reveal: revealActiveItem } = useRevealActive(
75
+ () => scrollArea.value?.$el?.querySelector<HTMLElement>("[data-slot='scroll-area-viewport']"),
76
+ "[data-active='true']",
77
+ );
78
+
79
+ onMounted(() => {
80
+ void nextTick(revealActiveItem);
81
+ });
82
+ watch(activeSectionId, () => {
83
+ void nextTick(revealActiveItem);
84
+ });
67
85
 
68
86
  const asideClass = computed(() =>
69
87
  cn(
@@ -76,13 +94,7 @@ const asideClass = computed(() =>
76
94
  </script>
77
95
 
78
96
  <template>
79
- <aside
80
- data-hovered="false"
81
- :data-variant="variant"
82
- :id="variant === 'desktop' ? 'nd-sidebar' : undefined"
83
- :aria-label="t('docs.label')"
84
- :class="asideClass"
85
- >
97
+ <aside :data-variant="variant" :aria-label="t('docs.label')" :class="asideClass">
86
98
  <div v-if="switcherSections.length > 1" class="flex flex-col gap-3 p-4 pb-2">
87
99
  <DocsSidebarTabs
88
100
  v-if="sidebarSwitcher === 'tabs'"
@@ -104,15 +116,13 @@ const asideClass = computed(() =>
104
116
  />
105
117
  </div>
106
118
 
107
- <div
108
- :class="[scrollViewportClass, switcherSections.length > 1 ? 'pt-2' : 'pt-4']"
109
- :data-switcher="switcherSections.length > 1"
110
- >
119
+ <ScrollArea ref="scrollArea" class="min-h-0 flex-1" :viewport-class="scrollViewportClass">
111
120
  <div class="flex min-w-full flex-col gap-0.5">
112
121
  <template v-for="group in groups" :key="group.id">
113
122
  <p
114
123
  v-if="group.title"
115
- class="mt-6 mb-1 inline-flex items-center gap-2 px-2 text-sm font-semibold text-foreground first:mt-0"
124
+ data-slot="docs-sidebar-group-title"
125
+ class="mt-5 mb-1.5 inline-flex items-center gap-2 px-2 text-[11px] font-semibold tracking-wider text-muted-foreground uppercase first:mt-0"
116
126
  >
117
127
  {{ group.title }}
118
128
  </p>
@@ -124,6 +134,6 @@ const asideClass = computed(() =>
124
134
  />
125
135
  </template>
126
136
  </div>
127
- </div>
137
+ </ScrollArea>
128
138
  </aside>
129
139
  </template>
@@ -2,12 +2,18 @@
2
2
  import { computed, ref, watch } from "vue";
3
3
  import { useRoute } from "vue-router";
4
4
  import {
5
- docsNavigationItemContainsPath,
6
- normalizeDocsNavigationPath,
7
- type DocsNavigationItem,
8
- } from "#ginko-docs/features/docs/docs-navigation";
9
- import { Badge } from "#ginko-docs/components/ui/badge";
10
- import { cn } from "#ginko-docs/lib/utils";
5
+ navigationItemContainsPath,
6
+ normalizeNavigationPath,
7
+ } from "@lupinum/ginko-content/navigation";
8
+ import type { DocsNavigationItem } from "#ginko-docs/features/docs/docs-navigation";
9
+ import { Button } from "#ginko-docs/components/ui/button";
10
+ import DocsSidebarRow from "./DocsSidebarRow.vue";
11
+ import {
12
+ Collapsible,
13
+ CollapsibleContent,
14
+ CollapsibleTrigger,
15
+ } from "#ginko-docs/components/ui/collapsible";
16
+ import { cn } from "#ginko-docs/utils";
11
17
 
12
18
  defineOptions({
13
19
  name: "DocsSidebarItem",
@@ -21,44 +27,36 @@ const props = defineProps<{
21
27
  const route = useRoute();
22
28
 
23
29
  function itemIsExactActive(path: string, item: DocsNavigationItem): boolean {
24
- return Boolean(
25
- item.path && normalizeDocsNavigationPath(path) === normalizeDocsNavigationPath(item.path),
26
- );
30
+ return Boolean(item.path && normalizeNavigationPath(path) === normalizeNavigationPath(item.path));
27
31
  }
28
32
 
29
33
  function folderShouldBeOpen(path: string, item: DocsNavigationItem): boolean {
30
- return item.children.length > 0 && docsNavigationItemContainsPath(item, path);
34
+ return item.children.length > 0 && navigationItemContainsPath(item, path);
31
35
  }
32
36
 
33
37
  const isExactActive = computed(() => itemIsExactActive(route.path, props.item));
34
38
 
35
39
  const paddingClass = computed(() => {
36
- const steps = ["ps-2", "ps-5", "ps-8", "ps-11"] as const;
37
- return steps[Math.min(props.depth, steps.length - 1)]!;
40
+ const steps = ["ps-[13px]", "ps-[25px]", "ps-[37px]"] as const;
41
+ return steps[Math.min(Math.max(props.depth - 1, 0), steps.length - 1)]!;
38
42
  });
39
43
 
44
+ // Depth 0: compact pill rows. Depth > 0: each row carries the rail as its own
45
+ // start border so the active segment recolors it — aligned by construction.
40
46
  const linkRowClass = computed(() =>
41
47
  cn(
42
- "group relative flex min-h-10 w-full flex-row items-center gap-2 rounded-lg px-2 py-2 text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
48
+ "group relative flex w-full flex-row items-center gap-2 text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
43
49
  "[&_svg]:size-4 [&_svg]:shrink-0",
44
50
  "hover:bg-accent/50 hover:text-accent-foreground/80 hover:transition-none",
45
- "data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-foreground data-[active=true]:hover:transition-colors",
46
- props.depth > 0
47
- ? "data-[active=true]:before:absolute data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5 data-[active=true]:before:w-px data-[active=true]:before:bg-primary data-[active=true]:before:content-['']"
48
- : undefined,
49
- paddingClass.value,
50
- ),
51
- );
52
-
53
- const splitFolderRowClass = computed(() =>
54
- cn(
55
- "group relative flex min-h-10 w-full flex-row items-center rounded-lg text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none",
56
- "hover:bg-accent/50 hover:text-accent-foreground/80 hover:transition-none",
57
- "data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-foreground data-[active=true]:hover:transition-colors",
58
- props.depth > 0
59
- ? "data-[active=true]:before:absolute data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5 data-[active=true]:before:w-px data-[active=true]:before:bg-primary data-[active=true]:before:content-['']"
60
- : undefined,
61
- paddingClass.value,
51
+ "data-[active=true]:bg-primary/10 data-[active=true]:text-primary data-[active=true]:font-medium data-[active=true]:hover:transition-colors",
52
+ props.depth === 0
53
+ ? "rounded-[7px] px-2 py-[5.5px] text-[13px]"
54
+ : cn(
55
+ "rounded-e-[7px] border-s border-border py-[5px] pe-2 text-[12.5px]",
56
+ "hover:border-s-muted-foreground/40",
57
+ "data-[active=true]:border-s-primary",
58
+ paddingClass.value,
59
+ ),
62
60
  ),
63
61
  );
64
62
 
@@ -73,82 +71,45 @@ watch(
73
71
  },
74
72
  );
75
73
 
76
- const contentRailClass =
77
- "relative flex flex-col gap-0.5 *:first:mt-0.5 before:absolute before:start-2.5 before:inset-y-1 before:w-px before:bg-border before:content-['']";
74
+ // No gap between child rows: their start borders stack into one continuous
75
+ // rail. Deeper levels indent via padding only, sharing the same rail.
76
+ const contentRailClass = computed(() =>
77
+ props.depth === 0 ? "ms-[15.5px] mt-0.5 mb-1 flex flex-col" : "flex flex-col",
78
+ );
78
79
  </script>
79
80
 
80
81
  <template>
81
- <div v-if="item.children.length > 0" :data-state="folderOpen ? 'open' : 'closed'">
82
- <div
83
- v-if="item.path"
84
- :class="splitFolderRowClass"
85
- :data-active="isExactActive ? 'true' : 'false'"
86
- >
82
+ <Collapsible v-if="item.children.length > 0" v-model:open="folderOpen" :unmount-on-hide="false">
83
+ <CollapsibleTrigger v-if="item.path" as-child>
87
84
  <NuxtLink
88
85
  :to="item.path"
86
+ :class="linkRowClass"
89
87
  :aria-current="isExactActive ? 'page' : undefined"
90
- class="flex min-w-0 flex-1 items-center gap-2 rounded-s-lg py-2 pe-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
91
- data-slot="docs-sidebar-folder-link"
88
+ :data-active="isExactActive ? 'true' : 'false'"
92
89
  >
93
- <Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
94
- <span class="min-w-0 truncate">{{ item.title }}</span>
95
- <Badge
96
- v-if="item.badge"
97
- variant="secondary"
98
- class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
99
- >
100
- {{ item.badge }}
101
- </Badge>
90
+ <DocsSidebarRow :item="item" show-chevron />
102
91
  </NuxtLink>
103
- <button
104
- class="group flex size-10 shrink-0 items-center justify-center rounded-e-lg transition-colors outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
105
- data-slot="docs-sidebar-folder-toggle"
106
- type="button"
107
- :aria-expanded="folderOpen"
108
- :data-state="folderOpen ? 'open' : 'closed'"
109
- @click="folderOpen = !folderOpen"
110
- >
111
- <Icon
112
- name="lucide:chevron-down"
113
- class="size-4 shrink-0 -rotate-90 transition-transform duration-200 group-data-[state=open]:rotate-0 rtl:rotate-90"
114
- aria-hidden="true"
115
- />
116
- <span class="sr-only">{{ item.title }}</span>
117
- </button>
118
- </div>
119
- <button
120
- v-else
121
- :class="linkRowClass"
122
- :data-active="isExactActive ? 'true' : 'false'"
123
- :data-state="folderOpen ? 'open' : 'closed'"
124
- :aria-expanded="folderOpen"
125
- type="button"
126
- @click="folderOpen = !folderOpen"
127
- >
128
- <Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
129
- <span class="min-w-0 truncate">{{ item.title }}</span>
130
- <Badge
131
- v-if="item.badge"
132
- variant="secondary"
133
- class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
92
+ </CollapsibleTrigger>
93
+ <CollapsibleTrigger v-else as-child>
94
+ <Button
95
+ variant="ghost"
96
+ :class="linkRowClass"
97
+ class="h-auto justify-start"
98
+ :data-active="isExactActive ? 'true' : 'false'"
134
99
  >
135
- {{ item.badge }}
136
- </Badge>
137
- <Icon
138
- name="lucide:chevron-down"
139
- class="ms-auto size-4 shrink-0 -rotate-90 transition-transform duration-200 group-data-[state=open]:rotate-0 rtl:rotate-90"
140
- aria-hidden="true"
141
- />
142
- </button>
143
- <div v-show="folderOpen" :class="contentRailClass">
100
+ <DocsSidebarRow :item="item" show-chevron />
101
+ </Button>
102
+ </CollapsibleTrigger>
103
+
104
+ <CollapsibleContent :class="contentRailClass">
144
105
  <DocsSidebarItem
145
106
  v-for="child in item.children"
146
107
  :key="child.path ?? child.title"
147
108
  :item="child"
148
109
  :depth="depth + 1"
149
110
  />
150
- </div>
151
- </div>
111
+ </CollapsibleContent>
112
+ </Collapsible>
152
113
  <NuxtLink
153
114
  v-else-if="item.path"
154
115
  :to="item.path"
@@ -156,14 +117,6 @@ const contentRailClass =
156
117
  :class="linkRowClass"
157
118
  :data-active="isExactActive ? 'true' : 'false'"
158
119
  >
159
- <Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
160
- <span class="min-w-0 truncate">{{ item.title }}</span>
161
- <Badge
162
- v-if="item.badge"
163
- variant="secondary"
164
- class="ms-auto h-4 shrink-0 rounded-sm px-1.5 py-0 text-[10px]"
165
- >
166
- {{ item.badge }}
167
- </Badge>
120
+ <DocsSidebarRow :item="item" />
168
121
  </NuxtLink>
169
122
  </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { DocsNavigationItem } from "#ginko-docs/features/docs/docs-navigation";
3
+ import { Badge } from "#ginko-docs/components/ui/badge";
4
+
5
+ defineProps<{
6
+ item: DocsNavigationItem;
7
+ showChevron?: boolean;
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" aria-hidden="true" />
13
+ <span class="min-w-0 truncate">{{ item.title }}</span>
14
+ <Badge
15
+ v-if="item.badge"
16
+ variant="secondary"
17
+ class="ms-auto h-4 shrink-0 rounded-full px-1.5 py-0 text-[10px]"
18
+ >
19
+ {{ item.badge }}
20
+ </Badge>
21
+ <Icon
22
+ v-if="showChevron"
23
+ name="lucide:chevron-down"
24
+ class="ms-auto size-3.5 shrink-0 -rotate-90 text-muted-foreground/70 transition-transform duration-200 ease-out group-hover:text-muted-foreground group-data-[state=open]:rotate-0 group-data-[active=true]:text-primary/70 rtl:rotate-90"
25
+ aria-hidden="true"
26
+ />
27
+ </template>