@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
@@ -3,12 +3,12 @@ import { useI18n, useRouter } from "#imports";
3
3
  import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
4
4
  import { computed } from "vue";
5
5
  import { useGinkoDocsConfig } from "./useGinkoDocsConfig";
6
- import { resolveBanner, resolveMainNav, type NavItem } from "./site-navigation.utils";
7
-
8
- export interface SidebarSection {
9
- title: string;
10
- items: { label: string; href: string; badge?: string }[];
11
- }
6
+ import {
7
+ resolveBanner,
8
+ resolveMainNav,
9
+ resolveSocialLinks,
10
+ type NavItem,
11
+ } from "./site-navigation.utils";
12
12
 
13
13
  export const useSiteNavigation = () => {
14
14
  const config = useGinkoDocsConfig();
@@ -32,34 +32,12 @@ export const useSiteNavigation = () => {
32
32
  const banner = computed(() =>
33
33
  resolveBanner(config.banner, {
34
34
  locale: locale.value,
35
- blogPath: blogPath.value,
36
- blogExists: blogExists.value,
37
35
  defaultText: t("banner.text"),
38
36
  defaultLinkLabel: t("banner.linkLabel"),
39
37
  }),
40
38
  );
41
39
 
42
- const socialLinks = computed<NavItem[]>(
43
- () =>
44
- [
45
- config.social.github
46
- ? {
47
- label: t("nav.github"),
48
- href: config.social.github,
49
- external: true,
50
- icon: "lucide:github",
51
- }
52
- : null,
53
- config.social.linkedin
54
- ? {
55
- label: "LinkedIn",
56
- href: config.social.linkedin,
57
- external: true,
58
- icon: "lucide:linkedin",
59
- }
60
- : null,
61
- ].filter(Boolean) as NavItem[],
62
- );
40
+ const socialLinks = computed<NavItem[]>(() => resolveSocialLinks(config.social));
63
41
 
64
42
  const mainNav = computed<NavItem[]>(() =>
65
43
  resolveMainNav(config.nav.links, {
@@ -69,18 +47,10 @@ export const useSiteNavigation = () => {
69
47
  blogExists: blogExists.value,
70
48
  docsLabel: t("nav.documentation"),
71
49
  blogLabel: t("nav.blog"),
50
+ docsDescription: t("nav.docsDescription"),
51
+ blogDescription: t("nav.blogDescription"),
72
52
  }),
73
53
  );
74
54
 
75
- const footerNav = computed<{
76
- product: NavItem[];
77
- resources: NavItem[];
78
- company: NavItem[];
79
- }>(() => ({
80
- product: [],
81
- resources: [...socialLinks.value],
82
- company: [],
83
- }));
84
-
85
- return { site, banner, mainNav, footerNav, socialLinks };
55
+ return { site, banner, mainNav, socialLinks };
86
56
  };
@@ -2,7 +2,7 @@ import type { GinkoDocsLocalizedText } from "../../shared/types/app-config";
2
2
  import { defaultLocale, type LocaleCode } from "../../i18n/locales";
3
3
 
4
4
  export function getLocalizedSiteText(
5
- value: GinkoDocsLocalizedText | string,
5
+ value: GinkoDocsLocalizedText,
6
6
  locale: string,
7
7
  fallbackLocale: LocaleCode = defaultLocale,
8
8
  ): string {
package/app/error.vue CHANGED
@@ -4,7 +4,6 @@ import { messages } from "../i18n/messages/index";
4
4
  import { normalizeAppError } from "./lib/errors";
5
5
  import { computed } from "vue";
6
6
  import { clearError, useHead } from "#imports";
7
- import { useDocsEntryPath } from "#ginko-docs/features/docs/composables/useDocsEntryPath";
8
7
  import { localeFromPath, localizedPath } from "../i18n/locales";
9
8
 
10
9
  type NuxtErrorLike = {
@@ -29,8 +28,8 @@ const copy = computed(() => {
29
28
  const catalog = messages[locale.value].errors;
30
29
  return catalog[normalized.value.kind] ?? catalog.unavailable;
31
30
  });
32
- const docsPath = await useDocsEntryPath();
33
31
  const homePath = computed(() => localizedPath(locale.value, localizedRoutes[locale.value].home));
32
+ const docsPath = computed(() => localizedPath(locale.value, localizedRoutes[locale.value].docs));
34
33
  const title = computed(() => `${normalized.value.statusCode} - ${copy.value.title}`);
35
34
  // Keep the full site chrome for content-level errors (404 and friends); hard
36
35
  // server failures fall back to the bare page so the error screen cannot crash.
@@ -59,9 +58,6 @@ const handleError = () => clearError({ redirect: homePath.value });
59
58
  {{ normalized.statusCode }}
60
59
  </p>
61
60
 
62
- <p class="mb-3 text-sm font-medium tracking-wider text-primary uppercase">
63
- {{ normalized.statusMessage }}
64
- </p>
65
61
  <h1 class="mb-4 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
66
62
  {{ copy.title }}
67
63
  </h1>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "#ginko-docs/utils";
4
+ import { useI18n } from "#imports";
5
+
6
+ const props = defineProps<{
7
+ items: { title: string; path?: string }[];
8
+ class?: HTMLAttributes["class"];
9
+ }>();
10
+
11
+ const { t } = useI18n();
12
+ </script>
13
+
14
+ <template>
15
+ <nav
16
+ v-if="items.length"
17
+ :class="cn('flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground', props.class)"
18
+ :aria-label="t('docs.breadcrumbs')"
19
+ >
20
+ <template v-for="(item, index) in items" :key="item.path ?? item.title">
21
+ <Icon
22
+ v-if="index > 0"
23
+ name="lucide:chevron-right"
24
+ class="size-3.5 text-border"
25
+ aria-hidden="true"
26
+ />
27
+ <NuxtLink
28
+ v-if="item.path"
29
+ :to="item.path"
30
+ class="rounded-sm transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
31
+ >
32
+ {{ item.title }}
33
+ </NuxtLink>
34
+ <span v-else>{{ item.title }}</span>
35
+ </template>
36
+ </nav>
37
+ </template>
@@ -2,12 +2,19 @@
2
2
  import type { GinkoDocsAppConfig } from "../../../../shared/types/app-config";
3
3
  import { computed } from "vue";
4
4
  import { useAppConfig, useI18n, useRoute } from "#imports";
5
+ import { buildRepoEditUrl, buildRepoIssueUrl } from "#ginko-docs/utils/repository";
5
6
 
6
- const props = defineProps<{
7
- stem?: string;
8
- extension?: string;
9
- title: string;
10
- }>();
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ stem?: string;
10
+ extension?: string;
11
+ title: string;
12
+ variant?: "article" | "rail";
13
+ }>(),
14
+ {
15
+ variant: "article",
16
+ },
17
+ );
11
18
 
12
19
  const route = useRoute();
13
20
  const { t } = useI18n();
@@ -20,25 +27,28 @@ const sourcePath = computed(() => {
20
27
  return `${directory}/${props.stem}.${extension}`;
21
28
  });
22
29
 
23
- const editUrl = computed(() => {
24
- if (!repository || !sourcePath.value) return null;
25
- const branch = repository.branch || "main";
26
- return `${repository.url.replace(/\/$/, "")}/edit/${encodeURIComponent(branch)}/${sourcePath.value}`;
27
- });
30
+ const editUrl = computed(() =>
31
+ repository && sourcePath.value ? buildRepoEditUrl(repository, sourcePath.value) : null,
32
+ );
28
33
 
29
- const issueUrl = computed(() => {
30
- if (!repository) return null;
31
- const url = new URL(`${repository.url.replace(/\/$/, "")}/issues/new`);
32
- url.searchParams.set("title", t("docs.issueTitle", { title: props.title }));
33
- url.searchParams.set("body", t("docs.issueBody", { path: route.path }));
34
- return url.toString();
35
- });
34
+ const issueUrl = computed(() =>
35
+ repository
36
+ ? buildRepoIssueUrl(repository, {
37
+ title: t("docs.issueTitle", { title: props.title }),
38
+ body: t("docs.issueBody", { path: route.path }),
39
+ })
40
+ : null,
41
+ );
36
42
  </script>
37
43
 
38
44
  <template>
39
45
  <div
40
46
  v-if="repository"
41
- class="mt-12 flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-border pt-5 text-sm"
47
+ :class="
48
+ variant === 'rail'
49
+ ? 'flex flex-col gap-2.5 text-sm'
50
+ : 'flex flex-wrap items-center gap-x-4 gap-y-2 text-sm'
51
+ "
42
52
  :aria-label="t('docs.contribute')"
43
53
  >
44
54
  <a
@@ -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,38 @@
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 {
3
+ filterTocByDepth,
4
+ flattenTocLinks,
5
+ formatContentDate,
6
+ getMarkdownTocLinks,
7
+ } from "#ginko-docs/utils/content";
4
8
  import ContentFeedback from "#ginko-docs/components/content/Feedback.vue";
9
+ import DocsBreadcrumb from "./DocsBreadcrumb.vue";
5
10
  import DocsMobileToc from "./DocsMobileToc.vue";
6
11
  import DocsPageNav from "./DocsPageNav.vue";
7
12
  import DocsToc from "./DocsToc.vue";
8
13
  import DocsContributeLinks from "./DocsContributeLinks.vue";
9
- import { computed, watch } from "vue";
10
- import { useAppConfig, useContentPage, useHead, useI18n, useRoute, useSeoMeta } from "#imports";
14
+ import { computed, provide, ref, watch } from "vue";
15
+ import { docsTocKey } from "#ginko-docs/features/docs/toc-context";
16
+ import {
17
+ createError,
18
+ useAppConfig,
19
+ useContentPage,
20
+ useHead,
21
+ useI18n,
22
+ useRoute,
23
+ useSeoMeta,
24
+ } from "#imports";
11
25
  import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
12
26
  import { useGinkoOgImage } from "#ginko-docs/composables/useGinkoOgImage";
13
27
  import { useScrollspy } from "#ginko-docs/features/docs/composables/useScrollspy";
28
+ import { useRevealActive } from "#ginko-docs/features/docs/composables/useRevealActive";
14
29
  import PageMarkdownCopy from "#ginko-docs/components/content/PageMarkdownCopy.vue";
15
30
  import { Separator } from "#ginko-docs/components/ui/separator";
16
31
  import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
17
32
  import { useSchemaJsonLd } from "#ginko-docs/composables/useSchemaJsonLd";
18
33
  import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
19
34
  import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
20
- import { syncContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
35
+ import { useContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
21
36
 
22
37
  type DocsNavLink = {
23
38
  path: string;
@@ -34,25 +49,26 @@ const { locale, t } = useI18n();
34
49
  const config = useAppConfig().ginkoDocs;
35
50
  const route = useRoute();
36
51
  const localizedPath = useLocalizedPath();
37
- const {
38
- page,
39
- previous,
40
- next: nextContent,
41
- } = await useContentPage("docs", {
42
- locale: () => locale.value,
52
+ const canonicalUrl = useCanonicalUrl();
53
+ const { sync: syncContentRouteAlternates } = useContentRouteAlternates();
54
+ const contentPageResult = useContentPage("docs", {
43
55
  fallback: true,
44
56
  surround: true,
45
57
  });
46
- if (!page.value) throw createContentNotFoundError();
58
+ const docsNavigationResult = useDocsNavigation();
59
+ const [{ page, previous, next: nextContent, error }, { trail }] = await Promise.all([
60
+ contentPageResult,
61
+ docsNavigationResult,
62
+ ]);
63
+ if (error.value) throw error.value;
64
+ if (!page.value) throw createError({ statusCode: 404, statusMessage: "Not Found" });
47
65
  syncContentRouteAlternates(page);
48
- const { trail } = await useDocsNavigation();
49
66
 
50
67
  const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
51
68
  const pageTitle = computed(() => page.value?.title ?? t("docs.fallbackTitle"));
52
69
  const pageDescription = computed(() => page.value?.description ?? t("docs.fallbackDescription"));
53
- const canonicalUrl = useCanonicalUrl();
54
70
  const tocItems = computed(() =>
55
- filterTocByDepth(flattenTocLinks(page.value?.body?.toc?.links), config.toc?.depth ?? 3),
71
+ filterTocByDepth(flattenTocLinks(getMarkdownTocLinks(page.value?.body)), config.toc?.depth ?? 3),
56
72
  );
57
73
  const prev = computed(() => toDocsNavLink(previous.value));
58
74
  const next = computed(() => toDocsNavLink(nextContent.value));
@@ -63,12 +79,24 @@ const schemaBreadcrumbs = computed(() => [
63
79
  .filter((item) => item.path)
64
80
  .map((item) => ({ name: item.title, path: item.path! })),
65
81
  ]);
66
- const { activeId, refresh } = useScrollspy(computed(() => tocItems.value.map((item) => item.id)));
82
+ provide(docsTocKey, tocItems);
83
+ const { activeIds, refresh } = useScrollspy(computed(() => tocItems.value.map((item) => item.id)));
67
84
 
68
85
  watch(tocItems, () => {
69
86
  void refresh();
70
87
  });
71
88
 
89
+ // Keep the active TOC region visible when a long TOC overflows the sticky
90
+ // aside.
91
+ const tocAside = ref<HTMLElement | null>(null);
92
+ const { reveal: revealActiveTocLink } = useRevealActive(tocAside, "[data-toc-active]", {
93
+ topPad: 16,
94
+ bottomPad: 44,
95
+ });
96
+ watch(activeIds, () => {
97
+ requestAnimationFrame(revealActiveTocLink);
98
+ });
99
+
72
100
  useSeoMeta({
73
101
  title: computed(() => `${pageTitle.value} - ${siteName.value}`),
74
102
  description: pageDescription,
@@ -113,6 +141,11 @@ useHead(() => ({
113
141
  link: [{ key: "canonical", rel: "canonical", href: canonicalUrl.value }],
114
142
  meta: [{ property: "og:type", content: "article" }],
115
143
  }));
144
+
145
+ function scrollToTop() {
146
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
147
+ window.scrollTo({ top: 0, behavior: reducedMotion ? "auto" : "smooth" });
148
+ }
116
149
  </script>
117
150
 
118
151
  <template>
@@ -121,45 +154,27 @@ useHead(() => ({
121
154
  <DocsMobileToc
122
155
  class="order-1"
123
156
  :items="tocItems"
124
- :active-id="activeId"
157
+ :active-ids="activeIds"
125
158
  :page-title="pageTitle"
126
159
  />
127
160
 
128
161
  <main
129
162
  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"
163
+ class="order-2 mx-auto w-full max-w-[72ch] flex-1 px-4 py-8 md:px-6 xl:py-14"
131
164
  >
132
165
  <article v-if="page">
133
166
  <div class="flex items-start justify-between gap-4 sm:gap-6">
134
167
  <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>
168
+ <DocsBreadcrumb class="mb-2" :items="visibleTrail" />
157
169
  <h1
158
- class="font-heading text-[2rem] leading-[1.15] font-semibold tracking-[-0.025em] text-balance text-foreground sm:text-[2.25rem]"
170
+ class="font-heading text-[27px] leading-[1.15] font-semibold tracking-[-0.02em] text-balance text-foreground"
159
171
  >
160
172
  {{ page.title }}
161
173
  </h1>
162
- <p v-if="page.description" class="mt-2 text-lg leading-7 text-muted-foreground">
174
+ <p
175
+ v-if="page.description"
176
+ class="mt-2 text-base leading-[1.55] text-muted-foreground"
177
+ >
163
178
  {{ page.description }}
164
179
  </p>
165
180
  </div>
@@ -171,18 +186,49 @@ useHead(() => ({
171
186
  <ContentRenderer :value="page" />
172
187
  </div>
173
188
 
174
- <DocsContributeLinks :stem="page.stem" :extension="page.extension" :title="pageTitle" />
175
- <DocsPageNav :prev="prev" :next="next" />
176
- <ContentFeedback :label="t('feedback.label')" />
189
+ <footer class="mt-12 border-t border-border pt-6">
190
+ <p v-if="page.updated" class="mb-4 text-sm text-muted-foreground">
191
+ {{ t("docs.lastUpdated") }}:
192
+ <time :datetime="page.updated">{{ formatContentDate(page.updated, locale) }}</time>
193
+ </p>
194
+ <div class="flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
195
+ <ContentFeedback :label="t('feedback.label')" />
196
+ <DocsContributeLinks
197
+ class="xl:hidden"
198
+ :stem="page.stem"
199
+ :extension="page.extension"
200
+ :title="pageTitle"
201
+ />
202
+ </div>
203
+ <DocsPageNav class="mt-8" :prev="prev" :next="next" />
204
+ </footer>
177
205
  </article>
178
206
  </main>
179
207
  </div>
180
208
 
181
209
  <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"
210
+ ref="tocAside"
211
+ 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
212
  :aria-label="t('docs.toc')"
184
213
  >
185
- <DocsToc :items="tocItems" :active-id="activeId" />
214
+ <DocsToc class="mb-6" :items="tocItems" :active-ids="activeIds" />
215
+
216
+ <div class="mt-auto flex flex-col gap-2.5 border-t border-border/70 pt-5">
217
+ <DocsContributeLinks
218
+ variant="rail"
219
+ :stem="page?.stem"
220
+ :extension="page?.extension"
221
+ :title="pageTitle"
222
+ />
223
+ <button
224
+ type="button"
225
+ 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"
226
+ @click="scrollToTop"
227
+ >
228
+ <Icon name="lucide:arrow-up" class="size-4" aria-hidden="true" />
229
+ {{ t("docs.backToTop") }}
230
+ </button>
231
+ </div>
186
232
  </aside>
187
233
  </div>
188
234
  </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,38 @@ 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
- 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)]";
67
+ // The reduced top padding only applies under the section switcher; without it
68
+ // the scroll mask would fade into the first group label.
69
+ const scrollViewportClass = computed(() =>
70
+ cn(
71
+ "size-full rounded-[inherit] p-4 overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]",
72
+ switcherSections.value.length > 1 && "pt-2",
73
+ ),
74
+ );
75
+
76
+ // Deep links can land on an item far outside the visible band — center it
77
+ // once on mount and on section switches.
78
+ const scrollArea = ref<{ $el?: HTMLElement } | null>(null);
79
+
80
+ const { reveal: revealActiveItem } = useRevealActive(
81
+ () => scrollArea.value?.$el?.querySelector<HTMLElement>("[data-slot='scroll-area-viewport']"),
82
+ "[data-active='true']",
83
+ );
84
+
85
+ onMounted(() => {
86
+ void nextTick(revealActiveItem);
87
+ });
88
+ watch(activeSectionId, () => {
89
+ void nextTick(revealActiveItem);
90
+ });
67
91
 
68
92
  const asideClass = computed(() =>
69
93
  cn(
@@ -76,13 +100,7 @@ const asideClass = computed(() =>
76
100
  </script>
77
101
 
78
102
  <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
- >
103
+ <aside :data-variant="variant" :aria-label="t('docs.label')" :class="asideClass">
86
104
  <div v-if="switcherSections.length > 1" class="flex flex-col gap-3 p-4 pb-2">
87
105
  <DocsSidebarTabs
88
106
  v-if="sidebarSwitcher === 'tabs'"
@@ -104,15 +122,13 @@ const asideClass = computed(() =>
104
122
  />
105
123
  </div>
106
124
 
107
- <div
108
- :class="[scrollViewportClass, switcherSections.length > 1 ? 'pt-2' : 'pt-4']"
109
- :data-switcher="switcherSections.length > 1"
110
- >
125
+ <ScrollArea ref="scrollArea" class="min-h-0 flex-1" :viewport-class="scrollViewportClass">
111
126
  <div class="flex min-w-full flex-col gap-0.5">
112
127
  <template v-for="group in groups" :key="group.id">
113
128
  <p
114
129
  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"
130
+ data-slot="docs-sidebar-group-title"
131
+ 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
132
  >
117
133
  {{ group.title }}
118
134
  </p>
@@ -124,6 +140,6 @@ const asideClass = computed(() =>
124
140
  />
125
141
  </template>
126
142
  </div>
127
- </div>
143
+ </ScrollArea>
128
144
  </aside>
129
145
  </template>