@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.
- package/README.md +130 -73
- package/app/app.config.ts +15 -13
- package/app/app.vue +4 -2
- package/app/assets/css/prose.css +2203 -149
- package/app/assets/css/tailwind.css +60 -38
- package/app/assets/css/theme-palettes.css +309 -0
- package/app/components/content/DocumentPageShell.vue +1 -1
- package/app/components/content/Feedback.vue +12 -11
- package/app/components/content/ImageZoomDialog.vue +84 -0
- package/app/components/content/PageMarkdownCopy.vue +2 -0
- package/app/components/content/imageZoom.ts +25 -0
- package/app/components/mdc/MdcAccordion.vue +6 -3
- package/app/components/mdc/MdcAccordionItem.vue +7 -16
- package/app/components/mdc/MdcApi.vue +159 -0
- package/app/components/mdc/MdcAside.vue +6 -10
- package/app/components/mdc/MdcCallout.vue +9 -10
- package/app/components/mdc/MdcCard.vue +36 -47
- package/app/components/mdc/MdcCards.vue +30 -0
- package/app/components/mdc/MdcCodeGroup.vue +4 -0
- package/app/components/mdc/MdcCodeTree.vue +234 -0
- package/app/components/mdc/MdcCollapse.vue +61 -0
- package/app/components/mdc/MdcError.vue +8 -1
- package/app/components/mdc/MdcExcerpt.vue +26 -0
- package/app/components/mdc/MdcFigure.vue +54 -20
- package/app/components/mdc/MdcFiles.vue +120 -0
- package/app/components/mdc/MdcIdea.vue +2 -1
- package/app/components/mdc/MdcInfo.vue +2 -1
- package/app/components/mdc/MdcInlineToc.vue +51 -0
- package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
- package/app/components/mdc/MdcNote.vue +2 -1
- package/app/components/mdc/MdcQuiz.vue +43 -83
- package/app/components/mdc/MdcQuizQuestion.vue +104 -109
- package/app/components/mdc/MdcReadMore.vue +57 -77
- package/app/components/mdc/MdcSteps.vue +45 -11
- package/app/components/mdc/MdcSuccess.vue +8 -1
- package/app/components/mdc/MdcTabs.vue +44 -26
- package/app/components/mdc/MdcTimeline.vue +4 -1
- package/app/components/mdc/MdcTimelineItem.vue +18 -27
- package/app/components/mdc/MdcWarning.vue +8 -1
- package/app/components/mdc/api.utils.ts +132 -0
- package/app/components/mdc/code-tree.utils.ts +22 -0
- package/app/components/mdc/quiz.utils.ts +38 -0
- package/app/components/prose/ProseBlockquote.vue +17 -1
- package/app/components/prose/ProseH1.vue +1 -1
- package/app/components/prose/ProseH2.vue +7 -2
- package/app/components/prose/ProseH3.vue +7 -2
- package/app/components/prose/ProseH4.vue +17 -2
- package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
- package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
- package/app/components/prose/ProseImg.vue +34 -99
- package/app/components/prose/ProsePre.vue +63 -100
- package/app/components/prose/ProseTable.vue +5 -3
- package/app/components/site/ModeToggle.vue +12 -3
- package/app/components/site/SiteBanner.vue +43 -2
- package/app/components/site/SiteFooter.vue +3 -5
- package/app/components/site/SiteHeader.vue +266 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +25 -9
- package/app/components/site/SiteSocialLinks.vue +31 -0
- package/app/components/ui/badge/Badge.vue +1 -1
- package/app/components/ui/button/Button.vue +1 -1
- package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
- package/app/components/ui/dropdown-menu/index.ts +0 -9
- package/app/components/ui/kbd/Kbd.vue +1 -1
- package/app/components/ui/kbd/index.ts +0 -1
- package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/separator/Separator.vue +1 -1
- package/app/components/ui/sheet/SheetContent.vue +1 -1
- package/app/components/ui/sheet/SheetDescription.vue +1 -1
- package/app/components/ui/sheet/SheetOverlay.vue +1 -1
- package/app/components/ui/sheet/SheetTitle.vue +1 -1
- package/app/components/ui/sheet/index.ts +0 -3
- package/app/components/ui/tabs/Tabs.vue +1 -1
- package/app/components/ui/tabs/TabsList.vue +1 -1
- package/app/components/ui/tabs/TabsTrigger.vue +1 -1
- package/app/components/ui/tabs/index.ts +0 -1
- package/app/composables/site-navigation.utils.ts +69 -12
- package/app/composables/useContentRouteAlternates.ts +13 -21
- package/app/composables/useLocalizedRouteSwitch.ts +1 -1
- package/app/composables/useMetaKey.ts +18 -0
- package/app/composables/useProseAppearance.ts +24 -0
- package/app/composables/useSiteNavigation.ts +10 -40
- package/app/config/site.utils.ts +1 -1
- package/app/error.vue +1 -5
- package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
- package/app/features/docs/components/DocsContributeLinks.vue +28 -18
- package/app/features/docs/components/DocsMobileToc.vue +12 -8
- package/app/features/docs/components/DocsPageContent.vue +93 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +37 -21
- package/app/features/docs/components/DocsSidebarItem.vue +53 -100
- package/app/features/docs/components/DocsSidebarRow.vue +27 -0
- package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
- package/app/features/docs/components/DocsToc.vue +80 -34
- package/app/features/docs/composables/useDocsNavigation.ts +2 -2
- package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
- package/app/features/docs/composables/useRevealActive.ts +58 -0
- package/app/features/docs/composables/useScrollspy.ts +87 -19
- package/app/features/docs/docs-navigation.ts +17 -30
- package/app/features/docs/toc-context.ts +6 -0
- package/app/features/search/command-center.ts +11 -90
- package/app/features/search/components/SiteCommandCenter.vue +58 -38
- package/app/features/search/useCommandCenter.ts +48 -48
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +26 -9
- package/app/pages/blog/index.vue +38 -14
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +180 -61
- package/app/plugins/ginko-docs-theme.ts +12 -0
- package/app/utils/content.ts +11 -0
- package/app/utils/file-icons.ts +90 -0
- package/app/utils/index.ts +7 -1
- package/app/utils/repository.ts +20 -0
- package/content.js +68 -50
- package/content.ts +51 -32
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +4 -0
- package/i18n/messages/global/nav.ts +6 -3
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +9 -56
- package/index.d.ts +7 -1
- package/modules/feature-routing.ts +15 -0
- package/nuxt.config.ts +21 -7
- package/package.json +29 -26
- package/runtime/server/agent-markdown.ts +0 -2
- package/server/api/_ginko-docs/redirects.get.ts +28 -0
- package/server/middleware/redirects.ts +22 -0
- package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
- package/server/routes/blog/rss.xml.get.ts +5 -0
- package/server/utils/blog-feed.ts +49 -0
- package/server/utils/feed.ts +70 -0
- package/server/utils/redirects.ts +38 -0
- package/server/utils/redirects.utils.ts +58 -0
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +75 -10
- package/tags.ts +47 -59
- package/app/components/mdc/MdcAlert.vue +0 -34
- package/app/components/mdc/MdcCardGroup.vue +0 -47
- package/app/components/mdc/MdcDocImg.vue +0 -25
- package/app/components/mdc/MdcField.vue +0 -62
- package/app/components/mdc/MdcFieldGroup.vue +0 -18
- package/app/components/mdc/MdcPassage.vue +0 -32
- package/app/components/mdc/MdcQuizOption.vue +0 -91
- package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
- package/app/components/mdc/MdcShortcut.vue +0 -46
- package/app/components/mdc/MdcStep.vue +0 -42
- package/app/components/ui/accordion/Accordion.vue +0 -15
- package/app/components/ui/accordion/AccordionContent.vue +0 -23
- package/app/components/ui/accordion/AccordionItem.vue +0 -24
- package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
- package/app/components/ui/accordion/index.ts +0 -4
- package/app/components/ui/alert/Alert.vue +0 -21
- package/app/components/ui/alert/AlertDescription.vue +0 -22
- package/app/components/ui/alert/AlertTitle.vue +0 -22
- package/app/components/ui/alert/index.ts +0 -34
- package/app/components/ui/card/Card.vue +0 -16
- package/app/components/ui/card/CardAction.vue +0 -17
- package/app/components/ui/card/CardContent.vue +0 -14
- package/app/components/ui/card/CardDescription.vue +0 -14
- package/app/components/ui/card/CardFooter.vue +0 -14
- package/app/components/ui/card/CardHeader.vue +0 -22
- package/app/components/ui/card/card-variants.ts +0 -15
- package/app/components/ui/card/index.ts +0 -8
- package/app/components/ui/checkbox/Checkbox.vue +0 -35
- package/app/components/ui/checkbox/index.ts +0 -1
- package/app/components/ui/dialog/Dialog.vue +0 -15
- package/app/components/ui/dialog/DialogClose.vue +0 -12
- package/app/components/ui/dialog/DialogContent.vue +0 -69
- package/app/components/ui/dialog/DialogDescription.vue +0 -21
- package/app/components/ui/dialog/DialogFooter.vue +0 -15
- package/app/components/ui/dialog/DialogOverlay.vue +0 -26
- package/app/components/ui/dialog/DialogPortal.vue +0 -12
- package/app/components/ui/dialog/DialogTitle.vue +0 -21
- package/app/components/ui/dialog/DialogTrigger.vue +0 -12
- package/app/components/ui/dialog/index.ts +0 -10
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
- package/app/components/ui/input/Input.vue +0 -23
- package/app/components/ui/input/index.ts +0 -1
- package/app/components/ui/label/Label.vue +0 -14
- package/app/components/ui/label/index.ts +0 -1
- package/app/components/ui/sheet/SheetClose.vue +0 -12
- package/app/components/ui/sheet/SheetFooter.vue +0 -12
- package/app/components/ui/sheet/SheetHeader.vue +0 -12
- package/app/components/ui/switch/Switch.vue +0 -39
- package/app/components/ui/switch/index.ts +0 -1
- package/app/components/ui/tabs/TabsContent.vue +0 -21
- package/app/components/ui/textarea/Textarea.vue +0 -31
- package/app/components/ui/textarea/index.ts +0 -1
- package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
- package/app/lib/utils.ts +0 -7
- package/i18n/messages/source.ts +0 -5
- package/runtime/markdown/inline-code-highlight.ts +0 -155
- 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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
};
|
package/app/config/site.utils.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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="
|
|
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/
|
|
12
|
-
import type {
|
|
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:
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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 {
|
|
3
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
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-[
|
|
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
|
|
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
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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-
|
|
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="
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
</
|
|
143
|
+
</ScrollArea>
|
|
128
144
|
</aside>
|
|
129
145
|
</template>
|