@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
|
@@ -8,26 +8,91 @@ import {
|
|
|
8
8
|
type MaybeRefOrGetter,
|
|
9
9
|
} from "vue";
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export type ScrollspyHeading = {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Viewport-relative top, in document order. */
|
|
14
|
+
top: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ScrollspyZone = {
|
|
18
|
+
zoneTop: number;
|
|
19
|
+
zoneBottom: number;
|
|
20
|
+
/** Viewport-relative bottom of the document (scrollHeight - scrollY). */
|
|
21
|
+
docEnd: number;
|
|
22
|
+
/** When the page is scrolled to the very bottom, the zone extends to docEnd
|
|
23
|
+
* so a short final section still activates. */
|
|
24
|
+
atBottom: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Headings use `scroll-margin-top: 7rem` (= ZONE_TOP), so after an anchor jump
|
|
28
|
+
// the clicked heading sits exactly at the zone top and becomes the first
|
|
29
|
+
// active entry. The tolerance absorbs sub-pixel layout drift.
|
|
30
|
+
const ZONE_TOP = 112;
|
|
31
|
+
const TOLERANCE = 8;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A heading is active while any part of its section (heading → next heading)
|
|
35
|
+
* intersects the reading zone. Pure so the zone model is unit-testable.
|
|
36
|
+
*/
|
|
37
|
+
export function computeActiveIds(headings: ScrollspyHeading[], zone: ScrollspyZone): string[] {
|
|
38
|
+
if (headings.length === 0) return [];
|
|
39
|
+
|
|
40
|
+
const zoneTop = zone.zoneTop + TOLERANCE;
|
|
41
|
+
const zoneBottom = zone.atBottom ? zone.docEnd : zone.zoneBottom;
|
|
42
|
+
|
|
43
|
+
const active: string[] = [];
|
|
44
|
+
for (let index = 0; index < headings.length; index++) {
|
|
45
|
+
const sectionTop = headings[index]!.top;
|
|
46
|
+
const sectionBottom = index < headings.length - 1 ? headings[index + 1]!.top : zone.docEnd;
|
|
47
|
+
if (sectionBottom > zoneTop && sectionTop < zoneBottom) {
|
|
48
|
+
active.push(headings[index]!.id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (active.length > 0) return active;
|
|
14
52
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
53
|
+
// No section in the zone (e.g. a long passage between headings): fall back
|
|
54
|
+
// to the last heading scrolled past the zone top, then to the first heading.
|
|
55
|
+
for (let index = headings.length - 1; index >= 0; index--) {
|
|
56
|
+
if (headings[index]!.top <= zoneTop) return [headings[index]!.id];
|
|
57
|
+
}
|
|
58
|
+
return [headings[0]!.id];
|
|
59
|
+
}
|
|
20
60
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
61
|
+
export function useScrollspy(ids: MaybeRefOrGetter<string[]>) {
|
|
62
|
+
const activeIds = ref<string[]>([]);
|
|
63
|
+
let observed: HTMLElement[] = [];
|
|
64
|
+
let rafId: number | null = null;
|
|
65
|
+
|
|
66
|
+
function update() {
|
|
67
|
+
const scrollY = window.scrollY;
|
|
68
|
+
const innerHeight = window.innerHeight;
|
|
69
|
+
const scrollHeight = document.documentElement.scrollHeight;
|
|
70
|
+
const next = computeActiveIds(
|
|
71
|
+
observed.map((element) => ({
|
|
24
72
|
id: element.id,
|
|
25
73
|
top: element.getBoundingClientRect().top,
|
|
26
|
-
}))
|
|
27
|
-
|
|
28
|
-
|
|
74
|
+
})),
|
|
75
|
+
{
|
|
76
|
+
zoneTop: ZONE_TOP,
|
|
77
|
+
zoneBottom: innerHeight * 0.8,
|
|
78
|
+
docEnd: scrollHeight - scrollY,
|
|
79
|
+
atBottom: scrollY + innerHeight >= scrollHeight - 2,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
if (
|
|
83
|
+
next.length !== activeIds.value.length ||
|
|
84
|
+
next.some((id, index) => id !== activeIds.value[index])
|
|
85
|
+
) {
|
|
86
|
+
activeIds.value = next;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
29
89
|
|
|
30
|
-
|
|
90
|
+
function scheduleUpdate() {
|
|
91
|
+
if (rafId !== null) return;
|
|
92
|
+
rafId = requestAnimationFrame(() => {
|
|
93
|
+
rafId = null;
|
|
94
|
+
update();
|
|
95
|
+
});
|
|
31
96
|
}
|
|
32
97
|
|
|
33
98
|
async function refresh() {
|
|
@@ -38,18 +103,21 @@ export function useScrollspy(ids: MaybeRefOrGetter<string[]>, offset = 112) {
|
|
|
38
103
|
.map((id) => document.getElementById(id))
|
|
39
104
|
.filter((element): element is HTMLElement => Boolean(element));
|
|
40
105
|
|
|
41
|
-
|
|
106
|
+
update();
|
|
42
107
|
}
|
|
43
108
|
|
|
44
109
|
onMounted(() => {
|
|
45
110
|
void refresh();
|
|
46
|
-
window.addEventListener("scroll",
|
|
111
|
+
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
112
|
+
window.addEventListener("resize", scheduleUpdate, { passive: true });
|
|
47
113
|
});
|
|
48
114
|
|
|
49
115
|
onBeforeUnmount(() => {
|
|
50
116
|
observed = [];
|
|
51
117
|
if (import.meta.client) {
|
|
52
|
-
window.removeEventListener("scroll",
|
|
118
|
+
window.removeEventListener("scroll", scheduleUpdate);
|
|
119
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
120
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
53
121
|
}
|
|
54
122
|
});
|
|
55
123
|
|
|
@@ -61,7 +129,7 @@ export function useScrollspy(ids: MaybeRefOrGetter<string[]>, offset = 112) {
|
|
|
61
129
|
);
|
|
62
130
|
|
|
63
131
|
return {
|
|
64
|
-
|
|
132
|
+
activeIds,
|
|
65
133
|
refresh,
|
|
66
134
|
};
|
|
67
135
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
findFirstNavigationPage,
|
|
3
|
+
navigationItemContainsPath,
|
|
4
|
+
normalizeNavigationPath,
|
|
5
|
+
type ContentNavigationTreeItem,
|
|
6
|
+
type NavigationSidebar,
|
|
7
|
+
} from "@lupinum/ginko-content/navigation";
|
|
2
8
|
|
|
3
9
|
export type RawDocsTreeItem = Omit<ContentNavigationTreeItem, "children"> & {
|
|
4
10
|
icon?: unknown;
|
|
5
11
|
badge?: unknown;
|
|
6
|
-
sidebar?: unknown;
|
|
7
12
|
children?: RawDocsTreeItem[];
|
|
8
13
|
};
|
|
9
14
|
|
|
@@ -13,7 +18,7 @@ export type DocsNavigationItem = {
|
|
|
13
18
|
path?: string;
|
|
14
19
|
icon?: string;
|
|
15
20
|
badge?: string;
|
|
16
|
-
sidebar?:
|
|
21
|
+
sidebar?: NavigationSidebar;
|
|
17
22
|
children: DocsNavigationItem[];
|
|
18
23
|
};
|
|
19
24
|
|
|
@@ -47,47 +52,29 @@ export function normalizeDocsNavigationItem(
|
|
|
47
52
|
path: item.path,
|
|
48
53
|
icon: typeof item.icon === "string" ? item.icon : undefined,
|
|
49
54
|
badge: typeof item.badge === "string" ? item.badge : undefined,
|
|
50
|
-
sidebar: item.sidebar
|
|
55
|
+
sidebar: item.sidebar,
|
|
51
56
|
children: (item.children ?? []).map((child, index) =>
|
|
52
57
|
normalizeDocsNavigationItem(child, `${positionKey}.${index}`),
|
|
53
58
|
),
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
export function normalizeDocsNavigationPath(path: string): string {
|
|
58
|
-
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function docsNavigationItemContainsPath(item: DocsNavigationItem, path: string): boolean {
|
|
62
|
-
const normalizedPath = normalizeDocsNavigationPath(path);
|
|
63
|
-
return (
|
|
64
|
-
(item.path !== undefined && normalizeDocsNavigationPath(item.path) === normalizedPath) ||
|
|
65
|
-
item.children.some((child) => docsNavigationItemContainsPath(child, normalizedPath))
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
62
|
export function docsNavigationSectionContainsPath(
|
|
70
63
|
section: DocsNavigationSection,
|
|
71
64
|
path: string,
|
|
72
65
|
): boolean {
|
|
73
|
-
const normalizedPath =
|
|
66
|
+
const normalizedPath = normalizeNavigationPath(path);
|
|
74
67
|
return (
|
|
75
|
-
(section.path !== undefined &&
|
|
76
|
-
section.items.some((item) =>
|
|
68
|
+
(section.path !== undefined && normalizeNavigationPath(section.path) === normalizedPath) ||
|
|
69
|
+
section.items.some((item) => navigationItemContainsPath(item, normalizedPath))
|
|
77
70
|
);
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
):
|
|
84
|
-
|
|
85
|
-
for (const item of items) {
|
|
86
|
-
if (item.path && normalizeDocsNavigationPath(item.path) === normalizedPath) return [item];
|
|
87
|
-
const descendants = findDocsNavigationTrail(item.children, normalizedPath);
|
|
88
|
-
if (descendants.length > 0) return [item, ...descendants];
|
|
89
|
-
}
|
|
90
|
-
return [];
|
|
73
|
+
// Structural sections have no page of their own — they resolve to their first
|
|
74
|
+
// navigable page so switching to them lands somewhere instead of only swapping
|
|
75
|
+
// the sidebar contents.
|
|
76
|
+
export function resolveDocsSectionTargetPath(section: DocsNavigationSection): string | undefined {
|
|
77
|
+
return section.path ?? findFirstNavigationPage(section.items)?.path;
|
|
91
78
|
}
|
|
92
79
|
|
|
93
80
|
export function getDocsNavigationSections(items: DocsNavigationItem[]): DocsNavigationSection[] {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from "vue";
|
|
2
|
+
import type { FlatTocItem } from "#ginko-docs/utils/content";
|
|
3
|
+
|
|
4
|
+
/** Current page TOC, provided by the docs page shell so in-content components
|
|
5
|
+
* (e.g. the `::toc` inline table of contents) can render it. */
|
|
6
|
+
export const docsTocKey: InjectionKey<ComputedRef<FlatTocItem[]>> = Symbol("ginko-docs-toc");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CommandCenterGroup = "recent" | "pages" | "docs_nav" | "
|
|
1
|
+
export type CommandCenterGroup = "recent" | "pages" | "docs_nav" | "actions" | "search";
|
|
2
2
|
|
|
3
3
|
export interface CommandCenterItem {
|
|
4
4
|
id: string;
|
|
@@ -24,19 +24,6 @@ export interface CommandCenterGroupResult {
|
|
|
24
24
|
export const MAX_RECENT_ITEMS = 5;
|
|
25
25
|
export const PAGE_HIGHLIGHT_STORAGE_KEY = "site-command-center-page-highlight";
|
|
26
26
|
|
|
27
|
-
const GROUP_PRIORITY: Record<CommandCenterGroup, number> = {
|
|
28
|
-
recent: 0,
|
|
29
|
-
pages: 1,
|
|
30
|
-
docs_nav: 2,
|
|
31
|
-
blog: 3,
|
|
32
|
-
docs: 4,
|
|
33
|
-
actions: 5,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function normalizeValue(value: string) {
|
|
37
|
-
return value.toLocaleLowerCase().trim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
27
|
export function dedupeCommandCenterItems(items: CommandCenterItem[]) {
|
|
41
28
|
const seen = new Set<string>();
|
|
42
29
|
return items.filter((item) => {
|
|
@@ -47,84 +34,22 @@ export function dedupeCommandCenterItems(items: CommandCenterItem[]) {
|
|
|
47
34
|
});
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
export function scoreCommandCenterItem(item: CommandCenterItem, query: string) {
|
|
51
|
-
if (!query.trim()) return 1;
|
|
52
|
-
|
|
53
|
-
const normalizedQuery = normalizeValue(query);
|
|
54
|
-
const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
55
|
-
const title = normalizeValue(item.title);
|
|
56
|
-
const subtitle = normalizeValue(item.subtitle ?? "");
|
|
57
|
-
const keywords = (item.keywords ?? []).map(normalizeValue);
|
|
58
|
-
const haystacks = [title, subtitle, ...keywords].filter(Boolean);
|
|
59
|
-
const isAnchoredResult = item.href?.includes("#") ?? false;
|
|
60
|
-
|
|
61
|
-
if (!tokens.every((token) => haystacks.some((value) => value.includes(token)))) return 0;
|
|
62
|
-
|
|
63
|
-
let score = 0;
|
|
64
|
-
if (title === normalizedQuery) score += isAnchoredResult ? 48 : 140;
|
|
65
|
-
if (title.startsWith(normalizedQuery)) score += isAnchoredResult ? 40 : 110;
|
|
66
|
-
if (title.includes(normalizedQuery)) score += isAnchoredResult ? 32 : 80;
|
|
67
|
-
if (subtitle.includes(normalizedQuery)) score += 48;
|
|
68
|
-
|
|
69
|
-
for (const keyword of keywords) {
|
|
70
|
-
if (keyword === normalizedQuery) score += 90;
|
|
71
|
-
else if (keyword.startsWith(normalizedQuery)) score += 56;
|
|
72
|
-
else if (keyword.includes(normalizedQuery)) score += 32;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const token of tokens) {
|
|
76
|
-
if (title.startsWith(token)) score += 18;
|
|
77
|
-
if (title.includes(token)) score += 12;
|
|
78
|
-
if (subtitle.includes(token)) score += 8;
|
|
79
|
-
if (keywords.some((keyword) => keyword.startsWith(token))) score += 10;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (item.href && !isAnchoredResult) score += 16;
|
|
83
|
-
if (isAnchoredResult) score -= 12;
|
|
84
|
-
if (item.group === "pages") score += 20;
|
|
85
|
-
return score;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
37
|
export function groupCommandCenterItems(
|
|
89
38
|
items: CommandCenterItem[],
|
|
90
|
-
query: string,
|
|
91
39
|
groupTitle: (group: CommandCenterGroup) => string,
|
|
92
40
|
): CommandCenterGroupResult[] {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}))
|
|
99
|
-
.filter(({ score }) => score > 0)
|
|
100
|
-
.sort((left, right) => {
|
|
101
|
-
if (isSearching && left.score !== right.score) return right.score - left.score;
|
|
102
|
-
if (left.item.group !== right.item.group) {
|
|
103
|
-
return GROUP_PRIORITY[left.item.group] - GROUP_PRIORITY[right.item.group];
|
|
104
|
-
}
|
|
105
|
-
return isSearching ? left.item.title.localeCompare(right.item.title) : 0;
|
|
106
|
-
})
|
|
107
|
-
.slice(0, isSearching ? 12 : undefined);
|
|
108
|
-
|
|
109
|
-
const groups = new Map<CommandCenterGroup, Array<{ item: CommandCenterItem; score: number }>>();
|
|
110
|
-
for (const entry of ranked) {
|
|
111
|
-
const group = groups.get(entry.item.group) ?? [];
|
|
112
|
-
group.push(entry);
|
|
113
|
-
groups.set(entry.item.group, group);
|
|
41
|
+
const groups = new Map<CommandCenterGroup, CommandCenterItem[]>();
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
const group = groups.get(item.group) ?? [];
|
|
44
|
+
group.push(item);
|
|
45
|
+
groups.set(item.group, group);
|
|
114
46
|
}
|
|
115
47
|
|
|
116
|
-
return [...groups.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
items: entries.slice(0, 5).map((entry) => entry.item),
|
|
122
|
-
}))
|
|
123
|
-
.sort((left, right) => {
|
|
124
|
-
if (isSearching && left.maxScore !== right.maxScore) return right.maxScore - left.maxScore;
|
|
125
|
-
return GROUP_PRIORITY[left.id] - GROUP_PRIORITY[right.id];
|
|
126
|
-
})
|
|
127
|
-
.map(({ maxScore: _maxScore, ...group }) => group);
|
|
48
|
+
return [...groups].map(([id, groupItems]) => ({
|
|
49
|
+
id,
|
|
50
|
+
title: groupTitle(id),
|
|
51
|
+
items: groupItems.slice(0, 5),
|
|
52
|
+
}));
|
|
128
53
|
}
|
|
129
54
|
|
|
130
55
|
export function resolveRecentItems(
|
|
@@ -150,7 +75,3 @@ export function rememberRecentItem(
|
|
|
150
75
|
const id = selectedItem.sourceId ?? selectedItem.id;
|
|
151
76
|
return [id, ...storedItems.filter((item) => item !== id)].slice(0, MAX_RECENT_ITEMS);
|
|
152
77
|
}
|
|
153
|
-
|
|
154
|
-
export function contentSearchGroup(collection: string): "blog" | "docs" {
|
|
155
|
-
return collection === "blog" ? "blog" : "docs";
|
|
156
|
-
}
|
|
@@ -16,39 +16,24 @@ const { t } = useI18n();
|
|
|
16
16
|
const route = useRoute();
|
|
17
17
|
const inputRef = useTemplateRef<HTMLInputElement>("input");
|
|
18
18
|
const highlightedId = ref("");
|
|
19
|
-
const { open, query, groupedItems, selectItem } =
|
|
19
|
+
const { open, query, groupedItems, searchPending, searchError, searchIsEmpty, selectItem } =
|
|
20
|
+
await useCommandCenter();
|
|
20
21
|
|
|
21
22
|
const visibleItems = computed(() => groupedItems.value.flatMap((g) => g.items));
|
|
23
|
+
const isSearching = computed(() => query.value.trim().length > 0);
|
|
22
24
|
|
|
23
25
|
// ── text highlighting ──────────────────────────────────────────────────────
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.trim()
|
|
36
|
-
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
37
|
-
.split(/\s+/)
|
|
38
|
-
.filter(Boolean)
|
|
39
|
-
.join("|");
|
|
40
|
-
const parts = text.split(new RegExp(`(${pattern})`, "gi"));
|
|
41
|
-
return parts
|
|
42
|
-
.map((part) =>
|
|
43
|
-
part.toLowerCase() === q.trim().toLowerCase() ||
|
|
44
|
-
q
|
|
45
|
-
.trim()
|
|
46
|
-
.split(/\s+/)
|
|
47
|
-
.some((t) => part.toLowerCase() === t.toLowerCase())
|
|
48
|
-
? `<mark class="bg-primary/15 text-primary rounded-[3px] font-medium not-italic">${escapeHtml(part)}</mark>`
|
|
49
|
-
: escapeHtml(part),
|
|
50
|
-
)
|
|
51
|
-
.join("");
|
|
26
|
+
function highlightTokens(text: string, q: string): { text: string; match: boolean }[] {
|
|
27
|
+
const terms = q.trim().split(/\s+/).filter(Boolean);
|
|
28
|
+
if (terms.length === 0) return [{ text, match: false }];
|
|
29
|
+
const pattern = terms.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
30
|
+
return text
|
|
31
|
+
.split(new RegExp(`(${pattern})`, "gi"))
|
|
32
|
+
.filter((part) => part !== "")
|
|
33
|
+
.map((part) => ({
|
|
34
|
+
text: part,
|
|
35
|
+
match: terms.some((term) => part.toLowerCase() === term.toLowerCase()),
|
|
36
|
+
}));
|
|
52
37
|
}
|
|
53
38
|
|
|
54
39
|
// ── keyboard navigation ────────────────────────────────────────────────────
|
|
@@ -181,8 +166,26 @@ onMounted(async () => {
|
|
|
181
166
|
|
|
182
167
|
<!-- Results -->
|
|
183
168
|
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain py-2">
|
|
169
|
+
<div
|
|
170
|
+
v-if="isSearching && searchPending"
|
|
171
|
+
class="flex items-center justify-center gap-2 px-6 py-12 text-sm text-muted-foreground"
|
|
172
|
+
role="status"
|
|
173
|
+
>
|
|
174
|
+
<Icon name="lucide:loader-circle" class="size-4 animate-spin" aria-hidden="true" />
|
|
175
|
+
{{ t("command.loading") }}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div
|
|
179
|
+
v-else-if="isSearching && searchError"
|
|
180
|
+
class="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center"
|
|
181
|
+
role="alert"
|
|
182
|
+
>
|
|
183
|
+
<Icon name="lucide:circle-alert" class="size-5 text-destructive" aria-hidden="true" />
|
|
184
|
+
<p class="max-w-sm text-sm font-medium text-foreground">{{ t("command.error") }}</p>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
184
187
|
<!-- Groups -->
|
|
185
|
-
<template v-if="visibleItems.length">
|
|
188
|
+
<template v-else-if="visibleItems.length">
|
|
186
189
|
<div v-for="group in groupedItems" :key="group.id" class="mb-1 px-2 last:mb-0">
|
|
187
190
|
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
|
188
191
|
<span class="text-[11px] font-medium text-muted-foreground">{{ group.title }}</span>
|
|
@@ -225,11 +228,18 @@ onMounted(async () => {
|
|
|
225
228
|
<!-- Text -->
|
|
226
229
|
<div class="min-w-0 flex-1">
|
|
227
230
|
<div class="flex min-w-0 items-center gap-2">
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
<span class="min-w-0 truncate text-sm leading-5 font-medium">
|
|
232
|
+
<template
|
|
233
|
+
v-for="(token, tokenIndex) in highlightTokens(item.title, query)"
|
|
234
|
+
:key="tokenIndex"
|
|
235
|
+
>
|
|
236
|
+
<mark
|
|
237
|
+
v-if="token.match"
|
|
238
|
+
class="rounded-[3px] bg-primary/15 font-medium text-primary not-italic"
|
|
239
|
+
>{{ token.text }}</mark
|
|
240
|
+
><template v-else>{{ token.text }}</template>
|
|
241
|
+
</template>
|
|
242
|
+
</span>
|
|
233
243
|
<span
|
|
234
244
|
v-if="item.badge"
|
|
235
245
|
class="shrink-0 rounded-full bg-primary/10 px-1.5 py-px text-[10px] font-medium text-primary"
|
|
@@ -246,8 +256,18 @@ onMounted(async () => {
|
|
|
246
256
|
class="mt-1 size-3 shrink-0 opacity-50"
|
|
247
257
|
aria-hidden="true"
|
|
248
258
|
/>
|
|
249
|
-
<span class="line-clamp-2 min-w-0"
|
|
250
|
-
|
|
259
|
+
<span class="line-clamp-2 min-w-0">
|
|
260
|
+
<template
|
|
261
|
+
v-for="(token, tokenIndex) in highlightTokens(item.subtitle, query)"
|
|
262
|
+
:key="tokenIndex"
|
|
263
|
+
>
|
|
264
|
+
<mark
|
|
265
|
+
v-if="token.match"
|
|
266
|
+
class="rounded-[3px] bg-primary/15 font-medium text-primary not-italic"
|
|
267
|
+
>{{ token.text }}</mark
|
|
268
|
+
><template v-else>{{ token.text }}</template>
|
|
269
|
+
</template>
|
|
270
|
+
</span>
|
|
251
271
|
</p>
|
|
252
272
|
</div>
|
|
253
273
|
|
|
@@ -274,7 +294,7 @@ onMounted(async () => {
|
|
|
274
294
|
|
|
275
295
|
<!-- Empty state -->
|
|
276
296
|
<div
|
|
277
|
-
v-else
|
|
297
|
+
v-else-if="!isSearching || searchIsEmpty"
|
|
278
298
|
class="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center"
|
|
279
299
|
>
|
|
280
300
|
<div
|