@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
import { createError, setHeader } from "h3";
|
|
3
|
+
import { many } from "@lupinum/ginko-content/server";
|
|
4
|
+
import { useAppConfig, useRuntimeConfig } from "#imports";
|
|
5
|
+
import { blog } from "../../i18n/messages/global/blog";
|
|
6
|
+
import { locales, localizedPath, type LocaleCode } from "../../i18n/locales";
|
|
7
|
+
import { routeSlugs } from "../../shared/route-slugs";
|
|
8
|
+
import { getLocalizedSiteText } from "../../app/config/site.utils";
|
|
9
|
+
import { buildRssFeed } from "./feed";
|
|
10
|
+
|
|
11
|
+
export const MAX_FEED_POSTS = 50;
|
|
12
|
+
|
|
13
|
+
export function blogFeedPath(locale: LocaleCode): string {
|
|
14
|
+
return `${localizedPath(locale, routeSlugs.blog[locale])}/rss.xml`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function serveBlogFeed(event: H3Event, locale: LocaleCode) {
|
|
18
|
+
const contentRuntime = useRuntimeConfig(event).public.content as
|
|
19
|
+
| { collections?: Record<string, unknown> }
|
|
20
|
+
| undefined;
|
|
21
|
+
if (!contentRuntime?.collections?.blog) {
|
|
22
|
+
throw createError({ statusCode: 404, statusMessage: "Blog is not enabled" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const site = useAppConfig().ginkoDocs.site;
|
|
26
|
+
const posts = await many(event, "blog", {
|
|
27
|
+
locale,
|
|
28
|
+
fallback: true,
|
|
29
|
+
populate: { author: "authors" },
|
|
30
|
+
sort: { date: "desc" },
|
|
31
|
+
limit: MAX_FEED_POSTS,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
setHeader(event, "content-type", "application/rss+xml; charset=utf-8");
|
|
35
|
+
return buildRssFeed({
|
|
36
|
+
title: `${blog.title[locale]} - ${getLocalizedSiteText(site.name, locale)}`,
|
|
37
|
+
description: blog.description[locale],
|
|
38
|
+
siteUrl: site.url,
|
|
39
|
+
feedPath: blogFeedPath(locale),
|
|
40
|
+
language: locales.find((entry) => entry.code === locale)?.language ?? locale,
|
|
41
|
+
items: posts.map((post) => ({
|
|
42
|
+
title: post.title,
|
|
43
|
+
path: post.route.resolvedPath,
|
|
44
|
+
description: post.description,
|
|
45
|
+
date: post.date,
|
|
46
|
+
authorName: post.author?.name,
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface RssFeedItem {
|
|
2
|
+
title: string;
|
|
3
|
+
/** Site-relative path of the post, e.g. "/blog/my-post". */
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
/** Authored publication date, YYYY-MM-DD. */
|
|
7
|
+
date: string;
|
|
8
|
+
authorName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RssFeedInput {
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
/** Absolute site origin, e.g. "https://docs.example.com". */
|
|
15
|
+
siteUrl: string;
|
|
16
|
+
/** Site-relative path of the feed itself, e.g. "/blog/rss.xml". */
|
|
17
|
+
feedPath: string;
|
|
18
|
+
language: string;
|
|
19
|
+
items: RssFeedItem[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeXml(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replaceAll("&", "&")
|
|
25
|
+
.replaceAll("<", "<")
|
|
26
|
+
.replaceAll(">", ">")
|
|
27
|
+
.replaceAll('"', """)
|
|
28
|
+
.replaceAll("'", "'");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toRfc822(date: string): string {
|
|
32
|
+
return new Date(`${date}T00:00:00Z`).toUTCString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildRssFeed(feed: RssFeedInput): string {
|
|
36
|
+
const base = feed.siteUrl.replace(/\/$/, "");
|
|
37
|
+
const newestDate = feed.items
|
|
38
|
+
.map((item) => item.date)
|
|
39
|
+
.sort()
|
|
40
|
+
.at(-1);
|
|
41
|
+
|
|
42
|
+
const items = feed.items
|
|
43
|
+
.map((item) => {
|
|
44
|
+
const link = `${base}${item.path}`;
|
|
45
|
+
const author = item.authorName
|
|
46
|
+
? `\n <dc:creator>${escapeXml(item.authorName)}</dc:creator>`
|
|
47
|
+
: "";
|
|
48
|
+
return ` <item>
|
|
49
|
+
<title>${escapeXml(item.title)}</title>
|
|
50
|
+
<link>${escapeXml(link)}</link>
|
|
51
|
+
<guid>${escapeXml(link)}</guid>
|
|
52
|
+
<pubDate>${toRfc822(item.date)}</pubDate>
|
|
53
|
+
<description>${escapeXml(item.description)}</description>${author}
|
|
54
|
+
</item>`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
57
|
+
|
|
58
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
59
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
60
|
+
<channel>
|
|
61
|
+
<title>${escapeXml(feed.title)}</title>
|
|
62
|
+
<link>${escapeXml(`${base}${feed.feedPath.replace(/rss\.xml$/, "")}`)}</link>
|
|
63
|
+
<atom:link href="${escapeXml(`${base}${feed.feedPath}`)}" rel="self" type="application/rss+xml"/>
|
|
64
|
+
<description>${escapeXml(feed.description)}</description>
|
|
65
|
+
<language>${escapeXml(feed.language)}</language>${newestDate ? `\n <lastBuildDate>${toRfc822(newestDate)}</lastBuildDate>` : ""}
|
|
66
|
+
${items}
|
|
67
|
+
</channel>
|
|
68
|
+
</rss>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
import { many } from "@lupinum/ginko-content/server";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { localeCodes } from "../../i18n/locales";
|
|
5
|
+
import { buildRedirectMap, type RedirectSourceDocument } from "./redirects.utils";
|
|
6
|
+
|
|
7
|
+
async function queryRedirectDocuments(event: H3Event): Promise<RedirectSourceDocument[]> {
|
|
8
|
+
const contentRuntime = useRuntimeConfig(event).public.content as
|
|
9
|
+
| { collections?: Record<string, unknown> }
|
|
10
|
+
| undefined;
|
|
11
|
+
const collections = ["docs", ...(contentRuntime?.collections?.blog ? ["blog"] : [])];
|
|
12
|
+
|
|
13
|
+
const results = await Promise.all(
|
|
14
|
+
collections.flatMap((collection) =>
|
|
15
|
+
localeCodes.map((locale) =>
|
|
16
|
+
// fallback: false — an English page resolved into a German route would
|
|
17
|
+
// otherwise register its redirectFrom entries twice.
|
|
18
|
+
many(event, collection, { locale, fallback: false }),
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
return results.flat() as RedirectSourceDocument[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedMap: Promise<Map<string, string>> | undefined;
|
|
26
|
+
|
|
27
|
+
export function loadRedirectMap(event: H3Event): Promise<Map<string, string>> {
|
|
28
|
+
if (import.meta.dev) {
|
|
29
|
+
return queryRedirectDocuments(event).then(buildRedirectMap);
|
|
30
|
+
}
|
|
31
|
+
cachedMap ??= queryRedirectDocuments(event)
|
|
32
|
+
.then(buildRedirectMap)
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
cachedMap = undefined;
|
|
35
|
+
throw error;
|
|
36
|
+
});
|
|
37
|
+
return cachedMap;
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { localeCodes, localizedPath } from "../../i18n/locales";
|
|
2
|
+
import { routeSlugs } from "../../shared/route-slugs";
|
|
3
|
+
|
|
4
|
+
export interface RedirectSourceDocument {
|
|
5
|
+
route: { resolvedPath: string };
|
|
6
|
+
redirectFrom?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeRedirectPath(path: string): string {
|
|
10
|
+
const trimmed = path.split("?")[0] ?? path;
|
|
11
|
+
if (trimmed === "/") return "/";
|
|
12
|
+
return trimmed.replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Routes the theme itself owns; a redirect must never shadow one of them. */
|
|
16
|
+
export function themeStaticRoutes(): string[] {
|
|
17
|
+
return localeCodes.flatMap((locale) => [
|
|
18
|
+
localizedPath(locale, routeSlugs.home[locale]),
|
|
19
|
+
localizedPath(locale, routeSlugs.docs[locale]),
|
|
20
|
+
localizedPath(locale, routeSlugs.blog[locale]),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds old-path → live-path pairs from every document's `redirectFrom`.
|
|
26
|
+
* Conflicts throw so `failOnError` stops the build instead of shipping a
|
|
27
|
+
* redirect that shadows a live page.
|
|
28
|
+
*/
|
|
29
|
+
export function buildRedirectMap(documents: RedirectSourceDocument[]): Map<string, string> {
|
|
30
|
+
const livePaths = new Set(
|
|
31
|
+
documents.map((document) => normalizeRedirectPath(document.route.resolvedPath)),
|
|
32
|
+
);
|
|
33
|
+
const reserved = new Set(themeStaticRoutes().map(normalizeRedirectPath));
|
|
34
|
+
const map = new Map<string, string>();
|
|
35
|
+
const problems: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const document of documents) {
|
|
38
|
+
const target = normalizeRedirectPath(document.route.resolvedPath);
|
|
39
|
+
for (const source of document.redirectFrom ?? []) {
|
|
40
|
+
const from = normalizeRedirectPath(source);
|
|
41
|
+
if (livePaths.has(from)) {
|
|
42
|
+
problems.push(`"${from}" redirects to "${target}" but is also a live page`);
|
|
43
|
+
} else if (reserved.has(from)) {
|
|
44
|
+
problems.push(`"${from}" redirects to "${target}" but is a theme route`);
|
|
45
|
+
} else if (map.has(from) && map.get(from) !== target) {
|
|
46
|
+
problems.push(`"${from}" is claimed by both "${map.get(from)}" and "${target}"`);
|
|
47
|
+
} else {
|
|
48
|
+
map.set(from, target);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (problems.length) {
|
|
54
|
+
throw new Error(`Invalid redirectFrom entries:\n${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const GINKO_DOCS_NEUTRAL_PALETTES = [
|
|
2
|
+
"slate",
|
|
3
|
+
"gray",
|
|
4
|
+
"zinc",
|
|
5
|
+
"neutral",
|
|
6
|
+
"stone",
|
|
7
|
+
"taupe",
|
|
8
|
+
"mauve",
|
|
9
|
+
"mist",
|
|
10
|
+
"olive",
|
|
11
|
+
"custom",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const GINKO_DOCS_PRIMARY_PALETTES = [
|
|
15
|
+
"neutral",
|
|
16
|
+
"red",
|
|
17
|
+
"orange",
|
|
18
|
+
"amber",
|
|
19
|
+
"yellow",
|
|
20
|
+
"lime",
|
|
21
|
+
"green",
|
|
22
|
+
"emerald",
|
|
23
|
+
"teal",
|
|
24
|
+
"cyan",
|
|
25
|
+
"sky",
|
|
26
|
+
"blue",
|
|
27
|
+
"indigo",
|
|
28
|
+
"violet",
|
|
29
|
+
"purple",
|
|
30
|
+
"fuchsia",
|
|
31
|
+
"pink",
|
|
32
|
+
"rose",
|
|
33
|
+
"custom",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export type GinkoDocsNeutralPalette = (typeof GINKO_DOCS_NEUTRAL_PALETTES)[number];
|
|
37
|
+
export type GinkoDocsPrimaryPalette = (typeof GINKO_DOCS_PRIMARY_PALETTES)[number];
|
|
@@ -1,13 +1,62 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type { GinkoDocsNeutralPalette, GinkoDocsPrimaryPalette } from "../theme-palettes";
|
|
2
|
+
import type { GinkoDocsNeutralPalette, GinkoDocsPrimaryPalette } from "../theme-palettes";
|
|
3
|
+
|
|
4
|
+
export type GinkoDocsLocalizedText = string | { en: string; de?: string };
|
|
5
|
+
|
|
6
|
+
export type GinkoDocsProseAppearance = "quiet" | "tint";
|
|
7
|
+
export type GinkoDocsProseFamily =
|
|
8
|
+
| "callout"
|
|
9
|
+
| "aside"
|
|
10
|
+
| "excerpt"
|
|
11
|
+
| "cards"
|
|
12
|
+
| "readMore"
|
|
13
|
+
| "accordion"
|
|
14
|
+
| "tabs"
|
|
15
|
+
| "code"
|
|
16
|
+
| "files"
|
|
17
|
+
| "api"
|
|
18
|
+
| "figure"
|
|
19
|
+
| "quiz"
|
|
20
|
+
| "steps"
|
|
21
|
+
| "timeline";
|
|
22
|
+
|
|
23
|
+
export interface GinkoDocsProseConfig {
|
|
24
|
+
appearance: GinkoDocsProseAppearance;
|
|
25
|
+
components?: Partial<Record<GinkoDocsProseFamily, GinkoDocsProseAppearance>>;
|
|
26
|
+
}
|
|
2
27
|
|
|
3
28
|
export interface GinkoDocsLink {
|
|
4
29
|
label: GinkoDocsLocalizedText;
|
|
5
30
|
to: GinkoDocsLocalizedText;
|
|
31
|
+
/** Iconify icon shown where nav items render with icons (e.g. mobile menu). */
|
|
32
|
+
icon?: string;
|
|
33
|
+
/** One-line subtitle shown under the label in the mobile menu. */
|
|
34
|
+
description?: GinkoDocsLocalizedText;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type GinkoDocsSocialPlatform = "github" | "discord" | "linkedin";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A URL uses the platform's built-in label and icon. Pass an object to override
|
|
41
|
+
* either — `icon` accepts any Iconify name the consuming app has registered, so
|
|
42
|
+
* sites that ship brand marks are not limited to the layer's bundled icons.
|
|
43
|
+
*/
|
|
44
|
+
export type GinkoDocsSocialEntry = string | { href: string; label?: string; icon?: string };
|
|
45
|
+
|
|
46
|
+
export interface GinkoDocsHeroCodeTab {
|
|
47
|
+
label: GinkoDocsLocalizedText;
|
|
48
|
+
/** Iconify icon shown in the tab. */
|
|
49
|
+
icon?: string;
|
|
50
|
+
filename?: string;
|
|
51
|
+
/** Shiki language id; derived from the filename extension when unset. */
|
|
52
|
+
language?: string;
|
|
53
|
+
code: string;
|
|
6
54
|
}
|
|
7
55
|
|
|
8
56
|
export type GinkoDocsHeroMedia =
|
|
9
57
|
| { type: "image"; src: string; alt: string }
|
|
10
|
-
| { type: "code"; code: string; language?: string; filename?: string }
|
|
58
|
+
| { type: "code"; code: string; language?: string; filename?: string }
|
|
59
|
+
| { type: "code-tabs"; tabs: GinkoDocsHeroCodeTab[] };
|
|
11
60
|
|
|
12
61
|
export type GinkoDocsPlausibleExtension =
|
|
13
62
|
| "hash"
|
|
@@ -21,22 +70,27 @@ export type GinkoDocsPlausibleExtension =
|
|
|
21
70
|
| "manual";
|
|
22
71
|
|
|
23
72
|
export interface GinkoDocsAppConfig {
|
|
73
|
+
theme: {
|
|
74
|
+
neutral: GinkoDocsNeutralPalette;
|
|
75
|
+
primary: GinkoDocsPrimaryPalette;
|
|
76
|
+
};
|
|
77
|
+
prose: GinkoDocsProseConfig;
|
|
24
78
|
site: {
|
|
25
79
|
name: GinkoDocsLocalizedText;
|
|
26
80
|
description: GinkoDocsLocalizedText;
|
|
27
81
|
url: string;
|
|
28
82
|
logo: { light: string; dark: string };
|
|
29
|
-
localeSwitcher: "dropdown" | "segmented";
|
|
30
83
|
docsSidebarSwitcher: "dropdown" | "list" | "tabs";
|
|
31
84
|
lupinumAttribution: boolean;
|
|
32
85
|
};
|
|
33
86
|
nav: {
|
|
34
87
|
/** "auto" derives Docs (+ Blog when blog routes exist); an array overrides entirely. */
|
|
35
88
|
links: "auto" | GinkoDocsLink[];
|
|
89
|
+
/** Render configured social links as icon buttons in the header instead of labelled rows. */
|
|
90
|
+
socialIcons: boolean;
|
|
36
91
|
};
|
|
37
92
|
banner: {
|
|
38
|
-
|
|
39
|
-
enabled: boolean | "auto";
|
|
93
|
+
enabled: boolean;
|
|
40
94
|
/** Dismissal storage key — change it to re-show the banner after an update. */
|
|
41
95
|
id: string;
|
|
42
96
|
text?: GinkoDocsLocalizedText;
|
|
@@ -53,10 +107,7 @@ export interface GinkoDocsAppConfig {
|
|
|
53
107
|
extensions?: GinkoDocsPlausibleExtension[];
|
|
54
108
|
};
|
|
55
109
|
};
|
|
56
|
-
social:
|
|
57
|
-
github?: string;
|
|
58
|
-
linkedin?: string;
|
|
59
|
-
};
|
|
110
|
+
social: Partial<Record<GinkoDocsSocialPlatform, GinkoDocsSocialEntry>>;
|
|
60
111
|
feedback: {
|
|
61
112
|
enabled: boolean;
|
|
62
113
|
};
|
|
@@ -83,7 +134,6 @@ export interface GinkoDocsAppConfig {
|
|
|
83
134
|
contentDirectory?: string;
|
|
84
135
|
};
|
|
85
136
|
landing: {
|
|
86
|
-
eyebrow?: GinkoDocsLocalizedText;
|
|
87
137
|
title: GinkoDocsLocalizedText;
|
|
88
138
|
description: GinkoDocsLocalizedText;
|
|
89
139
|
primary: GinkoDocsLink;
|
|
@@ -91,10 +141,25 @@ export interface GinkoDocsAppConfig {
|
|
|
91
141
|
hero?: {
|
|
92
142
|
media?: GinkoDocsHeroMedia;
|
|
93
143
|
};
|
|
144
|
+
/** Copyable install command shown under the hero CTAs. */
|
|
145
|
+
install?: { command: string };
|
|
94
146
|
features: Array<{
|
|
95
147
|
title: GinkoDocsLocalizedText;
|
|
96
148
|
description: GinkoDocsLocalizedText;
|
|
97
149
|
icon?: string;
|
|
98
150
|
}>;
|
|
151
|
+
/** Dark full-width band highlighting agent readiness. */
|
|
152
|
+
agent?: {
|
|
153
|
+
title: GinkoDocsLocalizedText;
|
|
154
|
+
description: GinkoDocsLocalizedText;
|
|
155
|
+
/** Terminal transcript; "$"-prefixed lines render as commands, "#" as comments. */
|
|
156
|
+
code: string;
|
|
157
|
+
};
|
|
158
|
+
/** Closing call to action; primary falls back to landing.primary. */
|
|
159
|
+
cta?: {
|
|
160
|
+
title: GinkoDocsLocalizedText;
|
|
161
|
+
primary?: GinkoDocsLink;
|
|
162
|
+
secondary?: GinkoDocsLink;
|
|
163
|
+
};
|
|
99
164
|
};
|
|
100
165
|
}
|
package/tags.ts
CHANGED
|
@@ -3,42 +3,36 @@ import type { PortableComponentPolicyV1 } from "@lupinum/ginko-content/cms-contr
|
|
|
3
3
|
export const contentComponentTags = {
|
|
4
4
|
accordion: "MdcAccordion",
|
|
5
5
|
"accordion-item": "MdcAccordionItem",
|
|
6
|
-
|
|
6
|
+
api: "MdcApi",
|
|
7
7
|
aside: "MdcAside",
|
|
8
|
-
callout: "MdcCallout",
|
|
9
8
|
card: "MdcCard",
|
|
10
|
-
|
|
9
|
+
cards: "MdcCards",
|
|
11
10
|
center: "MdcCenter",
|
|
12
11
|
"code-group": "MdcCodeGroup",
|
|
12
|
+
"code-tree": "MdcCodeTree",
|
|
13
|
+
collapse: "MdcCollapse",
|
|
13
14
|
column: "MdcColumn",
|
|
14
|
-
danger: "MdcError",
|
|
15
|
-
"doc-img": "MdcDocImg",
|
|
16
15
|
dropcap: "MdcDropcap",
|
|
17
16
|
error: "MdcError",
|
|
18
|
-
|
|
19
|
-
"field-group": "MdcFieldGroup",
|
|
17
|
+
excerpt: "MdcExcerpt",
|
|
20
18
|
figure: "MdcFigure",
|
|
19
|
+
files: "MdcFiles",
|
|
21
20
|
idea: "MdcIdea",
|
|
22
21
|
img: "ProseImg",
|
|
23
22
|
info: "MdcInfo",
|
|
24
|
-
kbd: "
|
|
23
|
+
kbd: "MdcKbd",
|
|
25
24
|
layout: "MdcLayout",
|
|
26
25
|
note: "MdcNote",
|
|
27
|
-
passage: "MdcPassage",
|
|
28
26
|
quiz: "MdcQuiz",
|
|
29
|
-
"quiz-option": "MdcQuizOption",
|
|
30
27
|
"quiz-question": "MdcQuizQuestion",
|
|
31
28
|
"read-more": "MdcReadMore",
|
|
32
|
-
"read-more-group": "MdcReadMoreGroup",
|
|
33
|
-
shortcut: "MdcShortcut",
|
|
34
|
-
step: "MdcStep",
|
|
35
29
|
steps: "MdcSteps",
|
|
36
30
|
success: "MdcSuccess",
|
|
37
31
|
tab: "MdcTab",
|
|
38
32
|
tabs: "MdcTabs",
|
|
39
33
|
timeline: "MdcTimeline",
|
|
40
34
|
"timeline-item": "MdcTimelineItem",
|
|
41
|
-
|
|
35
|
+
toc: "MdcInlineToc",
|
|
42
36
|
warning: "MdcWarning",
|
|
43
37
|
} as const;
|
|
44
38
|
|
|
@@ -59,12 +53,13 @@ const inline = (props: ComponentProps = {}): ComponentDefinition => ({
|
|
|
59
53
|
media: null,
|
|
60
54
|
});
|
|
61
55
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
56
|
+
const appearance = { appearance: optional("string") };
|
|
57
|
+
const notice = block({ title: optional("string"), icon: optional("string"), ...appearance });
|
|
64
58
|
|
|
65
59
|
export const contentComponentPolicy = {
|
|
66
60
|
components: {
|
|
67
61
|
accordion: block({
|
|
62
|
+
...appearance,
|
|
68
63
|
type: optional("string"),
|
|
69
64
|
collapsible: optional("boolean"),
|
|
70
65
|
defaultValue: optional("json"),
|
|
@@ -77,17 +72,15 @@ export const contentComponentPolicy = {
|
|
|
77
72
|
},
|
|
78
73
|
["default", "title", "content"],
|
|
79
74
|
),
|
|
80
|
-
|
|
75
|
+
api: block({
|
|
76
|
+
...appearance,
|
|
81
77
|
title: optional("string"),
|
|
82
78
|
icon: optional("string"),
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
callout: block({
|
|
87
|
-
title: optional("string"),
|
|
88
|
-
type: optional("string"),
|
|
89
|
-
icon: optional("string"),
|
|
79
|
+
method: optional("string"),
|
|
80
|
+
path: optional("string"),
|
|
81
|
+
groups: optional("json"),
|
|
90
82
|
}),
|
|
83
|
+
aside: block({ label: optional("string"), ...appearance }),
|
|
91
84
|
card: block(
|
|
92
85
|
{
|
|
93
86
|
title: optional("string"),
|
|
@@ -100,32 +93,31 @@ export const contentComponentPolicy = {
|
|
|
100
93
|
img: optional("asset"),
|
|
101
94
|
showLinkIcon: optional("boolean"),
|
|
102
95
|
horizontal: optional("boolean"),
|
|
96
|
+
...appearance,
|
|
103
97
|
},
|
|
104
98
|
["default", "title", "description", "footer"],
|
|
105
99
|
{ sourceProp: "img", altProp: null, titleProp: null, filenameProp: null },
|
|
106
100
|
),
|
|
107
|
-
|
|
101
|
+
cards: block({ cols: optional("string"), ...appearance }),
|
|
108
102
|
center: block({
|
|
109
103
|
size: optional("string"),
|
|
110
104
|
max: optional("string"),
|
|
111
105
|
type: optional("string"),
|
|
112
106
|
}),
|
|
113
|
-
"code-group": block(),
|
|
107
|
+
"code-group": block(appearance),
|
|
108
|
+
"code-tree": block({
|
|
109
|
+
...appearance,
|
|
110
|
+
defaultValue: optional("string"),
|
|
111
|
+
expandAll: optional("boolean"),
|
|
112
|
+
}),
|
|
113
|
+
collapse: block(appearance),
|
|
114
114
|
column: block({ size: optional("string") }),
|
|
115
|
-
danger: notice,
|
|
116
|
-
"doc-img": block({ bleed: optional("string") }),
|
|
117
115
|
dropcap: block({ lines: optional("json") }),
|
|
118
116
|
error: notice,
|
|
119
|
-
|
|
120
|
-
name: required("string"),
|
|
121
|
-
type: optional("string"),
|
|
122
|
-
required: optional("boolean"),
|
|
123
|
-
description: optional("string"),
|
|
124
|
-
defaultValue: optional("string"),
|
|
125
|
-
}),
|
|
126
|
-
"field-group": block(),
|
|
117
|
+
excerpt: block({ label: optional("string"), source: optional("string"), ...appearance }),
|
|
127
118
|
figure: block(
|
|
128
119
|
{
|
|
120
|
+
...appearance,
|
|
129
121
|
src: required("asset"),
|
|
130
122
|
alt: required("string"),
|
|
131
123
|
caption: optional("string"),
|
|
@@ -134,17 +126,19 @@ export const contentComponentPolicy = {
|
|
|
134
126
|
bleed: optional("string"),
|
|
135
127
|
aspect: optional("string"),
|
|
136
128
|
fit: optional("string"),
|
|
129
|
+
zoom: optional("json"),
|
|
137
130
|
},
|
|
138
131
|
["default"],
|
|
139
132
|
{ sourceProp: "src", altProp: "alt", titleProp: null, filenameProp: null },
|
|
140
133
|
),
|
|
134
|
+
files: block({ active: optional("string"), annotations: optional("json"), ...appearance }),
|
|
141
135
|
idea: notice,
|
|
142
136
|
info: notice,
|
|
143
|
-
kbd:
|
|
137
|
+
kbd: inline(),
|
|
144
138
|
layout: block({ type: optional("string") }),
|
|
145
139
|
note: notice,
|
|
146
|
-
passage: block({ label: optional("string") }),
|
|
147
140
|
quiz: block({
|
|
141
|
+
...appearance,
|
|
148
142
|
title: optional("string"),
|
|
149
143
|
description: optional("string"),
|
|
150
144
|
questionLabel: optional("string"),
|
|
@@ -156,44 +150,38 @@ export const contentComponentPolicy = {
|
|
|
156
150
|
retryPrompt: optional("string"),
|
|
157
151
|
resetLabel: optional("string"),
|
|
158
152
|
}),
|
|
159
|
-
"quiz-option": block({
|
|
160
|
-
correct: optional("boolean"),
|
|
161
|
-
label: optional("string"),
|
|
162
|
-
}),
|
|
163
153
|
"quiz-question": block({
|
|
164
154
|
question: required("string"),
|
|
165
155
|
type: optional("string"),
|
|
166
156
|
explanation: optional("string"),
|
|
167
|
-
options:
|
|
157
|
+
options: required("json"),
|
|
168
158
|
checkLabel: optional("string"),
|
|
169
159
|
correctLabel: optional("string"),
|
|
170
160
|
incorrectLabel: optional("string"),
|
|
171
161
|
resetLabel: optional("string"),
|
|
172
162
|
multipleChoiceLabel: optional("string"),
|
|
173
163
|
}),
|
|
174
|
-
"read-more": block(
|
|
175
|
-
|
|
176
|
-
title: optional("string"),
|
|
177
|
-
to: required("string"),
|
|
178
|
-
target: optional("string"),
|
|
179
|
-
icon: optional("string"),
|
|
180
|
-
},
|
|
181
|
-
[],
|
|
182
|
-
),
|
|
183
|
-
"read-more-group": block(),
|
|
184
|
-
shortcut,
|
|
185
|
-
step: block({ title: optional("string"), icon: optional("string") }),
|
|
186
|
-
steps: block({ variant: optional("string") }),
|
|
164
|
+
"read-more": block({ title: optional("string"), links: required("json"), ...appearance }, []),
|
|
165
|
+
steps: block({ mode: optional("string"), ...appearance }),
|
|
187
166
|
success: notice,
|
|
188
167
|
tab: block({ icon: optional("string"), label: optional("string") }),
|
|
189
|
-
tabs: block({
|
|
190
|
-
timeline: block(),
|
|
168
|
+
tabs: block({ layout: optional("string"), padded: optional("boolean"), ...appearance }),
|
|
169
|
+
timeline: block(appearance),
|
|
191
170
|
"timeline-item": block({
|
|
192
171
|
date: optional("string"),
|
|
193
172
|
label: optional("string"),
|
|
194
173
|
title: optional("string"),
|
|
174
|
+
icon: optional("string"),
|
|
175
|
+
active: optional("boolean"),
|
|
195
176
|
}),
|
|
196
|
-
|
|
177
|
+
toc: block(
|
|
178
|
+
{
|
|
179
|
+
title: optional("string"),
|
|
180
|
+
depth: optional("json"),
|
|
181
|
+
open: optional("json"),
|
|
182
|
+
},
|
|
183
|
+
[],
|
|
184
|
+
),
|
|
197
185
|
warning: notice,
|
|
198
186
|
},
|
|
199
187
|
} satisfies PortableComponentPolicyV1;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { HTMLAttributes } from "vue";
|
|
3
|
-
import { computed } from "vue";
|
|
4
|
-
import MdcCallout from "./MdcCallout.vue";
|
|
5
|
-
|
|
6
|
-
type AlertType = "default" | "info" | "warning" | "success" | "danger" | "secondary";
|
|
7
|
-
type CalloutType = "info" | "warning" | "error" | "success" | "idea";
|
|
8
|
-
|
|
9
|
-
const props = withDefaults(
|
|
10
|
-
defineProps<{
|
|
11
|
-
title?: string;
|
|
12
|
-
icon?: string;
|
|
13
|
-
type?: AlertType;
|
|
14
|
-
class?: HTMLAttributes["class"];
|
|
15
|
-
}>(),
|
|
16
|
-
{
|
|
17
|
-
type: "warning",
|
|
18
|
-
},
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const calloutType = computed<CalloutType>(() => {
|
|
22
|
-
if (props.type === "danger") return "error";
|
|
23
|
-
if (props.type === "success") return "success";
|
|
24
|
-
if (props.type === "info" || props.type === "secondary") return "info";
|
|
25
|
-
|
|
26
|
-
return "warning";
|
|
27
|
-
});
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<template>
|
|
31
|
-
<MdcCallout :title="title" :icon="icon" :type="calloutType" :class="props.class">
|
|
32
|
-
<slot />
|
|
33
|
-
</MdcCallout>
|
|
34
|
-
</template>
|