@lupinum/ginko-docs 0.2.2 → 0.2.3
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 +14 -12
- package/app/app.vue +2 -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 +19 -112
- 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 +255 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +22 -4
- 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 +29 -11
- 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 +3 -18
- 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 +84 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +30 -20
- 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 +46 -46
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +16 -8
- package/app/pages/blog/index.vue +26 -13
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +174 -59
- 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 +23 -26
- package/content.ts +30 -28
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +3 -0
- package/i18n/messages/global/nav.ts +6 -2
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +5 -0
- package/index.d.ts +7 -1
- package/nuxt.config.ts +2 -2
- package/package.json +9 -6
- package/runtime/server/agent-markdown.ts +0 -2
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +63 -6
- 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,38 +1,102 @@
|
|
|
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
|
+
}
|
|
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
|
+
}
|
|
36
100
|
|
|
37
101
|
useSeoMeta({
|
|
38
102
|
title: computed(() => localize(config.site.name)),
|
|
@@ -47,9 +111,6 @@ useSeoMeta({
|
|
|
47
111
|
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
112
|
>
|
|
49
113
|
<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
114
|
<h1
|
|
54
115
|
class="max-w-4xl text-5xl leading-[0.98] font-semibold tracking-[-0.035em] text-balance text-foreground sm:text-7xl"
|
|
55
116
|
:class="landing.heroMedia ? 'lg:text-6xl' : 'lg:text-[5.75rem]'"
|
|
@@ -75,66 +136,120 @@ useSeoMeta({
|
|
|
75
136
|
{{ landing.secondary.label }}
|
|
76
137
|
</NuxtLink>
|
|
77
138
|
</div>
|
|
139
|
+
<div v-if="landing.install" class="mt-5">
|
|
140
|
+
<div
|
|
141
|
+
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"
|
|
142
|
+
>
|
|
143
|
+
<span class="text-muted-foreground select-none" aria-hidden="true">$</span>
|
|
144
|
+
<span class="truncate">{{ landing.install.command }}</span>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
class="content-codeblock-copy-button"
|
|
148
|
+
:aria-label="copied ? t('docs.copiedText') : t('docs.copyText')"
|
|
149
|
+
@click="copyInstallCommand"
|
|
150
|
+
>
|
|
151
|
+
<Icon
|
|
152
|
+
:name="copied ? 'lucide:check' : 'lucide:clipboard'"
|
|
153
|
+
class="size-3.5"
|
|
154
|
+
aria-hidden="true"
|
|
155
|
+
/>
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
78
159
|
</div>
|
|
79
160
|
|
|
80
|
-
<div
|
|
161
|
+
<div
|
|
162
|
+
v-if="landing.heroMedia"
|
|
163
|
+
class="w-full min-w-0 max-w-2xl shrink-0 lg:w-[30rem] lg:max-w-full xl:w-[36rem]"
|
|
164
|
+
>
|
|
81
165
|
<NuxtImg
|
|
82
166
|
v-if="landing.heroMedia.type === 'image'"
|
|
83
167
|
:src="landing.heroMedia.src"
|
|
84
168
|
:alt="landing.heroMedia.alt"
|
|
85
169
|
class="w-full rounded-xl border border-border object-cover shadow-sm"
|
|
86
|
-
width="
|
|
170
|
+
width="576"
|
|
87
171
|
loading="lazy"
|
|
88
172
|
/>
|
|
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>
|
|
173
|
+
<SiteHeroCode v-else-if="heroCodeTabs.length" :tabs="heroCodeTabs" />
|
|
113
174
|
</div>
|
|
114
175
|
</div>
|
|
115
176
|
</section>
|
|
116
177
|
|
|
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
|
-
>
|
|
178
|
+
<section v-if="landing.features.length" class="mx-auto max-w-6xl px-5 py-20 sm:px-8 sm:py-24">
|
|
179
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
121
180
|
<article
|
|
122
|
-
v-for="feature in landing.features"
|
|
181
|
+
v-for="(feature, index) in landing.features"
|
|
123
182
|
:key="feature.title"
|
|
124
|
-
class="min-w-0
|
|
183
|
+
class="min-w-0 rounded-lg border border-border bg-card p-6"
|
|
125
184
|
>
|
|
126
|
-
<
|
|
185
|
+
<span
|
|
127
186
|
v-if="feature.icon"
|
|
128
|
-
|
|
129
|
-
class="
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
187
|
+
class="flex size-10 items-center justify-center rounded-md"
|
|
188
|
+
:class="tint(index).tile"
|
|
189
|
+
>
|
|
190
|
+
<Icon
|
|
191
|
+
:name="feature.icon"
|
|
192
|
+
class="size-5"
|
|
193
|
+
:class="tint(index).icon"
|
|
194
|
+
aria-hidden="true"
|
|
195
|
+
/>
|
|
196
|
+
</span>
|
|
197
|
+
<h2 class="mt-5 text-base font-semibold tracking-tight text-foreground">
|
|
133
198
|
{{ feature.title }}
|
|
134
199
|
</h2>
|
|
135
|
-
<p class="mt-
|
|
200
|
+
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ feature.description }}</p>
|
|
136
201
|
</article>
|
|
137
202
|
</div>
|
|
138
203
|
</section>
|
|
204
|
+
|
|
205
|
+
<section v-if="landing.agent" class="border-y border-border bg-agent-background">
|
|
206
|
+
<div
|
|
207
|
+
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"
|
|
208
|
+
>
|
|
209
|
+
<div class="min-w-0">
|
|
210
|
+
<h2
|
|
211
|
+
class="text-3xl font-semibold tracking-[-0.03em] text-balance text-agent-foreground sm:text-4xl"
|
|
212
|
+
>
|
|
213
|
+
{{ landing.agent.title }}
|
|
214
|
+
</h2>
|
|
215
|
+
<p class="mt-4 max-w-xl text-base leading-7 text-agent-muted">
|
|
216
|
+
{{ landing.agent.description }}
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
<pre
|
|
220
|
+
class="min-w-0 overflow-x-auto rounded-lg border border-white/10 bg-black/30 p-5 font-mono text-[13px] leading-7"
|
|
221
|
+
><code><span
|
|
222
|
+
v-for="(line, index) in agentTerminalLines"
|
|
223
|
+
:key="index"
|
|
224
|
+
class="block min-h-[1.75rem] whitespace-pre"
|
|
225
|
+
><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>
|
|
226
|
+
</div>
|
|
227
|
+
</section>
|
|
228
|
+
|
|
229
|
+
<section v-if="landing.cta">
|
|
230
|
+
<div class="mx-auto max-w-6xl px-5 py-20 text-center sm:px-8 sm:py-28">
|
|
231
|
+
<h2
|
|
232
|
+
class="text-3xl font-semibold tracking-[-0.03em] text-balance text-foreground sm:text-5xl"
|
|
233
|
+
>
|
|
234
|
+
{{ landing.cta.title }}
|
|
235
|
+
</h2>
|
|
236
|
+
<div class="mt-9 flex flex-wrap items-center justify-center gap-3">
|
|
237
|
+
<NuxtLink
|
|
238
|
+
:to="landing.cta.primary.to"
|
|
239
|
+
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"
|
|
240
|
+
>
|
|
241
|
+
{{ landing.cta.primary.label }}
|
|
242
|
+
<Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
|
|
243
|
+
</NuxtLink>
|
|
244
|
+
<NuxtLink
|
|
245
|
+
v-if="landing.cta.secondary"
|
|
246
|
+
:to="landing.cta.secondary.to"
|
|
247
|
+
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"
|
|
248
|
+
>
|
|
249
|
+
{{ landing.cta.secondary.label }}
|
|
250
|
+
</NuxtLink>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
139
254
|
</div>
|
|
140
255
|
</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,12 +23,14 @@ const routeSlugs = {
|
|
|
24
23
|
};
|
|
25
24
|
//#endregion
|
|
26
25
|
//#region layer/content.ts
|
|
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);
|
|
27
28
|
const docsSchema = z.object({
|
|
28
29
|
title: z.string(),
|
|
29
30
|
description: z.string(),
|
|
30
31
|
icon: z.string().optional(),
|
|
31
32
|
badge: z.string().optional(),
|
|
32
|
-
updated:
|
|
33
|
+
updated: isoDate.optional(),
|
|
33
34
|
sidebar: z.enum(["section", "group"]).optional(),
|
|
34
35
|
navigation: z
|
|
35
36
|
.object({
|
|
@@ -44,8 +45,8 @@ const blogSchema = z.object({
|
|
|
44
45
|
title: z.string(),
|
|
45
46
|
description: z.string(),
|
|
46
47
|
badge: z.string().optional(),
|
|
47
|
-
date:
|
|
48
|
-
readingTime:
|
|
48
|
+
date: isoDate,
|
|
49
|
+
readingTime: nonEmptyString,
|
|
49
50
|
author: reference("authors"),
|
|
50
51
|
image: z.string().optional(),
|
|
51
52
|
});
|
|
@@ -66,10 +67,13 @@ const authorsSchema = z.object({
|
|
|
66
67
|
});
|
|
67
68
|
function defineGinkoDocsConfig(options) {
|
|
68
69
|
const locales = options.locales ?? ["en"];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
if (
|
|
71
|
+
!(locales.length === 1
|
|
72
|
+
? locales[0] === "en"
|
|
73
|
+
: locales.length === 2 && locales[0] === "en" && locales[1] === "de")
|
|
74
|
+
)
|
|
75
|
+
throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
|
|
76
|
+
const i18n = locales.length === 2;
|
|
73
77
|
const metadata = defineAgentMetadataFields([
|
|
74
78
|
"title",
|
|
75
79
|
"description",
|
|
@@ -85,7 +89,7 @@ function defineGinkoDocsConfig(options) {
|
|
|
85
89
|
type: "page",
|
|
86
90
|
source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
|
|
87
91
|
i18n: i18n ? true : void 0,
|
|
88
|
-
route: i18n ?
|
|
92
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
89
93
|
agent: {
|
|
90
94
|
section: "optional",
|
|
91
95
|
markdown: true,
|
|
@@ -95,9 +99,9 @@ function defineGinkoDocsConfig(options) {
|
|
|
95
99
|
});
|
|
96
100
|
const blog = defineCollection({
|
|
97
101
|
type: "page",
|
|
98
|
-
source: "2.blog
|
|
102
|
+
source: "2.blog/*.md",
|
|
99
103
|
i18n: i18n ? true : void 0,
|
|
100
|
-
route: i18n ?
|
|
104
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
|
|
101
105
|
agent: {
|
|
102
106
|
section: "blog",
|
|
103
107
|
markdown: true,
|
|
@@ -131,30 +135,23 @@ function defineGinkoDocsConfig(options) {
|
|
|
131
135
|
? [
|
|
132
136
|
defineAgentSection({
|
|
133
137
|
id: "blog",
|
|
134
|
-
title:
|
|
138
|
+
title: {
|
|
139
|
+
en: "Blog",
|
|
140
|
+
de: "Blog",
|
|
141
|
+
},
|
|
135
142
|
order: 40,
|
|
136
143
|
}),
|
|
137
144
|
]
|
|
138
145
|
: []),
|
|
139
146
|
defineAgentSection({
|
|
140
147
|
id: "optional",
|
|
141
|
-
title:
|
|
148
|
+
title: {
|
|
149
|
+
en: "Documentation",
|
|
150
|
+
de: "Dokumentation",
|
|
151
|
+
},
|
|
142
152
|
order: 100,
|
|
143
153
|
}),
|
|
144
154
|
],
|
|
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
155
|
},
|
|
159
156
|
};
|
|
160
157
|
if (options.blog)
|
package/content.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
defineAgentAppPage,
|
|
3
2
|
defineAgentMetadataFields,
|
|
4
3
|
defineAgentSection,
|
|
5
4
|
defineCollection,
|
|
@@ -10,10 +9,16 @@ import type { ContentCollectionConfig, ContentConfig } from "@lupinum/ginko-cont
|
|
|
10
9
|
import { z } from "zod";
|
|
11
10
|
import { routeSlugs } from "./shared/route-slugs";
|
|
12
11
|
|
|
12
|
+
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
|
|
13
|
+
const nonEmptyString = z.string().trim().min(1);
|
|
14
|
+
|
|
13
15
|
export interface GinkoDocsContentOptions {
|
|
14
|
-
site: {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
site: {
|
|
17
|
+
name: string | { en: string; de: string };
|
|
18
|
+
description: string | { en: string; de: string };
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
locales?: readonly ["en"] | readonly ["en", "de"];
|
|
17
22
|
blog?: boolean;
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -22,7 +27,7 @@ const docsSchema = z.object({
|
|
|
22
27
|
description: z.string(),
|
|
23
28
|
icon: z.string().optional(),
|
|
24
29
|
badge: z.string().optional(),
|
|
25
|
-
updated:
|
|
30
|
+
updated: isoDate.optional(),
|
|
26
31
|
sidebar: z.enum(["section", "group"]).optional(),
|
|
27
32
|
navigation: z
|
|
28
33
|
.object({
|
|
@@ -37,8 +42,8 @@ const blogSchema = z.object({
|
|
|
37
42
|
title: z.string(),
|
|
38
43
|
description: z.string(),
|
|
39
44
|
badge: z.string().optional(),
|
|
40
|
-
date:
|
|
41
|
-
readingTime:
|
|
45
|
+
date: isoDate,
|
|
46
|
+
readingTime: nonEmptyString,
|
|
42
47
|
author: reference("authors"),
|
|
43
48
|
image: z.string().optional(),
|
|
44
49
|
});
|
|
@@ -69,10 +74,14 @@ export function defineGinkoDocsConfig(
|
|
|
69
74
|
options: GinkoDocsContentOptions,
|
|
70
75
|
): DocsContentConfig | DocsBlogContentConfig {
|
|
71
76
|
const locales = options.locales ?? ["en"];
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const validLocales =
|
|
78
|
+
locales.length === 1
|
|
79
|
+
? locales[0] === "en"
|
|
80
|
+
: locales.length === 2 && locales[0] === "en" && locales[1] === "de";
|
|
81
|
+
if (!validLocales) {
|
|
82
|
+
throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
|
|
83
|
+
}
|
|
84
|
+
const i18n = locales.length === 2;
|
|
76
85
|
const metadata = defineAgentMetadataFields([
|
|
77
86
|
"title",
|
|
78
87
|
"description",
|
|
@@ -88,16 +97,16 @@ export function defineGinkoDocsConfig(
|
|
|
88
97
|
type: "page",
|
|
89
98
|
source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
|
|
90
99
|
i18n: i18n ? true : undefined,
|
|
91
|
-
route: i18n ?
|
|
100
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
92
101
|
agent: { section: "optional", markdown: true },
|
|
93
102
|
strict: true,
|
|
94
103
|
schema: docsSchema,
|
|
95
104
|
});
|
|
96
105
|
const blog = defineCollection({
|
|
97
106
|
type: "page",
|
|
98
|
-
source: "2.blog
|
|
107
|
+
source: "2.blog/*.md",
|
|
99
108
|
i18n: i18n ? true : undefined,
|
|
100
|
-
route: i18n ?
|
|
109
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
|
|
101
110
|
agent: { section: "blog", markdown: true },
|
|
102
111
|
strict: true,
|
|
103
112
|
schema: blogSchema,
|
|
@@ -119,20 +128,13 @@ export function defineGinkoDocsConfig(
|
|
|
119
128
|
},
|
|
120
129
|
markdown: { metadata: { enabled: true, defaultFields: metadata } },
|
|
121
130
|
sections: [
|
|
122
|
-
...(options.blog
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
locales.map((locale) => [locale, locale === defaultLocale ? "/" : `/${locale}`]),
|
|
130
|
-
),
|
|
131
|
-
section: "optional",
|
|
132
|
-
title: options.site.name,
|
|
133
|
-
description: options.site.description,
|
|
134
|
-
metadata,
|
|
135
|
-
render: () => `# ${options.site.name}\n\n> ${options.site.description}`,
|
|
131
|
+
...(options.blog
|
|
132
|
+
? [defineAgentSection({ id: "blog", title: { en: "Blog", de: "Blog" }, order: 40 })]
|
|
133
|
+
: []),
|
|
134
|
+
defineAgentSection({
|
|
135
|
+
id: "optional",
|
|
136
|
+
title: { en: "Documentation", de: "Dokumentation" },
|
|
137
|
+
order: 100,
|
|
136
138
|
}),
|
|
137
139
|
],
|
|
138
140
|
},
|
|
@@ -11,4 +11,8 @@ export const blog = {
|
|
|
11
11
|
toc: { de: "Im Artikel", en: "In this article" },
|
|
12
12
|
readMore: { de: "Weiterlesen", en: "Read more" },
|
|
13
13
|
moreArticles: { de: "Weitere Artikel", en: "More articles" },
|
|
14
|
+
empty: {
|
|
15
|
+
de: "Noch keine Artikel veröffentlicht.",
|
|
16
|
+
en: "No articles have been published yet.",
|
|
17
|
+
},
|
|
14
18
|
} as const;
|