@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
package/app/pages/index.vue
CHANGED
|
@@ -1,42 +1,110 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { useClipboard } from "@vueuse/core";
|
|
3
4
|
import { useI18n, useSeoMeta } from "#imports";
|
|
4
5
|
import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
|
|
5
6
|
import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
|
|
7
|
+
import SiteHeroCode from "#ginko-docs/components/site/SiteHeroCode.vue";
|
|
6
8
|
|
|
7
9
|
const config = useGinkoDocsConfig();
|
|
8
|
-
const { locale } = useI18n();
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const { locale, t } = useI18n();
|
|
11
|
+
|
|
12
|
+
type LocalizableText = string | { en: string; de?: string };
|
|
13
|
+
const localize = (value: LocalizableText) => getLocalizedSiteText(value, locale.value);
|
|
14
|
+
const localizeLink = (link: { label: LocalizableText; to: LocalizableText }) => ({
|
|
15
|
+
label: localize(link.label),
|
|
16
|
+
to: localize(link.to),
|
|
17
|
+
});
|
|
18
|
+
|
|
11
19
|
const landing = computed(() => ({
|
|
12
|
-
eyebrow: config.landing.eyebrow ? localize(config.landing.eyebrow) : undefined,
|
|
13
20
|
title: localize(config.landing.title),
|
|
14
21
|
description: localize(config.landing.description),
|
|
15
|
-
primary:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
secondary: config.landing.secondary
|
|
20
|
-
? {
|
|
21
|
-
label: localize(config.landing.secondary.label),
|
|
22
|
-
to: localize(config.landing.secondary.to),
|
|
23
|
-
}
|
|
24
|
-
: undefined,
|
|
22
|
+
primary: localizeLink(config.landing.primary),
|
|
23
|
+
secondary: config.landing.secondary ? localizeLink(config.landing.secondary) : undefined,
|
|
24
|
+
install: config.landing.install,
|
|
25
25
|
features: config.landing.features.map((feature) => ({
|
|
26
26
|
...feature,
|
|
27
27
|
title: localize(feature.title),
|
|
28
28
|
description: localize(feature.description),
|
|
29
29
|
})),
|
|
30
30
|
heroMedia: config.landing.hero?.media,
|
|
31
|
+
agent: config.landing.agent
|
|
32
|
+
? {
|
|
33
|
+
title: localize(config.landing.agent.title),
|
|
34
|
+
description: localize(config.landing.agent.description),
|
|
35
|
+
code: config.landing.agent.code,
|
|
36
|
+
}
|
|
37
|
+
: undefined,
|
|
38
|
+
cta: config.landing.cta
|
|
39
|
+
? {
|
|
40
|
+
title: localize(config.landing.cta.title),
|
|
41
|
+
primary: config.landing.cta.primary
|
|
42
|
+
? localizeLink(config.landing.cta.primary)
|
|
43
|
+
: localizeLink(config.landing.primary),
|
|
44
|
+
secondary: config.landing.cta.secondary
|
|
45
|
+
? localizeLink(config.landing.cta.secondary)
|
|
46
|
+
: undefined,
|
|
47
|
+
}
|
|
48
|
+
: undefined,
|
|
31
49
|
}));
|
|
32
50
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
const heroCodeTabs = computed(() => {
|
|
52
|
+
const media = landing.value.heroMedia;
|
|
53
|
+
|
|
54
|
+
if (media?.type === "code-tabs") {
|
|
55
|
+
return media.tabs.map((tab) => ({ ...tab, label: localize(tab.label) }));
|
|
56
|
+
}
|
|
36
57
|
|
|
58
|
+
if (media?.type === "code") {
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
label: media.filename ?? media.language ?? "Code",
|
|
62
|
+
filename: media.filename,
|
|
63
|
+
language: media.language,
|
|
64
|
+
code: media.code,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Feature icon tiles cycle through the theme-adaptive hero accent pairs.
|
|
73
|
+
const FEATURE_TINTS = [
|
|
74
|
+
{ tile: "bg-(--hero-blue-muted)", icon: "text-(--hero-blue-dark)" },
|
|
75
|
+
{ tile: "bg-(--hero-mint-muted)", icon: "text-(--hero-mint-text)" },
|
|
76
|
+
{ tile: "bg-(--hero-yellow-muted)", icon: "text-(--hero-yellow-dark)" },
|
|
77
|
+
{ tile: "bg-(--hero-coral-muted)", icon: "text-(--hero-coral-text)" },
|
|
78
|
+
];
|
|
79
|
+
const tint = (index: number) => FEATURE_TINTS[index % FEATURE_TINTS.length]!;
|
|
80
|
+
|
|
81
|
+
type TerminalLine = { kind: "command" | "comment" | "output"; text: string };
|
|
82
|
+
const agentTerminalLines = computed<TerminalLine[]>(() => {
|
|
83
|
+
const code = landing.value.agent?.code;
|
|
84
|
+
if (!code) return [];
|
|
85
|
+
|
|
86
|
+
return code.split("\n").map((line) => ({
|
|
87
|
+
kind: line.startsWith("$")
|
|
88
|
+
? "command"
|
|
89
|
+
: line.trimStart().startsWith("#")
|
|
90
|
+
? "comment"
|
|
91
|
+
: "output",
|
|
92
|
+
text: line,
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { copy, copied } = useClipboard();
|
|
97
|
+
async function copyInstallCommand() {
|
|
98
|
+
await copy(landing.value.install?.command ?? "");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const landingTitle = computed(() => localize(config.site.name));
|
|
102
|
+
const landingDescription = computed(() => localize(config.site.description));
|
|
37
103
|
useSeoMeta({
|
|
38
|
-
title:
|
|
39
|
-
description:
|
|
104
|
+
title: landingTitle,
|
|
105
|
+
description: landingDescription,
|
|
106
|
+
ogTitle: landingTitle,
|
|
107
|
+
ogDescription: landingDescription,
|
|
40
108
|
});
|
|
41
109
|
</script>
|
|
42
110
|
|
|
@@ -47,9 +115,6 @@ useSeoMeta({
|
|
|
47
115
|
class="relative mx-auto flex max-w-6xl flex-col gap-14 px-5 py-24 sm:px-8 sm:py-32 lg:flex-row lg:items-center"
|
|
48
116
|
>
|
|
49
117
|
<div class="min-w-0 flex-1">
|
|
50
|
-
<p v-if="landing.eyebrow" class="mb-6 text-sm font-semibold tracking-wide text-primary">
|
|
51
|
-
{{ landing.eyebrow }}
|
|
52
|
-
</p>
|
|
53
118
|
<h1
|
|
54
119
|
class="max-w-4xl text-5xl leading-[0.98] font-semibold tracking-[-0.035em] text-balance text-foreground sm:text-7xl"
|
|
55
120
|
:class="landing.heroMedia ? 'lg:text-6xl' : 'lg:text-[5.75rem]'"
|
|
@@ -75,66 +140,120 @@ useSeoMeta({
|
|
|
75
140
|
{{ landing.secondary.label }}
|
|
76
141
|
</NuxtLink>
|
|
77
142
|
</div>
|
|
143
|
+
<div v-if="landing.install" class="mt-5">
|
|
144
|
+
<div
|
|
145
|
+
class="inline-flex h-10 max-w-full items-center gap-3 rounded-md border border-border bg-muted/40 pr-1.5 pl-4 font-mono text-[13px] text-foreground/90"
|
|
146
|
+
>
|
|
147
|
+
<span class="text-muted-foreground select-none" aria-hidden="true">$</span>
|
|
148
|
+
<span class="truncate">{{ landing.install.command }}</span>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
class="content-codeblock-copy-button"
|
|
152
|
+
:aria-label="copied ? t('docs.copiedText') : t('docs.copyText')"
|
|
153
|
+
@click="copyInstallCommand"
|
|
154
|
+
>
|
|
155
|
+
<Icon
|
|
156
|
+
:name="copied ? 'lucide:check' : 'lucide:clipboard'"
|
|
157
|
+
class="size-3.5"
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
/>
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
78
163
|
</div>
|
|
79
164
|
|
|
80
|
-
<div
|
|
165
|
+
<div
|
|
166
|
+
v-if="landing.heroMedia"
|
|
167
|
+
class="w-full min-w-0 max-w-2xl shrink-0 lg:w-[30rem] lg:max-w-full xl:w-[36rem]"
|
|
168
|
+
>
|
|
81
169
|
<NuxtImg
|
|
82
170
|
v-if="landing.heroMedia.type === 'image'"
|
|
83
171
|
:src="landing.heroMedia.src"
|
|
84
172
|
:alt="landing.heroMedia.alt"
|
|
85
173
|
class="w-full rounded-xl border border-border object-cover shadow-sm"
|
|
86
|
-
width="
|
|
174
|
+
width="576"
|
|
87
175
|
loading="lazy"
|
|
88
176
|
/>
|
|
89
|
-
<
|
|
90
|
-
v-else-if="landing.heroMedia.type === 'code'"
|
|
91
|
-
class="overflow-hidden rounded-xl border border-border bg-card shadow-sm"
|
|
92
|
-
>
|
|
93
|
-
<div
|
|
94
|
-
v-if="landing.heroMedia.filename"
|
|
95
|
-
class="flex h-9 items-center gap-2 border-b border-border bg-muted/50 px-4"
|
|
96
|
-
>
|
|
97
|
-
<span class="flex gap-1.5" aria-hidden="true">
|
|
98
|
-
<span class="size-2.5 rounded-full bg-border" />
|
|
99
|
-
<span class="size-2.5 rounded-full bg-border" />
|
|
100
|
-
<span class="size-2.5 rounded-full bg-border" />
|
|
101
|
-
</span>
|
|
102
|
-
<span class="text-xs font-medium text-muted-foreground">
|
|
103
|
-
{{ landing.heroMedia.filename }}
|
|
104
|
-
</span>
|
|
105
|
-
</div>
|
|
106
|
-
<pre
|
|
107
|
-
role="region"
|
|
108
|
-
tabindex="0"
|
|
109
|
-
:aria-label="landing.heroMedia.filename ?? 'Code'"
|
|
110
|
-
class="overflow-x-auto p-4 font-mono text-[13px] leading-6 text-foreground/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
|
111
|
-
><code><span v-for="(line, index) in heroCodeLines" :key="index" class="block">{{ line }}</span></code></pre>
|
|
112
|
-
</div>
|
|
177
|
+
<SiteHeroCode v-else-if="heroCodeTabs.length" :tabs="heroCodeTabs" />
|
|
113
178
|
</div>
|
|
114
179
|
</div>
|
|
115
180
|
</section>
|
|
116
181
|
|
|
117
|
-
<section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-
|
|
118
|
-
<div
|
|
119
|
-
class="grid gap-12 border-t border-border pt-12 md:grid-cols-3 md:gap-0 md:divide-x md:divide-border"
|
|
120
|
-
>
|
|
182
|
+
<section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-24">
|
|
183
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
121
184
|
<article
|
|
122
|
-
v-for="feature in landing.features"
|
|
185
|
+
v-for="(feature, index) in landing.features"
|
|
123
186
|
:key="feature.title"
|
|
124
|
-
class="min-w-0
|
|
187
|
+
class="min-w-0 rounded-lg border border-border bg-card p-6"
|
|
125
188
|
>
|
|
126
|
-
<
|
|
189
|
+
<span
|
|
127
190
|
v-if="feature.icon"
|
|
128
|
-
|
|
129
|
-
class="
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
191
|
+
class="flex size-10 items-center justify-center rounded-md"
|
|
192
|
+
:class="tint(index).tile"
|
|
193
|
+
>
|
|
194
|
+
<Icon
|
|
195
|
+
:name="feature.icon"
|
|
196
|
+
class="size-5"
|
|
197
|
+
:class="tint(index).icon"
|
|
198
|
+
aria-hidden="true"
|
|
199
|
+
/>
|
|
200
|
+
</span>
|
|
201
|
+
<h2 class="mt-5 text-base font-semibold tracking-tight text-foreground">
|
|
133
202
|
{{ feature.title }}
|
|
134
203
|
</h2>
|
|
135
|
-
<p class="mt-
|
|
204
|
+
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ feature.description }}</p>
|
|
136
205
|
</article>
|
|
137
206
|
</div>
|
|
138
207
|
</section>
|
|
208
|
+
|
|
209
|
+
<section v-if="landing.agent" class="border-y border-border bg-agent-background">
|
|
210
|
+
<div
|
|
211
|
+
class="mx-auto grid max-w-6xl items-center gap-10 px-5 py-16 sm:px-8 sm:py-20 lg:grid-cols-2 lg:gap-16"
|
|
212
|
+
>
|
|
213
|
+
<div class="min-w-0">
|
|
214
|
+
<h2
|
|
215
|
+
class="text-3xl font-semibold tracking-[-0.03em] text-balance text-agent-foreground sm:text-4xl"
|
|
216
|
+
>
|
|
217
|
+
{{ landing.agent.title }}
|
|
218
|
+
</h2>
|
|
219
|
+
<p class="mt-4 max-w-xl text-base leading-7 text-agent-muted">
|
|
220
|
+
{{ landing.agent.description }}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<pre
|
|
224
|
+
class="min-w-0 overflow-x-auto rounded-lg border border-white/10 bg-black/30 p-5 font-mono text-[13px] leading-7"
|
|
225
|
+
><code><span
|
|
226
|
+
v-for="(line, index) in agentTerminalLines"
|
|
227
|
+
:key="index"
|
|
228
|
+
class="block min-h-[1.75rem] whitespace-pre"
|
|
229
|
+
><template v-if="line.kind === 'command'"><span class="text-agent-muted/70 select-none">$</span><span class="text-agent-foreground">{{ line.text.slice(1) }}</span></template><span v-else-if="line.kind === 'comment'" class="text-emerald-300/90">{{ line.text }}</span><span v-else class="text-agent-muted">{{ line.text }}</span></span></code></pre>
|
|
230
|
+
</div>
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
<section v-if="landing.cta">
|
|
234
|
+
<div class="mx-auto max-w-6xl px-5 py-20 text-center sm:px-8 sm:py-28">
|
|
235
|
+
<h2
|
|
236
|
+
class="text-3xl font-semibold tracking-[-0.03em] text-balance text-foreground sm:text-5xl"
|
|
237
|
+
>
|
|
238
|
+
{{ landing.cta.title }}
|
|
239
|
+
</h2>
|
|
240
|
+
<div class="mt-9 flex flex-wrap items-center justify-center gap-3">
|
|
241
|
+
<NuxtLink
|
|
242
|
+
:to="landing.cta.primary.to"
|
|
243
|
+
class="inline-flex h-11 items-center gap-2 rounded-md bg-primary px-5 text-sm font-semibold text-primary-foreground transition-transform hover:-translate-y-0.5"
|
|
244
|
+
>
|
|
245
|
+
{{ landing.cta.primary.label }}
|
|
246
|
+
<Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
|
|
247
|
+
</NuxtLink>
|
|
248
|
+
<NuxtLink
|
|
249
|
+
v-if="landing.cta.secondary"
|
|
250
|
+
:to="landing.cta.secondary.to"
|
|
251
|
+
class="inline-flex h-11 items-center gap-2 rounded-md border border-border bg-background px-5 text-sm font-semibold text-foreground transition-colors hover:bg-muted"
|
|
252
|
+
>
|
|
253
|
+
{{ landing.cta.secondary.label }}
|
|
254
|
+
</NuxtLink>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</section>
|
|
139
258
|
</div>
|
|
140
259
|
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useAppConfig, useHead } from "#imports";
|
|
2
|
+
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
const { theme } = useAppConfig().ginkoDocs;
|
|
5
|
+
|
|
6
|
+
useHead({
|
|
7
|
+
htmlAttrs: {
|
|
8
|
+
"data-neutral": theme.neutral,
|
|
9
|
+
"data-primary": theme.primary,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
});
|
package/app/utils/content.ts
CHANGED
|
@@ -12,6 +12,17 @@ export type FlatTocItem = {
|
|
|
12
12
|
depth?: number;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export function getMarkdownTocLinks(body: unknown): TocLinkLike[] | undefined {
|
|
16
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
|
17
|
+
|
|
18
|
+
const root = body as { type?: unknown; children?: unknown; toc?: unknown };
|
|
19
|
+
if (root.type !== "root" || !Array.isArray(root.children)) return undefined;
|
|
20
|
+
if (!root.toc || typeof root.toc !== "object" || Array.isArray(root.toc)) return undefined;
|
|
21
|
+
|
|
22
|
+
const links = (root.toc as { links?: unknown }).links;
|
|
23
|
+
return Array.isArray(links) ? (links as TocLinkLike[]) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export function formatContentDate(
|
|
16
27
|
value?: string | Date | null,
|
|
17
28
|
locale: string = defaultLocale,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const FALLBACK_FILE_ICON = "lucide:file";
|
|
2
|
+
|
|
3
|
+
export const FILENAME_ICONS: Record<string, string> = {
|
|
4
|
+
".env": "lucide:file-key",
|
|
5
|
+
".env.example": "lucide:file-key",
|
|
6
|
+
".gitignore": "lucide:git-branch",
|
|
7
|
+
"components.json": "lucide:blocks",
|
|
8
|
+
"eslint.config.cjs": "lucide:badge-check",
|
|
9
|
+
"eslint.config.js": "lucide:badge-check",
|
|
10
|
+
"eslint.config.mjs": "lucide:badge-check",
|
|
11
|
+
"nuxt.config.js": "logos:nuxt-icon",
|
|
12
|
+
"nuxt.config.ts": "logos:nuxt-icon",
|
|
13
|
+
"package.json": "lucide:package",
|
|
14
|
+
"pnpm-lock.yaml": "lucide:package",
|
|
15
|
+
"pnpm-workspace.yaml": "lucide:package",
|
|
16
|
+
"tailwind.config.js": "lucide:palette",
|
|
17
|
+
"tailwind.config.ts": "lucide:palette",
|
|
18
|
+
"tsconfig.json": "lucide:file-cog",
|
|
19
|
+
"vite.config.js": "lucide:zap",
|
|
20
|
+
"vite.config.ts": "lucide:zap",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const EXTENSION_ICONS: Record<string, string> = {
|
|
24
|
+
bash: "lucide:terminal",
|
|
25
|
+
cjs: "lucide:file-json",
|
|
26
|
+
css: "lucide:braces",
|
|
27
|
+
csv: "lucide:table",
|
|
28
|
+
htm: "lucide:code-xml",
|
|
29
|
+
html: "lucide:code-xml",
|
|
30
|
+
js: "lucide:file-json",
|
|
31
|
+
json: "lucide:braces",
|
|
32
|
+
jsx: "lucide:atom",
|
|
33
|
+
md: "lucide:file-text",
|
|
34
|
+
mdc: "lucide:file-text",
|
|
35
|
+
mjs: "lucide:file-json",
|
|
36
|
+
mts: "lucide:file-code",
|
|
37
|
+
scss: "lucide:braces",
|
|
38
|
+
sh: "lucide:terminal",
|
|
39
|
+
ts: "logos:typescript-icon",
|
|
40
|
+
tsx: "lucide:atom",
|
|
41
|
+
txt: "lucide:file-text",
|
|
42
|
+
vue: "logos:vue",
|
|
43
|
+
yaml: "lucide:file-cog",
|
|
44
|
+
yml: "lucide:file-cog",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const LANGUAGE_ICONS: Record<string, string> = {
|
|
48
|
+
bash: "lucide:terminal",
|
|
49
|
+
css: "lucide:braces",
|
|
50
|
+
html: "lucide:code-xml",
|
|
51
|
+
javascript: "lucide:file-json",
|
|
52
|
+
js: "lucide:file-json",
|
|
53
|
+
json: "lucide:braces",
|
|
54
|
+
jsx: "lucide:atom",
|
|
55
|
+
markdown: "lucide:file-text",
|
|
56
|
+
mdc: "lucide:file-text",
|
|
57
|
+
shell: "lucide:terminal",
|
|
58
|
+
sh: "lucide:terminal",
|
|
59
|
+
ts: "logos:typescript-icon",
|
|
60
|
+
tsx: "lucide:atom",
|
|
61
|
+
typescript: "logos:typescript-icon",
|
|
62
|
+
vue: "logos:vue",
|
|
63
|
+
"vue-html": "logos:vue",
|
|
64
|
+
yaml: "lucide:file-cog",
|
|
65
|
+
yml: "lucide:file-cog",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function normalizedFilename(filename: string | null | undefined): string | null {
|
|
69
|
+
return filename?.trim().replace(/^\.\//, "").toLowerCase() || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filenameExtension(filename: string | null | undefined): string | null {
|
|
73
|
+
const normalized = normalizedFilename(filename);
|
|
74
|
+
const basename = normalized?.split("/").pop();
|
|
75
|
+
const index = basename?.lastIndexOf(".") ?? -1;
|
|
76
|
+
|
|
77
|
+
if (!basename || index < 0 || index === basename.length - 1) return null;
|
|
78
|
+
return basename.slice(index + 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Best-effort icon for a file, by exact name first, then extension. */
|
|
82
|
+
export function resolveFileIcon(filename: string | null | undefined): string {
|
|
83
|
+
const normalized = normalizedFilename(filename);
|
|
84
|
+
if (normalized && FILENAME_ICONS[normalized]) return FILENAME_ICONS[normalized];
|
|
85
|
+
|
|
86
|
+
const extension = filenameExtension(filename);
|
|
87
|
+
if (extension && EXTENSION_ICONS[extension]) return EXTENSION_ICONS[extension];
|
|
88
|
+
|
|
89
|
+
return FALLBACK_FILE_ICON;
|
|
90
|
+
}
|
package/app/utils/index.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GinkoDocsAppConfig } from "../../shared/types/app-config";
|
|
2
|
+
|
|
3
|
+
type RepositoryConfig = NonNullable<GinkoDocsAppConfig["repository"]>;
|
|
4
|
+
|
|
5
|
+
const repoBase = (repository: RepositoryConfig) => repository.url.replace(/\/$/, "");
|
|
6
|
+
|
|
7
|
+
export function buildRepoIssueUrl(
|
|
8
|
+
repository: RepositoryConfig,
|
|
9
|
+
params: { title: string; body: string },
|
|
10
|
+
): string {
|
|
11
|
+
const url = new URL(`${repoBase(repository)}/issues/new`);
|
|
12
|
+
url.searchParams.set("title", params.title);
|
|
13
|
+
url.searchParams.set("body", params.body);
|
|
14
|
+
return url.toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildRepoEditUrl(repository: RepositoryConfig, sourcePath: string): string {
|
|
18
|
+
const branch = repository.branch || "main";
|
|
19
|
+
return `${repoBase(repository)}/edit/${encodeURIComponent(branch)}/${sourcePath}`;
|
|
20
|
+
}
|
package/content.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
defineAgentAppPage,
|
|
3
2
|
defineAgentMetadataFields,
|
|
4
3
|
defineAgentSection,
|
|
5
4
|
defineCollection,
|
|
@@ -24,31 +23,54 @@ const routeSlugs = {
|
|
|
24
23
|
};
|
|
25
24
|
//#endregion
|
|
26
25
|
//#region layer/content.ts
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
26
|
+
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
|
|
27
|
+
const nonEmptyString = z.string().trim().min(1);
|
|
28
|
+
/** Former public URLs of a page, as served: locale prefix and translated slugs included. */
|
|
29
|
+
const redirectFrom = z
|
|
30
|
+
.array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
|
|
31
|
+
.optional();
|
|
32
|
+
/**
|
|
33
|
+
* Derives the sitemap lastmod from the authored date so it has one source of
|
|
34
|
+
* truth. Route records require normalized UTC ISO values.
|
|
35
|
+
*/
|
|
36
|
+
const withSitemapLastmod = (data, lastmod) =>
|
|
37
|
+
lastmod
|
|
38
|
+
? {
|
|
39
|
+
...data,
|
|
40
|
+
sitemap: { lastmod: `${lastmod}T00:00:00.000Z` },
|
|
41
|
+
}
|
|
42
|
+
: data;
|
|
43
|
+
const docsSchemaWithLastmod = z
|
|
44
|
+
.object({
|
|
45
|
+
title: z.string(),
|
|
46
|
+
description: z.string(),
|
|
47
|
+
icon: z.string().optional(),
|
|
48
|
+
badge: z.string().optional(),
|
|
49
|
+
updated: isoDate.optional(),
|
|
50
|
+
redirectFrom,
|
|
51
|
+
sidebar: z.enum(["section", "group"]).optional(),
|
|
52
|
+
navigation: z
|
|
53
|
+
.object({
|
|
54
|
+
title: z.string().optional(),
|
|
55
|
+
icon: z.string().optional(),
|
|
56
|
+
badge: z.string().optional(),
|
|
57
|
+
sidebar: z.enum(["section", "group"]).optional(),
|
|
58
|
+
})
|
|
59
|
+
.optional(),
|
|
60
|
+
})
|
|
61
|
+
.transform((data) => withSitemapLastmod(data, data.updated));
|
|
62
|
+
const blogSchemaWithLastmod = z
|
|
63
|
+
.object({
|
|
64
|
+
title: z.string(),
|
|
65
|
+
description: z.string(),
|
|
66
|
+
badge: z.string().optional(),
|
|
67
|
+
date: isoDate,
|
|
68
|
+
readingTime: nonEmptyString,
|
|
69
|
+
author: reference("authors"),
|
|
70
|
+
image: z.string().optional(),
|
|
71
|
+
redirectFrom,
|
|
72
|
+
})
|
|
73
|
+
.transform((data) => withSitemapLastmod(data, data.date));
|
|
52
74
|
const authorsSchema = z.object({
|
|
53
75
|
slug: z.string(),
|
|
54
76
|
name: z.string(),
|
|
@@ -66,10 +88,13 @@ const authorsSchema = z.object({
|
|
|
66
88
|
});
|
|
67
89
|
function defineGinkoDocsConfig(options) {
|
|
68
90
|
const locales = options.locales ?? ["en"];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
if (
|
|
92
|
+
!(locales.length === 1
|
|
93
|
+
? locales[0] === "en"
|
|
94
|
+
: locales.length === 2 && locales[0] === "en" && locales[1] === "de")
|
|
95
|
+
)
|
|
96
|
+
throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
|
|
97
|
+
const i18n = locales.length === 2;
|
|
73
98
|
const metadata = defineAgentMetadataFields([
|
|
74
99
|
"title",
|
|
75
100
|
"description",
|
|
@@ -85,25 +110,25 @@ function defineGinkoDocsConfig(options) {
|
|
|
85
110
|
type: "page",
|
|
86
111
|
source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
|
|
87
112
|
i18n: i18n ? true : void 0,
|
|
88
|
-
route: i18n ?
|
|
113
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
89
114
|
agent: {
|
|
90
115
|
section: "optional",
|
|
91
116
|
markdown: true,
|
|
92
117
|
},
|
|
93
118
|
strict: true,
|
|
94
|
-
schema:
|
|
119
|
+
schema: docsSchemaWithLastmod,
|
|
95
120
|
});
|
|
96
121
|
const blog = defineCollection({
|
|
97
122
|
type: "page",
|
|
98
|
-
source: "2.blog
|
|
123
|
+
source: "2.blog/*.md",
|
|
99
124
|
i18n: i18n ? true : void 0,
|
|
100
|
-
route: i18n ?
|
|
125
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
|
|
101
126
|
agent: {
|
|
102
127
|
section: "blog",
|
|
103
128
|
markdown: true,
|
|
104
129
|
},
|
|
105
130
|
strict: true,
|
|
106
|
-
schema:
|
|
131
|
+
schema: blogSchemaWithLastmod,
|
|
107
132
|
});
|
|
108
133
|
const authors = defineCollection({
|
|
109
134
|
type: "data",
|
|
@@ -131,30 +156,23 @@ function defineGinkoDocsConfig(options) {
|
|
|
131
156
|
? [
|
|
132
157
|
defineAgentSection({
|
|
133
158
|
id: "blog",
|
|
134
|
-
title:
|
|
159
|
+
title: {
|
|
160
|
+
en: "Blog",
|
|
161
|
+
de: "Blog",
|
|
162
|
+
},
|
|
135
163
|
order: 40,
|
|
136
164
|
}),
|
|
137
165
|
]
|
|
138
166
|
: []),
|
|
139
167
|
defineAgentSection({
|
|
140
168
|
id: "optional",
|
|
141
|
-
title:
|
|
169
|
+
title: {
|
|
170
|
+
en: "Documentation",
|
|
171
|
+
de: "Dokumentation",
|
|
172
|
+
},
|
|
142
173
|
order: 100,
|
|
143
174
|
}),
|
|
144
175
|
],
|
|
145
|
-
pages: [
|
|
146
|
-
defineAgentAppPage({
|
|
147
|
-
id: "home",
|
|
148
|
-
route: Object.fromEntries(
|
|
149
|
-
locales.map((locale) => [locale, locale === defaultLocale ? "/" : `/${locale}`]),
|
|
150
|
-
),
|
|
151
|
-
section: "optional",
|
|
152
|
-
title: options.site.name,
|
|
153
|
-
description: options.site.description,
|
|
154
|
-
metadata,
|
|
155
|
-
render: () => `# ${options.site.name}\n\n> ${options.site.description}`,
|
|
156
|
-
}),
|
|
157
|
-
],
|
|
158
176
|
},
|
|
159
177
|
};
|
|
160
178
|
if (options.blog)
|