@lupinum/ginko-docs 0.3.0 → 0.4.0-rc.10
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 +12 -4
- package/app/app.config.ts +2 -0
- package/app/app.vue +26 -2
- package/app/assets/css/tailwind.css +24 -0
- package/app/assets/css/theme-presets.css +141 -0
- package/app/components/OgImage/GinkoDocs.satori.vue +131 -10
- package/app/components/site/SiteBanner.vue +2 -2
- package/app/components/site/SiteFooter.vue +1 -1
- package/app/components/site/SiteHeader.vue +1 -1
- package/app/components/site/SiteSocialLinks.vue +9 -1
- package/app/components/ui/badge/index.ts +1 -1
- package/app/components/ui/button/index.ts +1 -1
- package/app/composables/site-navigation.utils.ts +3 -7
- package/app/composables/useLocalizedPath.ts +3 -2
- package/app/error.vue +18 -8
- package/app/features/docs/components/DocsPageContent.vue +2 -2
- package/app/features/docs/components/DocsSidebarItem.vue +2 -2
- package/app/features/docs/components/DocsSidebarList.vue +1 -1
- package/app/features/docs/components/DocsToc.vue +2 -2
- package/app/features/search/command-center.ts +33 -0
- package/app/features/search/components/SiteCommandCenter.vue +25 -9
- package/app/features/search/components/SiteSearchPageHighlight.vue +4 -2
- package/app/features/search/useCommandCenter.ts +2 -2
- package/app/layouts/docs.vue +1 -1
- package/app/pages/index.vue +22 -3
- package/app/plugins/ginko-docs-theme.ts +1 -0
- package/content-collections.ts +13 -5
- package/content.js +16 -10
- package/content.ts +25 -8
- package/i18n/messages/global/command.ts +1 -0
- package/icon-bundle.ts +24 -4
- package/index.d.ts +1 -0
- package/modules/feature-routing.ts +47 -22
- package/modules/syntax-highlighting.ts +5 -0
- package/nuxt.config.ts +7 -7
- package/package.json +9 -4
- package/runtime/generate-favicons.mjs +124 -0
- package/server/plugins/sitemap-policy.ts +19 -0
- package/server/routes/.well-known/ai-catalog.json.get.ts +10 -0
- package/server/routes/[locale]/blog/rss.xml.get.ts +6 -1
- package/server/routes/blog/rss.xml.get.ts +10 -2
- package/server/routes/mcp/server-card.get.ts +14 -0
- package/server/utils/blog-feed.ts +24 -6
- package/server/utils/mcp-server-card.ts +45 -0
- package/server/utils/redirects.ts +10 -3
- package/server/utils/redirects.utils.ts +10 -7
- package/server/utils/sitemap.ts +50 -0
- package/shared/theme-presets.ts +3 -0
- package/shared/types/app-config.ts +20 -2
- package/shared/types/nuxt-config.ts +3 -0
package/app/error.vue
CHANGED
|
@@ -3,8 +3,8 @@ import { localizedRoutes } from "../i18n/routes";
|
|
|
3
3
|
import { messages } from "../i18n/messages/index";
|
|
4
4
|
import { normalizeAppError } from "./lib/errors";
|
|
5
5
|
import { computed } from "vue";
|
|
6
|
-
import { clearError, useHead } from "#imports";
|
|
7
|
-
import { localeFromPath, localizedPath } from "../i18n/locales";
|
|
6
|
+
import { clearError, useHead, useRequestURL, useRuntimeConfig } from "#imports";
|
|
7
|
+
import { defaultLocale, isLocaleCode, localeFromPath, localizedPath } from "../i18n/locales";
|
|
8
8
|
|
|
9
9
|
type NuxtErrorLike = {
|
|
10
10
|
message?: string;
|
|
@@ -19,17 +19,26 @@ const props = defineProps<{
|
|
|
19
19
|
error: NuxtErrorLike;
|
|
20
20
|
}>();
|
|
21
21
|
|
|
22
|
+
const configuredPrimaryLocale = useRuntimeConfig().public.ginkoDocs?.primaryLocale;
|
|
23
|
+
const primaryLocale =
|
|
24
|
+
typeof configuredPrimaryLocale === "string" && isLocaleCode(configuredPrimaryLocale)
|
|
25
|
+
? configuredPrimaryLocale
|
|
26
|
+
: defaultLocale;
|
|
27
|
+
|
|
22
28
|
const normalized = computed(() => normalizeAppError(props.error));
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
const locale = computed(() => localeFromPath(requestPath.value));
|
|
29
|
+
const requestUrl = useRequestURL();
|
|
30
|
+
const requestPath = computed(() => props.error?.url || requestUrl.pathname);
|
|
31
|
+
const locale = computed(() => localeFromPath(requestPath.value, primaryLocale));
|
|
27
32
|
const copy = computed(() => {
|
|
28
33
|
const catalog = messages[locale.value].errors;
|
|
29
34
|
return catalog[normalized.value.kind] ?? catalog.unavailable;
|
|
30
35
|
});
|
|
31
|
-
const homePath = computed(() =>
|
|
32
|
-
|
|
36
|
+
const homePath = computed(() =>
|
|
37
|
+
localizedPath(locale.value, localizedRoutes[locale.value].home, primaryLocale),
|
|
38
|
+
);
|
|
39
|
+
const docsPath = computed(() =>
|
|
40
|
+
localizedPath(locale.value, localizedRoutes[locale.value].docs, primaryLocale),
|
|
41
|
+
);
|
|
33
42
|
const title = computed(() => `${normalized.value.statusCode} - ${copy.value.title}`);
|
|
34
43
|
// Keep the full site chrome for content-level errors (404 and friends); hard
|
|
35
44
|
// server failures fall back to the bare page so the error screen cannot crash.
|
|
@@ -37,6 +46,7 @@ const showChrome = computed(() => normalized.value.kind !== "server");
|
|
|
37
46
|
|
|
38
47
|
useHead(() => ({
|
|
39
48
|
title: title.value,
|
|
49
|
+
htmlAttrs: { lang: locale.value },
|
|
40
50
|
meta: [{ name: "robots", content: "noindex, nofollow" }],
|
|
41
51
|
}));
|
|
42
52
|
|
|
@@ -165,7 +165,7 @@ function scrollToTop() {
|
|
|
165
165
|
<article v-if="page">
|
|
166
166
|
<div class="flex items-start justify-between gap-4 sm:gap-6">
|
|
167
167
|
<div class="min-w-0">
|
|
168
|
-
<DocsBreadcrumb class="mb-2" :items="visibleTrail" />
|
|
168
|
+
<DocsBreadcrumb v-if="visibleTrail.length > 1" class="mb-2" :items="visibleTrail" />
|
|
169
169
|
<h1
|
|
170
170
|
class="font-heading text-[27px] leading-[1.15] font-semibold tracking-[-0.02em] text-balance text-foreground"
|
|
171
171
|
>
|
|
@@ -208,7 +208,7 @@ function scrollToTop() {
|
|
|
208
208
|
|
|
209
209
|
<aside
|
|
210
210
|
ref="tocAside"
|
|
211
|
-
class="sticky top-[var(--site-header-height)]
|
|
211
|
+
class="docs-toc-shell sticky top-[var(--site-header-height)] h-[calc(100dvh-var(--site-header-height)-var(--site-banner-height,0px))] w-[var(--docs-toc-width)] shrink-0 flex-col overflow-y-auto pt-10 pr-4 pb-6"
|
|
212
212
|
:aria-label="t('docs.toc')"
|
|
213
213
|
>
|
|
214
214
|
<DocsToc class="mb-6" :items="tocItems" :active-ids="activeIds" />
|
|
@@ -48,13 +48,13 @@ const linkRowClass = computed(() =>
|
|
|
48
48
|
"group relative flex w-full flex-row items-center gap-2 text-start [overflow-wrap:anywhere] text-muted-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
49
49
|
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
50
50
|
"hover:bg-accent/50 hover:text-accent-foreground/80 hover:transition-none",
|
|
51
|
-
"data-[active=true]:bg-
|
|
51
|
+
"data-[active=true]:bg-muted data-[active=true]:text-foreground data-[active=true]:font-medium data-[active=true]:hover:bg-muted data-[active=true]:hover:transition-colors",
|
|
52
52
|
props.depth === 0
|
|
53
53
|
? "rounded-[7px] px-2 py-[5.5px] text-[13px]"
|
|
54
54
|
: cn(
|
|
55
55
|
"rounded-e-[7px] border-s border-border py-[5px] pe-2 text-[12.5px]",
|
|
56
56
|
"hover:border-s-muted-foreground/40",
|
|
57
|
-
"data-[active=true]:border-s-
|
|
57
|
+
"data-[active=true]:border-s-brand",
|
|
58
58
|
paddingClass.value,
|
|
59
59
|
),
|
|
60
60
|
),
|
|
@@ -21,7 +21,7 @@ const emit = defineEmits<{
|
|
|
21
21
|
class="flex min-h-9 w-full items-center gap-2 rounded-lg px-2 text-start text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
22
22
|
:class="
|
|
23
23
|
activeId === section.id
|
|
24
|
-
? 'bg-
|
|
24
|
+
? 'bg-muted font-medium text-foreground'
|
|
25
25
|
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
|
26
26
|
"
|
|
27
27
|
:aria-pressed="activeId === section.id"
|
|
@@ -76,7 +76,7 @@ function scrollToHeading(id: string) {
|
|
|
76
76
|
|
|
77
77
|
<div class="relative border-l border-border pl-3">
|
|
78
78
|
<div
|
|
79
|
-
class="pointer-events-none absolute -left-px w-0.5 rounded-full bg-
|
|
79
|
+
class="pointer-events-none absolute -left-px w-0.5 rounded-full bg-brand transition-all duration-200 ease-out"
|
|
80
80
|
:style="indicatorStyle"
|
|
81
81
|
/>
|
|
82
82
|
|
|
@@ -91,7 +91,7 @@ function scrollToHeading(id: string) {
|
|
|
91
91
|
cn(
|
|
92
92
|
item.depth === 3 ? 'pl-3' : (item.depth ?? 2) >= 4 ? 'pl-6' : '',
|
|
93
93
|
activeIds.includes(item.id)
|
|
94
|
-
? 'font-medium text-
|
|
94
|
+
? 'font-medium text-foreground'
|
|
95
95
|
: 'text-muted-foreground hover:text-foreground',
|
|
96
96
|
)
|
|
97
97
|
"
|
|
@@ -24,6 +24,39 @@ export interface CommandCenterGroupResult {
|
|
|
24
24
|
export const MAX_RECENT_ITEMS = 5;
|
|
25
25
|
export const PAGE_HIGHLIGHT_STORAGE_KEY = "site-command-center-page-highlight";
|
|
26
26
|
|
|
27
|
+
const SEARCH_HIGHLIGHT_STOP_WORDS = new Set([
|
|
28
|
+
"a",
|
|
29
|
+
"an",
|
|
30
|
+
"and",
|
|
31
|
+
"der",
|
|
32
|
+
"die",
|
|
33
|
+
"das",
|
|
34
|
+
"ein",
|
|
35
|
+
"eine",
|
|
36
|
+
"for",
|
|
37
|
+
"in",
|
|
38
|
+
"of",
|
|
39
|
+
"or",
|
|
40
|
+
"the",
|
|
41
|
+
"to",
|
|
42
|
+
"und",
|
|
43
|
+
"von",
|
|
44
|
+
"zu",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function getSearchHighlightTerms(query: string): string[] {
|
|
48
|
+
const terms = [...new Set(query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean))];
|
|
49
|
+
const meaningfulTerms = terms.filter(
|
|
50
|
+
(term) => term.length > 1 && !SEARCH_HIGHLIGHT_STOP_WORDS.has(term),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return meaningfulTerms.length > 0 ? meaningfulTerms : terms;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function shouldShowSearchResultBadges(items: CommandCenterItem[]): boolean {
|
|
57
|
+
return new Set(items.map((item) => item.badge).filter(Boolean)).size > 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
export function dedupeCommandCenterItems(items: CommandCenterItem[]) {
|
|
28
61
|
const seen = new Set<string>();
|
|
29
62
|
return items.filter((item) => {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from "vue";
|
|
3
3
|
import { useI18n, useRoute } from "#imports";
|
|
4
4
|
import { useCommandCenter } from "#ginko-docs/features/search/useCommandCenter";
|
|
5
|
+
import {
|
|
6
|
+
getSearchHighlightTerms,
|
|
7
|
+
shouldShowSearchResultBadges,
|
|
8
|
+
} from "#ginko-docs/features/search/command-center";
|
|
5
9
|
import {
|
|
6
10
|
DialogContent,
|
|
7
11
|
DialogDescription,
|
|
@@ -21,10 +25,13 @@ const { open, query, groupedItems, searchPending, searchError, searchIsEmpty, se
|
|
|
21
25
|
|
|
22
26
|
const visibleItems = computed(() => groupedItems.value.flatMap((g) => g.items));
|
|
23
27
|
const isSearching = computed(() => query.value.trim().length > 0);
|
|
28
|
+
const showResultBadges = computed(
|
|
29
|
+
() => !isSearching.value || shouldShowSearchResultBadges(visibleItems.value),
|
|
30
|
+
);
|
|
24
31
|
|
|
25
32
|
// ── text highlighting ──────────────────────────────────────────────────────
|
|
26
33
|
function highlightTokens(text: string, q: string): { text: string; match: boolean }[] {
|
|
27
|
-
const terms = q
|
|
34
|
+
const terms = getSearchHighlightTerms(q);
|
|
28
35
|
if (terms.length === 0) return [{ text, match: false }];
|
|
29
36
|
const pattern = terms.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
30
37
|
return text
|
|
@@ -123,7 +130,7 @@ onMounted(async () => {
|
|
|
123
130
|
class="fixed inset-0 z-50 bg-overlay backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0"
|
|
124
131
|
/>
|
|
125
132
|
<DialogContent
|
|
126
|
-
class="fixed top-[8vh] left-1/2 z-50 flex max-h-[
|
|
133
|
+
class="fixed top-[8vh] left-1/2 z-50 flex max-h-[80vh] w-[min(94vw,44rem)] -translate-x-1/2 flex-col overflow-hidden rounded-xl border border-border bg-background shadow-lg outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:top-[10vh]"
|
|
127
134
|
@keydown="handleListNavigation"
|
|
128
135
|
>
|
|
129
136
|
<VisuallyHidden>
|
|
@@ -165,7 +172,9 @@ onMounted(async () => {
|
|
|
165
172
|
</div>
|
|
166
173
|
|
|
167
174
|
<!-- Results -->
|
|
168
|
-
<div
|
|
175
|
+
<div
|
|
176
|
+
class="min-h-0 flex-1 overflow-y-auto overscroll-contain py-2 [scrollbar-color:var(--border)_transparent]"
|
|
177
|
+
>
|
|
169
178
|
<div
|
|
170
179
|
v-if="isSearching && searchPending"
|
|
171
180
|
class="flex items-center justify-center gap-2 px-6 py-12 text-sm text-muted-foreground"
|
|
@@ -190,6 +199,13 @@ onMounted(async () => {
|
|
|
190
199
|
<div class="flex items-center gap-2 px-2 pt-1.5 pb-1">
|
|
191
200
|
<span class="text-[11px] font-medium text-muted-foreground">{{ group.title }}</span>
|
|
192
201
|
<div class="flex-1 border-t border-border/60" />
|
|
202
|
+
<span
|
|
203
|
+
v-if="group.id === 'search'"
|
|
204
|
+
class="text-[11px] tabular-nums text-muted-foreground"
|
|
205
|
+
aria-live="polite"
|
|
206
|
+
>
|
|
207
|
+
{{ t("command.resultCount", { count: group.items.length }) }}
|
|
208
|
+
</span>
|
|
193
209
|
</div>
|
|
194
210
|
|
|
195
211
|
<button
|
|
@@ -198,7 +214,7 @@ onMounted(async () => {
|
|
|
198
214
|
type="button"
|
|
199
215
|
:data-item="item.id"
|
|
200
216
|
:aria-label="itemLabel(item)"
|
|
201
|
-
class="group flex w-full items-start gap-3 rounded-lg px-2.5 py-2
|
|
217
|
+
class="group flex w-full items-start gap-3 rounded-lg px-2.5 py-2 text-left transition-colors outline-none"
|
|
202
218
|
:class="
|
|
203
219
|
highlightedId === item.id ? 'bg-accent text-accent-foreground' : 'text-foreground'
|
|
204
220
|
"
|
|
@@ -208,7 +224,7 @@ onMounted(async () => {
|
|
|
208
224
|
>
|
|
209
225
|
<!-- Icon -->
|
|
210
226
|
<div
|
|
211
|
-
class="mt-0.5 flex size-
|
|
227
|
+
class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-muted/50 transition-colors"
|
|
212
228
|
:class="highlightedId === item.id ? 'border-border/80 bg-background' : ''"
|
|
213
229
|
>
|
|
214
230
|
<Icon
|
|
@@ -235,14 +251,14 @@ onMounted(async () => {
|
|
|
235
251
|
>
|
|
236
252
|
<mark
|
|
237
253
|
v-if="token.match"
|
|
238
|
-
class="rounded-[3px] bg-
|
|
254
|
+
class="rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground not-italic"
|
|
239
255
|
>{{ token.text }}</mark
|
|
240
256
|
><template v-else>{{ token.text }}</template>
|
|
241
257
|
</template>
|
|
242
258
|
</span>
|
|
243
259
|
<span
|
|
244
|
-
v-if="item.badge"
|
|
245
|
-
class="shrink-0 rounded-full bg-
|
|
260
|
+
v-if="item.badge && showResultBadges"
|
|
261
|
+
class="shrink-0 rounded-full bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground"
|
|
246
262
|
>
|
|
247
263
|
{{ item.badge }}
|
|
248
264
|
</span>
|
|
@@ -263,7 +279,7 @@ onMounted(async () => {
|
|
|
263
279
|
>
|
|
264
280
|
<mark
|
|
265
281
|
v-if="token.match"
|
|
266
|
-
class="rounded-[3px] bg-
|
|
282
|
+
class="rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground not-italic"
|
|
267
283
|
>{{ token.text }}</mark
|
|
268
284
|
><template v-else>{{ token.text }}</template>
|
|
269
285
|
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { nextTick, onBeforeUnmount, watch } from "vue";
|
|
3
3
|
import { useRoute } from "#imports";
|
|
4
|
+
import { getSearchHighlightTerms } from "#ginko-docs/features/search/command-center";
|
|
4
5
|
|
|
5
6
|
const STORAGE_KEY = "site-command-center-page-highlight";
|
|
6
7
|
const HIGHLIGHT_SELECTOR = "mark[data-search-page-highlight]";
|
|
@@ -65,7 +66,8 @@ function highlightPattern(root: Element, pattern: RegExp) {
|
|
|
65
66
|
|
|
66
67
|
const mark = document.createElement("mark");
|
|
67
68
|
mark.dataset.searchPageHighlight = "";
|
|
68
|
-
mark.className =
|
|
69
|
+
mark.className =
|
|
70
|
+
"rounded-[3px] bg-accent-yellow px-0.5 font-medium text-accent-yellow-foreground";
|
|
69
71
|
mark.textContent = value;
|
|
70
72
|
fragment.append(mark);
|
|
71
73
|
|
|
@@ -83,7 +85,7 @@ function escapePattern(value: string) {
|
|
|
83
85
|
|
|
84
86
|
function buildPatterns(query: string) {
|
|
85
87
|
const phrase = query.trim().replace(/\s+/g, " ");
|
|
86
|
-
const tokens = phrase
|
|
88
|
+
const tokens = getSearchHighlightTerms(phrase);
|
|
87
89
|
const phrasePattern = phrase.split(/\s+/).map(escapePattern).join("\\s+");
|
|
88
90
|
|
|
89
91
|
return [
|
|
@@ -71,7 +71,7 @@ export async function useCommandCenter() {
|
|
|
71
71
|
// Nuxt/i18n composable after an awaited operation loses the setup context.
|
|
72
72
|
const docsNavigationResult = useDocsNavigation();
|
|
73
73
|
const contentSearchResult = useContentSearch({
|
|
74
|
-
limit:
|
|
74
|
+
limit: 8,
|
|
75
75
|
locale: () => locale.value,
|
|
76
76
|
});
|
|
77
77
|
const [
|
|
@@ -190,7 +190,7 @@ export async function useCommandCenter() {
|
|
|
190
190
|
href: github.href,
|
|
191
191
|
external: true,
|
|
192
192
|
group: "actions" as const,
|
|
193
|
-
icon: github.icon ?? "
|
|
193
|
+
icon: github.icon ?? "ginko-social:github",
|
|
194
194
|
keywords: ["repo", "source", "code"],
|
|
195
195
|
},
|
|
196
196
|
]
|
package/app/layouts/docs.vue
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<div class="mx-auto flex w-full max-w-screen-2xl min-w-0 flex-1">
|
|
8
8
|
<div
|
|
9
|
-
class="pointer-events-none sticky top-[var(--site-header-height)] z-20
|
|
9
|
+
class="docs-sidebar-shell pointer-events-none sticky top-[var(--site-header-height)] z-20 h-[calc(100dvh-var(--site-header-height)-var(--site-banner-height,0px))] w-[var(--docs-sidebar-width)] shrink-0 overflow-hidden *:pointer-events-auto"
|
|
10
10
|
>
|
|
11
11
|
<DocsSidebar />
|
|
12
12
|
</div>
|
package/app/pages/index.vue
CHANGED
|
@@ -69,6 +69,11 @@ const heroCodeTabs = computed(() => {
|
|
|
69
69
|
return [];
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
const hasWideHeroCode = computed(() => {
|
|
73
|
+
const media = landing.value.heroMedia;
|
|
74
|
+
return media?.type !== "image" && media?.layout === "wide";
|
|
75
|
+
});
|
|
76
|
+
|
|
72
77
|
// Feature icon tiles cycle through the theme-adaptive hero accent pairs.
|
|
73
78
|
const FEATURE_TINTS = [
|
|
74
79
|
{ tile: "bg-(--hero-blue-muted)", icon: "text-(--hero-blue-dark)" },
|
|
@@ -127,7 +132,7 @@ useSeoMeta({
|
|
|
127
132
|
<div class="mt-10 flex flex-wrap items-center gap-3">
|
|
128
133
|
<NuxtLink
|
|
129
134
|
:to="landing.primary.to"
|
|
130
|
-
class="inline-flex h-11 items-center gap-2 rounded-md bg-
|
|
135
|
+
class="inline-flex h-11 items-center gap-2 rounded-md bg-brand px-5 text-sm font-semibold text-brand-foreground transition-transform hover:-translate-y-0.5"
|
|
131
136
|
>
|
|
132
137
|
{{ landing.primary.label }}
|
|
133
138
|
<Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
|
|
@@ -174,7 +179,11 @@ useSeoMeta({
|
|
|
174
179
|
width="576"
|
|
175
180
|
loading="lazy"
|
|
176
181
|
/>
|
|
177
|
-
<SiteHeroCode
|
|
182
|
+
<SiteHeroCode
|
|
183
|
+
v-else-if="heroCodeTabs.length"
|
|
184
|
+
:tabs="heroCodeTabs"
|
|
185
|
+
:class="hasWideHeroCode && 'site-hero-code-wide'"
|
|
186
|
+
/>
|
|
178
187
|
</div>
|
|
179
188
|
</div>
|
|
180
189
|
</section>
|
|
@@ -240,7 +249,7 @@ useSeoMeta({
|
|
|
240
249
|
<div class="mt-9 flex flex-wrap items-center justify-center gap-3">
|
|
241
250
|
<NuxtLink
|
|
242
251
|
:to="landing.cta.primary.to"
|
|
243
|
-
class="inline-flex h-11 items-center gap-2 rounded-md bg-
|
|
252
|
+
class="inline-flex h-11 items-center gap-2 rounded-md bg-brand px-5 text-sm font-semibold text-brand-foreground transition-transform hover:-translate-y-0.5"
|
|
244
253
|
>
|
|
245
254
|
{{ landing.cta.primary.label }}
|
|
246
255
|
<Icon name="lucide:arrow-right" class="size-4" aria-hidden="true" />
|
|
@@ -257,3 +266,13 @@ useSeoMeta({
|
|
|
257
266
|
</section>
|
|
258
267
|
</div>
|
|
259
268
|
</template>
|
|
269
|
+
|
|
270
|
+
<style scoped>
|
|
271
|
+
/* Keep the hero grid intact; only use spare space toward the viewport edge. */
|
|
272
|
+
@media (min-width: 80rem) {
|
|
273
|
+
.site-hero-code-wide {
|
|
274
|
+
width: min(48.75rem, calc(50vw - 2rem));
|
|
275
|
+
max-width: none;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
</style>
|
package/content-collections.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineCollection, defineContentConfig, reference } from "@lupinum/ginko-content/config";
|
|
2
|
+
import type { ContentAgentCollectionConfig } from "@lupinum/ginko-content/config";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { routeSlugs } from "./shared/route-slugs";
|
|
4
5
|
|
|
@@ -51,15 +52,22 @@ const authorsSchema = z.object({
|
|
|
51
52
|
links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
export function createGinkoDocsCollections(
|
|
55
|
+
export function createGinkoDocsCollections(
|
|
56
|
+
locales: readonly ["en"] | readonly ["de"] | readonly ["en", "de"] = ["en"],
|
|
57
|
+
docsMarkdown: ContentAgentCollectionConfig["markdown"] = true,
|
|
58
|
+
) {
|
|
59
|
+
const i18n = locales.length === 2;
|
|
60
|
+
const primaryLocale = locales[0];
|
|
55
61
|
return defineContentConfig({
|
|
56
62
|
collections: {
|
|
57
63
|
docs: defineCollection({
|
|
58
64
|
type: "page",
|
|
59
|
-
source: i18n
|
|
65
|
+
source: i18n
|
|
66
|
+
? "{1.docs,1.dokumentation}/**/*.md"
|
|
67
|
+
: `${routeSlugs.docs[primaryLocale].slice(1)}/**/*.md`,
|
|
60
68
|
i18n: i18n ? true : undefined,
|
|
61
|
-
route: i18n ? routeSlugs.docs : routeSlugs.docs
|
|
62
|
-
agent: { section: "optional", markdown:
|
|
69
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs[primaryLocale],
|
|
70
|
+
agent: { section: "optional", markdown: docsMarkdown },
|
|
63
71
|
strict: true,
|
|
64
72
|
schema: docsSchemaWithLastmod,
|
|
65
73
|
}),
|
|
@@ -67,7 +75,7 @@ export function createGinkoDocsCollections(i18n: boolean) {
|
|
|
67
75
|
type: "page",
|
|
68
76
|
source: "2.blog/*.md",
|
|
69
77
|
i18n: i18n ? true : undefined,
|
|
70
|
-
route: i18n ? routeSlugs.blog : routeSlugs.blog
|
|
78
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog[primaryLocale],
|
|
71
79
|
agent: { section: "blog", markdown: true },
|
|
72
80
|
strict: true,
|
|
73
81
|
schema: blogSchemaWithLastmod,
|
package/content.js
CHANGED
|
@@ -81,17 +81,21 @@ const authorsSchema = z.object({
|
|
|
81
81
|
)
|
|
82
82
|
.optional(),
|
|
83
83
|
});
|
|
84
|
-
function createGinkoDocsCollections(
|
|
84
|
+
function createGinkoDocsCollections(locales = ["en"], docsMarkdown = true) {
|
|
85
|
+
const i18n = locales.length === 2;
|
|
86
|
+
const primaryLocale = locales[0];
|
|
85
87
|
return defineContentConfig({
|
|
86
88
|
collections: {
|
|
87
89
|
docs: defineCollection({
|
|
88
90
|
type: "page",
|
|
89
|
-
source: i18n
|
|
91
|
+
source: i18n
|
|
92
|
+
? "{1.docs,1.dokumentation}/**/*.md"
|
|
93
|
+
: `${routeSlugs.docs[primaryLocale].slice(1)}/**/*.md`,
|
|
90
94
|
i18n: i18n ? true : void 0,
|
|
91
|
-
route: i18n ? routeSlugs.docs : routeSlugs.docs
|
|
95
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs[primaryLocale],
|
|
92
96
|
agent: {
|
|
93
97
|
section: "optional",
|
|
94
|
-
markdown:
|
|
98
|
+
markdown: docsMarkdown,
|
|
95
99
|
},
|
|
96
100
|
strict: true,
|
|
97
101
|
schema: docsSchemaWithLastmod,
|
|
@@ -100,7 +104,7 @@ function createGinkoDocsCollections(i18n) {
|
|
|
100
104
|
type: "page",
|
|
101
105
|
source: "2.blog/*.md",
|
|
102
106
|
i18n: i18n ? true : void 0,
|
|
103
|
-
route: i18n ? routeSlugs.blog : routeSlugs.blog
|
|
107
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog[primaryLocale],
|
|
104
108
|
agent: {
|
|
105
109
|
section: "blog",
|
|
106
110
|
markdown: true,
|
|
@@ -125,11 +129,10 @@ function defineGinkoDocsConfig(options) {
|
|
|
125
129
|
const locales = options.locales ?? ["en"];
|
|
126
130
|
if (
|
|
127
131
|
!(locales.length === 1
|
|
128
|
-
? locales[0] === "en"
|
|
132
|
+
? locales[0] === "en" || locales[0] === "de"
|
|
129
133
|
: locales.length === 2 && locales[0] === "en" && locales[1] === "de")
|
|
130
134
|
)
|
|
131
|
-
throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
|
|
132
|
-
const i18n = locales.length === 2;
|
|
135
|
+
throw new TypeError('locales must be exactly ["en"], ["de"], or ["en", "de"]');
|
|
133
136
|
const metadata = defineAgentMetadataFields([
|
|
134
137
|
"title",
|
|
135
138
|
"description",
|
|
@@ -141,13 +144,14 @@ function defineGinkoDocsConfig(options) {
|
|
|
141
144
|
"source",
|
|
142
145
|
"updated",
|
|
143
146
|
]);
|
|
144
|
-
const { docs, blog, authors } = createGinkoDocsCollections(
|
|
147
|
+
const { docs, blog, authors } = createGinkoDocsCollections(locales, options.agent?.documentation);
|
|
145
148
|
const config = {
|
|
146
149
|
agent: {
|
|
147
150
|
site: {
|
|
148
151
|
title: options.site.name,
|
|
149
152
|
description: options.site.description,
|
|
150
|
-
|
|
153
|
+
whenToUse: options.site.whenToUse,
|
|
154
|
+
...(options.site.whenNotToUse ? { whenNotToUse: options.site.whenNotToUse } : {}),
|
|
151
155
|
},
|
|
152
156
|
markdown: {
|
|
153
157
|
metadata: {
|
|
@@ -156,6 +160,7 @@ function defineGinkoDocsConfig(options) {
|
|
|
156
160
|
},
|
|
157
161
|
},
|
|
158
162
|
sections: [
|
|
163
|
+
...(options.agent?.sections ?? []),
|
|
159
164
|
...(options.blog
|
|
160
165
|
? [
|
|
161
166
|
defineAgentSection({
|
|
@@ -177,6 +182,7 @@ function defineGinkoDocsConfig(options) {
|
|
|
177
182
|
order: 100,
|
|
178
183
|
}),
|
|
179
184
|
],
|
|
185
|
+
...(options.agent?.pages ? { pages: [...options.agent.pages] } : {}),
|
|
180
186
|
},
|
|
181
187
|
};
|
|
182
188
|
if (options.blog)
|
package/content.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
|
+
defineAgentAppPage,
|
|
2
3
|
defineAgentMetadataFields,
|
|
3
4
|
defineAgentSection,
|
|
4
5
|
defineContentConfig,
|
|
5
6
|
} from "@lupinum/ginko-content/config";
|
|
6
|
-
import type { ContentConfig } from "@lupinum/ginko-content/config";
|
|
7
|
+
import type { ContentAgentMarkdownOptions, ContentConfig } from "@lupinum/ginko-content/config";
|
|
7
8
|
import { createGinkoDocsCollections } from "./content-collections";
|
|
8
9
|
|
|
10
|
+
type GinkoDocsAgentPage = Parameters<typeof defineAgentAppPage>[0];
|
|
11
|
+
type GinkoDocsAgentSection = Parameters<typeof defineAgentSection>[0];
|
|
12
|
+
|
|
13
|
+
export interface GinkoDocsAgentOptions {
|
|
14
|
+
/** Control whether authored documentation appears in compact and full indexes. */
|
|
15
|
+
documentation?: ContentAgentMarkdownOptions;
|
|
16
|
+
/** Add compact index pages that point agents to canonical raw Markdown. */
|
|
17
|
+
pages?: readonly GinkoDocsAgentPage[];
|
|
18
|
+
/** Add sections before the built-in blog and optional documentation sections. */
|
|
19
|
+
sections?: readonly GinkoDocsAgentSection[];
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export interface GinkoDocsContentOptions {
|
|
10
23
|
site: {
|
|
11
24
|
name: string | { en: string; de: string };
|
|
12
25
|
description: string | { en: string; de: string };
|
|
13
|
-
|
|
26
|
+
whenToUse: string | { en: string; de: string };
|
|
27
|
+
whenNotToUse?: string | { en: string; de: string };
|
|
14
28
|
};
|
|
15
|
-
|
|
29
|
+
agent?: GinkoDocsAgentOptions;
|
|
30
|
+
locales?: readonly ["en"] | readonly ["de"] | readonly ["en", "de"];
|
|
16
31
|
blog?: boolean;
|
|
17
32
|
}
|
|
18
33
|
|
|
@@ -37,12 +52,11 @@ export function defineGinkoDocsConfig(
|
|
|
37
52
|
const locales = options.locales ?? ["en"];
|
|
38
53
|
const validLocales =
|
|
39
54
|
locales.length === 1
|
|
40
|
-
? locales[0] === "en"
|
|
55
|
+
? locales[0] === "en" || locales[0] === "de"
|
|
41
56
|
: locales.length === 2 && locales[0] === "en" && locales[1] === "de";
|
|
42
57
|
if (!validLocales) {
|
|
43
|
-
throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
|
|
58
|
+
throw new TypeError('locales must be exactly ["en"], ["de"], or ["en", "de"]');
|
|
44
59
|
}
|
|
45
|
-
const i18n = locales.length === 2;
|
|
46
60
|
const metadata = defineAgentMetadataFields([
|
|
47
61
|
"title",
|
|
48
62
|
"description",
|
|
@@ -54,16 +68,18 @@ export function defineGinkoDocsConfig(
|
|
|
54
68
|
"source",
|
|
55
69
|
"updated",
|
|
56
70
|
]);
|
|
57
|
-
const { docs, blog, authors } = createGinkoDocsCollections(
|
|
71
|
+
const { docs, blog, authors } = createGinkoDocsCollections(locales, options.agent?.documentation);
|
|
58
72
|
const config = {
|
|
59
73
|
agent: {
|
|
60
74
|
site: {
|
|
61
75
|
title: options.site.name,
|
|
62
76
|
description: options.site.description,
|
|
63
|
-
|
|
77
|
+
whenToUse: options.site.whenToUse,
|
|
78
|
+
...(options.site.whenNotToUse ? { whenNotToUse: options.site.whenNotToUse } : {}),
|
|
64
79
|
},
|
|
65
80
|
markdown: { metadata: { enabled: true, defaultFields: metadata } },
|
|
66
81
|
sections: [
|
|
82
|
+
...(options.agent?.sections ?? []),
|
|
67
83
|
...(options.blog
|
|
68
84
|
? [defineAgentSection({ id: "blog", title: { en: "Blog", de: "Blog" }, order: 40 })]
|
|
69
85
|
: []),
|
|
@@ -73,6 +89,7 @@ export function defineGinkoDocsConfig(
|
|
|
73
89
|
order: 100,
|
|
74
90
|
}),
|
|
75
91
|
],
|
|
92
|
+
...(options.agent?.pages ? { pages: [...options.agent.pages] } : {}),
|
|
76
93
|
},
|
|
77
94
|
};
|
|
78
95
|
|
|
@@ -23,6 +23,7 @@ export const command = {
|
|
|
23
23
|
actions: { de: "Aktionen", en: "Actions" },
|
|
24
24
|
},
|
|
25
25
|
loading: { de: "Suche läuft …", en: "Searching…" },
|
|
26
|
+
resultCount: { de: "{count} Treffer", en: "{count} results" },
|
|
26
27
|
error: {
|
|
27
28
|
de: "Die Suche ist gerade nicht verfügbar. Versuche es erneut.",
|
|
28
29
|
en: "Search is temporarily unavailable. Please try again.",
|
package/icon-bundle.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
export const layerIconNames = [
|
|
4
4
|
"circle-flags:de",
|
|
5
5
|
"circle-flags:us",
|
|
6
|
+
"ginko-social:discord",
|
|
7
|
+
"ginko-social:github",
|
|
6
8
|
"logos:bun",
|
|
7
|
-
"logos:discord-icon",
|
|
8
9
|
"logos:npm-icon",
|
|
9
10
|
"logos:nuxt-icon",
|
|
10
11
|
"logos:pnpm",
|
|
@@ -91,6 +92,25 @@ type IconCollection = {
|
|
|
91
92
|
|
|
92
93
|
const require = createRequire(import.meta.url);
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const ginkoSocialCollection = {
|
|
96
|
+
prefix: "ginko-social",
|
|
97
|
+
width: 24,
|
|
98
|
+
height: 24,
|
|
99
|
+
icons: {
|
|
100
|
+
// Google Material Icons: https://github.com/material-icons/material-icons/blob/master/LICENSE
|
|
101
|
+
discord: {
|
|
102
|
+
body: '<path fill="currentColor" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12"/>',
|
|
103
|
+
},
|
|
104
|
+
// Material Design Icons: https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
|
|
105
|
+
github: {
|
|
106
|
+
body: '<path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"/>',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
} satisfies IconCollection;
|
|
110
|
+
|
|
111
|
+
export const layerIconCollections = [
|
|
112
|
+
...["circle-flags", "logos", "lucide"].map(
|
|
113
|
+
(prefix) => require(`@iconify-json/${prefix}/icons.json`) as IconCollection,
|
|
114
|
+
),
|
|
115
|
+
ginkoSocialCollection,
|
|
116
|
+
];
|