@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
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import { useRoute } from "vue-router";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "#ginko-docs/features/docs/docs-navigation";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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 &&
|
|
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-
|
|
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
|
|
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-
|
|
46
|
-
props.depth
|
|
47
|
-
? "
|
|
48
|
-
:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
<
|
|
82
|
-
<
|
|
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
|
-
|
|
91
|
-
data-slot="docs-sidebar-folder-link"
|
|
88
|
+
:data-active="isExactActive ? 'true' : 'false'"
|
|
92
89
|
>
|
|
93
|
-
<
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
:
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
136
|
-
</
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
</
|
|
151
|
-
</
|
|
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
|
-
<
|
|
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>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
resolveDocsSectionTargetPath,
|
|
5
|
+
type DocsNavigationSection,
|
|
6
|
+
} from "#ginko-docs/features/docs/docs-navigation";
|
|
7
|
+
import { Tabs, TabsList, TabsTrigger } from "#ginko-docs/components/ui/tabs";
|
|
3
8
|
|
|
4
|
-
defineProps<{
|
|
9
|
+
const props = defineProps<{
|
|
5
10
|
sections: Array<DocsNavigationSection & { title: string }>;
|
|
6
11
|
activeId: string;
|
|
7
12
|
}>();
|
|
@@ -9,27 +14,46 @@ defineProps<{
|
|
|
9
14
|
const emit = defineEmits<{
|
|
10
15
|
"update:activeId": [id: string];
|
|
11
16
|
}>();
|
|
17
|
+
|
|
18
|
+
const tabs = computed(() =>
|
|
19
|
+
props.sections.map((section) => ({
|
|
20
|
+
id: section.id,
|
|
21
|
+
title: section.title,
|
|
22
|
+
target: resolveDocsSectionTargetPath(section),
|
|
23
|
+
})),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function onTabClick(id: string, event: MouseEvent) {
|
|
27
|
+
// Re-clicking the active section would otherwise navigate away from the
|
|
28
|
+
// current page to the section's first page.
|
|
29
|
+
if (id === props.activeId) event.preventDefault();
|
|
30
|
+
}
|
|
12
31
|
</script>
|
|
13
32
|
|
|
14
33
|
<template>
|
|
15
|
-
<
|
|
16
|
-
class="
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
<Tabs :model-value="activeId" activation-mode="manual" data-slot="docs-sidebar-tabs">
|
|
35
|
+
<TabsList class="w-full">
|
|
36
|
+
<template v-for="tab in tabs" :key="tab.id">
|
|
37
|
+
<TabsTrigger v-if="tab.target" :value="tab.id" as-child>
|
|
38
|
+
<NuxtLink
|
|
39
|
+
:to="tab.target"
|
|
40
|
+
:title="tab.title"
|
|
41
|
+
class="min-w-0 flex-1 basis-0"
|
|
42
|
+
@click="onTabClick(tab.id, $event)"
|
|
43
|
+
>
|
|
44
|
+
<span class="truncate">{{ tab.title }}</span>
|
|
45
|
+
</NuxtLink>
|
|
46
|
+
</TabsTrigger>
|
|
47
|
+
<TabsTrigger
|
|
48
|
+
v-else
|
|
49
|
+
:value="tab.id"
|
|
50
|
+
:title="tab.title"
|
|
51
|
+
class="min-w-0 flex-1 basis-0"
|
|
52
|
+
@click="emit('update:activeId', tab.id)"
|
|
53
|
+
>
|
|
54
|
+
<span class="truncate">{{ tab.title }}</span>
|
|
55
|
+
</TabsTrigger>
|
|
56
|
+
</template>
|
|
57
|
+
</TabsList>
|
|
58
|
+
</Tabs>
|
|
35
59
|
</template>
|
|
@@ -1,60 +1,106 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import { nextTick, onMounted, ref, watch } from "vue";
|
|
4
|
+
import { useEventListener } from "@vueuse/core";
|
|
5
|
+
import { cn } from "#ginko-docs/utils";
|
|
6
|
+
import type { FlatTocItem } from "#ginko-docs/utils/content";
|
|
4
7
|
import { useI18n } from "#imports";
|
|
5
8
|
|
|
6
|
-
export interface TocItem {
|
|
7
|
-
id: string;
|
|
8
|
-
label: string;
|
|
9
|
-
depth?: number; // 2 = h2, 3 = h3
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
const props = withDefaults(
|
|
13
10
|
defineProps<{
|
|
14
|
-
items:
|
|
15
|
-
|
|
11
|
+
items: FlatTocItem[];
|
|
12
|
+
activeIds?: string[];
|
|
16
13
|
showTitle?: boolean;
|
|
17
14
|
class?: HTMLAttributes["class"];
|
|
18
15
|
}>(),
|
|
19
16
|
{
|
|
20
17
|
showTitle: true,
|
|
18
|
+
activeIds: () => [],
|
|
21
19
|
},
|
|
22
20
|
);
|
|
23
21
|
const { t } = useI18n();
|
|
22
|
+
|
|
23
|
+
// The sliding indicator spans the active rows. Measure the rendered rows
|
|
24
|
+
// instead of assuming a fixed row height, so wrapped or restyled rows keep
|
|
25
|
+
// the indicator aligned.
|
|
26
|
+
const listRef = ref<HTMLElement | null>(null);
|
|
27
|
+
const indicatorStyle = ref({ opacity: "0", top: "0px", height: "0px" });
|
|
28
|
+
|
|
29
|
+
function measureIndicator() {
|
|
30
|
+
const list = listRef.value;
|
|
31
|
+
if (!list) return;
|
|
32
|
+
const active = list.querySelectorAll<HTMLElement>("[data-toc-active]");
|
|
33
|
+
const first = active[0];
|
|
34
|
+
const last = active[active.length - 1];
|
|
35
|
+
if (!first || !last) {
|
|
36
|
+
indicatorStyle.value = { ...indicatorStyle.value, opacity: "0" };
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
indicatorStyle.value = {
|
|
40
|
+
opacity: "1",
|
|
41
|
+
top: `${first.offsetTop}px`,
|
|
42
|
+
height: `${last.offsetTop + last.offsetHeight - first.offsetTop}px`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
watch(
|
|
47
|
+
() => [props.activeIds, props.items],
|
|
48
|
+
() => nextTick(measureIndicator),
|
|
49
|
+
{ deep: true },
|
|
50
|
+
);
|
|
51
|
+
onMounted(measureIndicator);
|
|
52
|
+
useEventListener("resize", measureIndicator, { passive: true });
|
|
53
|
+
|
|
54
|
+
function scrollToHeading(id: string) {
|
|
55
|
+
const element = document.getElementById(id);
|
|
56
|
+
if (!element) return;
|
|
57
|
+
// scrollIntoView ignores the global reduced-motion CSS override, so check
|
|
58
|
+
// explicitly. `scroll-margin-top` on headings handles the sticky offset.
|
|
59
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
60
|
+
element.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "start" });
|
|
61
|
+
// Sync the hash without router involvement — router-driven hash navigation
|
|
62
|
+
// would scroll-restore and fight the smooth scroll above.
|
|
63
|
+
history.replaceState(history.state, "", `#${encodeURIComponent(id)}`);
|
|
64
|
+
}
|
|
24
65
|
</script>
|
|
25
66
|
|
|
26
67
|
<template>
|
|
27
|
-
<
|
|
28
|
-
<h3
|
|
68
|
+
<nav v-if="items.length" :class="cn('w-full', props.class)" :aria-label="t('docs.toc')">
|
|
69
|
+
<h3
|
|
70
|
+
v-if="showTitle"
|
|
71
|
+
class="mb-4 flex items-center gap-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
|
72
|
+
>
|
|
29
73
|
<Icon name="lucide:list" class="size-3.5 shrink-0" aria-hidden="true" />
|
|
30
74
|
{{ t("docs.toc") }}
|
|
31
75
|
</h3>
|
|
32
76
|
|
|
33
|
-
<div class="relative
|
|
34
|
-
<!-- Animated active indicator -->
|
|
77
|
+
<div class="relative border-l border-border pl-3">
|
|
35
78
|
<div
|
|
36
|
-
class="absolute -left-px w-0.5 rounded-full bg-primary transition-all duration-200"
|
|
37
|
-
:style="
|
|
38
|
-
activeId && items.findIndex((i) => i.id === activeId) !== -1
|
|
39
|
-
? `top: ${items.findIndex((i) => i.id === activeId) * 26 + 2}px; height: 20px; opacity: 1;`
|
|
40
|
-
: 'top: 2px; height: 20px; opacity: 0;'
|
|
41
|
-
"
|
|
79
|
+
class="pointer-events-none absolute -left-px w-0.5 rounded-full bg-primary transition-all duration-200 ease-out"
|
|
80
|
+
:style="indicatorStyle"
|
|
42
81
|
/>
|
|
43
82
|
|
|
44
|
-
<
|
|
45
|
-
v-for="item in items"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
<ul ref="listRef" class="space-y-1">
|
|
84
|
+
<li v-for="item in items" :key="item.id">
|
|
85
|
+
<a
|
|
86
|
+
:href="`#${item.id}`"
|
|
87
|
+
:title="item.label"
|
|
88
|
+
:data-toc-active="activeIds.includes(item.id) ? 'true' : undefined"
|
|
89
|
+
class="block truncate rounded-sm py-0.5 text-[13px] leading-5 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
|
90
|
+
:class="
|
|
91
|
+
cn(
|
|
92
|
+
item.depth === 3 ? 'pl-3' : (item.depth ?? 2) >= 4 ? 'pl-6' : '',
|
|
93
|
+
activeIds.includes(item.id)
|
|
94
|
+
? 'font-medium text-primary'
|
|
95
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
96
|
+
)
|
|
97
|
+
"
|
|
98
|
+
@click.prevent="scrollToHeading(item.id)"
|
|
99
|
+
>
|
|
100
|
+
{{ item.label }}
|
|
101
|
+
</a>
|
|
102
|
+
</li>
|
|
103
|
+
</ul>
|
|
58
104
|
</div>
|
|
59
|
-
</
|
|
105
|
+
</nav>
|
|
60
106
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
+
import { findNavigationTrail } from "@lupinum/ginko-content/navigation";
|
|
2
3
|
import { useRoute } from "#imports";
|
|
3
4
|
import {
|
|
4
|
-
findDocsNavigationTrail,
|
|
5
5
|
getDocsNavigationSections,
|
|
6
6
|
normalizeDocsNavigationItem,
|
|
7
7
|
type DocsNavigationSection,
|
|
@@ -20,7 +20,7 @@ export async function useDocsNavigation() {
|
|
|
20
20
|
return getDocsNavigationSections(roots.value);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
const trail = computed(() =>
|
|
23
|
+
const trail = computed(() => findNavigationTrail(roots.value, route.path));
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
sections,
|
|
@@ -2,7 +2,10 @@ import { computed } from "vue";
|
|
|
2
2
|
import { navigation } from "@lupinum/ginko-content/client";
|
|
3
3
|
import { useAsyncData, useI18n } from "#imports";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Sync on purpose: the returned AsyncData is itself awaitable, so async callers
|
|
6
|
+
// still `await` it, while sync callers (e.g. the site header) read `data`
|
|
7
|
+
// reactively without suspending.
|
|
8
|
+
export function useDocsNavigationData() {
|
|
6
9
|
const { locale } = useI18n();
|
|
7
10
|
const navigationKey = computed(() => `docs-navigation:${locale.value}`);
|
|
8
11
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface RevealActiveOptions {
|
|
4
|
+
/** Band padding in px; the active element is left alone inside the band. */
|
|
5
|
+
topPad?: number;
|
|
6
|
+
bottomPad?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RevealMetrics {
|
|
10
|
+
scrollTop: number;
|
|
11
|
+
containerHeight: number;
|
|
12
|
+
/** Active element's offset from the container's visible top edge. */
|
|
13
|
+
activeTop: number;
|
|
14
|
+
activeHeight: number;
|
|
15
|
+
topPad: number;
|
|
16
|
+
bottomPad: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Returns the scrollTop that centers the active element, or null if it is
|
|
20
|
+
* already inside the visible band. */
|
|
21
|
+
export function computeRevealScrollTop(metrics: RevealMetrics): number | null {
|
|
22
|
+
const { scrollTop, containerHeight, activeTop, activeHeight, topPad, bottomPad } = metrics;
|
|
23
|
+
const visible = activeTop >= topPad && activeTop + activeHeight <= containerHeight - bottomPad;
|
|
24
|
+
if (visible) return null;
|
|
25
|
+
return scrollTop + activeTop - containerHeight / 2 + activeHeight / 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keeps the active element of an overflowing container visible by centering
|
|
30
|
+
* it when it drifts outside the visible band. Scrolling is instant on
|
|
31
|
+
* purpose: this is positioning, not motion, and must not compete with the
|
|
32
|
+
* page scroll. Callers decide when to schedule `reveal` (nextTick, rAF, …).
|
|
33
|
+
*/
|
|
34
|
+
export function useRevealActive(
|
|
35
|
+
container: Ref<HTMLElement | null> | (() => HTMLElement | null | undefined),
|
|
36
|
+
selector: string,
|
|
37
|
+
options: RevealActiveOptions = {},
|
|
38
|
+
) {
|
|
39
|
+
function reveal() {
|
|
40
|
+
const el = typeof container === "function" ? container() : container.value;
|
|
41
|
+
if (!el || el.scrollHeight <= el.clientHeight) return;
|
|
42
|
+
const active = el.querySelector<HTMLElement>(selector);
|
|
43
|
+
if (!active) return;
|
|
44
|
+
const containerRect = el.getBoundingClientRect();
|
|
45
|
+
const activeRect = active.getBoundingClientRect();
|
|
46
|
+
const next = computeRevealScrollTop({
|
|
47
|
+
scrollTop: el.scrollTop,
|
|
48
|
+
containerHeight: el.clientHeight,
|
|
49
|
+
activeTop: activeRect.top - containerRect.top,
|
|
50
|
+
activeHeight: activeRect.height,
|
|
51
|
+
topPad: options.topPad ?? 8,
|
|
52
|
+
bottomPad: options.bottomPad ?? 8,
|
|
53
|
+
});
|
|
54
|
+
if (next !== null) el.scrollTop = next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { reveal };
|
|
58
|
+
}
|