@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/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/lupinum-dev/ginko-docs/main/docs/public/
|
|
2
|
+
<img src="https://raw.githubusercontent.com/lupinum-dev/ginko-docs/main/docs/public/favicon.svg" width="128" alt="Ginko Docs icon">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">@lupinum/ginko-docs</h1>
|
|
@@ -23,13 +23,13 @@ Use this package when a Nuxt application needs documentation routes, navigation,
|
|
|
23
23
|
|
|
24
24
|
- Node.js 22.18, 24.11, or 26 and later maintenance releases
|
|
25
25
|
- Nuxt 4.5.1 or later in the Nuxt 4 line
|
|
26
|
-
- Vue 3.5.
|
|
26
|
+
- Vue 3.5.40 or later
|
|
27
27
|
- Ginko Content 0.4 prerelease or later in the 0.4 line
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
pnpm add -D @lupinum/ginko-docs@0.
|
|
32
|
+
pnpm add -D @lupinum/ginko-docs@0.4.0-rc.10 @lupinum/ginko-content@1.0.0-beta.7
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
```ts
|
|
@@ -49,7 +49,7 @@ export default defineGinkoDocsConfig({
|
|
|
49
49
|
site: {
|
|
50
50
|
name: "Example Docs",
|
|
51
51
|
description: "Documentation for Example.",
|
|
52
|
-
|
|
52
|
+
whenToUse: "Use this site to learn and operate Example.",
|
|
53
53
|
},
|
|
54
54
|
locales: ["en"],
|
|
55
55
|
blog: false,
|
|
@@ -58,6 +58,14 @@ export default defineGinkoDocsConfig({
|
|
|
58
58
|
|
|
59
59
|
Put Markdown files in `content/docs`. Open `/docs` after the Nuxt server starts.
|
|
60
60
|
|
|
61
|
+
Generate the browser icon set from one square SVG or raster source:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm exec ginko-docs-favicons --source public/product-icon.svg --out public
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The command writes `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`, and `favicon.ico`. Ginko Docs adds the matching head links and uses the generated 96px PNG in Nuxt-preset social images so every supported source format renders consistently.
|
|
68
|
+
|
|
61
69
|
## Exports
|
|
62
70
|
|
|
63
71
|
- `@lupinum/ginko-docs` is the Nuxt layer entry.
|
package/app/app.config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { GinkoDocsAppConfig } from "../shared/types/app-config";
|
|
|
2
2
|
export default {
|
|
3
3
|
ginkoDocs: {
|
|
4
4
|
theme: {
|
|
5
|
+
preset: "default",
|
|
5
6
|
neutral: "zinc",
|
|
6
7
|
primary: "neutral",
|
|
7
8
|
codeBlocks: "dark",
|
|
@@ -16,6 +17,7 @@ export default {
|
|
|
16
17
|
de: "Dokumentations-Sites auf Basis von Nuxt und Ginko Content.",
|
|
17
18
|
},
|
|
18
19
|
url: "http://localhost:3000",
|
|
20
|
+
icon: "/favicon.svg",
|
|
19
21
|
logo: { light: "/logo.svg", dark: "/logo-dark.svg" },
|
|
20
22
|
docsSidebarSwitcher: "tabs",
|
|
21
23
|
lupinumAttribution: true,
|
package/app/app.vue
CHANGED
|
@@ -7,10 +7,9 @@ import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
|
|
|
7
7
|
import { useGinkoAnalytics } from "#ginko-docs/composables/useGinkoAnalytics";
|
|
8
8
|
import { useGinkoOgImage } from "#ginko-docs/composables/useGinkoOgImage";
|
|
9
9
|
import { useSchemaJsonLd } from "#ginko-docs/composables/useSchemaJsonLd";
|
|
10
|
-
import { defaultLocale } from "../i18n/locales";
|
|
11
10
|
|
|
12
11
|
const canonicalUrl = useCanonicalUrl();
|
|
13
|
-
const { locale, locales } = useI18n();
|
|
12
|
+
const { defaultLocale, locale, locales } = useI18n();
|
|
14
13
|
const route = useRoute();
|
|
15
14
|
const { switchPathname } = useLocalizedRouteSwitch();
|
|
16
15
|
const docsConfig = useAppConfig().ginkoDocs;
|
|
@@ -18,6 +17,7 @@ const siteUrl = docsConfig.site.url;
|
|
|
18
17
|
|
|
19
18
|
useSeoMeta({
|
|
20
19
|
ogSiteName: computed(() => getLocalizedSiteText(docsConfig.site.name, locale.value)),
|
|
20
|
+
ogType: "website",
|
|
21
21
|
ogUrl: canonicalUrl,
|
|
22
22
|
twitterCard: "summary_large_image",
|
|
23
23
|
});
|
|
@@ -35,6 +35,15 @@ useSchemaJsonLd(() => [
|
|
|
35
35
|
url: siteUrl,
|
|
36
36
|
inLanguage: locale.value,
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"@type": "SoftwareApplication",
|
|
40
|
+
name: getLocalizedSiteText(docsConfig.site.name, locale.value),
|
|
41
|
+
description: getLocalizedSiteText(docsConfig.site.description, locale.value),
|
|
42
|
+
url: siteUrl,
|
|
43
|
+
applicationCategory: "DeveloperApplication",
|
|
44
|
+
operatingSystem: "Web",
|
|
45
|
+
...(docsConfig.social?.github ? { sameAs: [docsConfig.social.github] } : {}),
|
|
46
|
+
},
|
|
38
47
|
]);
|
|
39
48
|
|
|
40
49
|
useHead(() => ({
|
|
@@ -42,6 +51,21 @@ useHead(() => ({
|
|
|
42
51
|
lang: locale.value,
|
|
43
52
|
},
|
|
44
53
|
link: [
|
|
54
|
+
{
|
|
55
|
+
key: "favicon-svg",
|
|
56
|
+
rel: "icon",
|
|
57
|
+
type: "image/svg+xml",
|
|
58
|
+
href: docsConfig.site.icon,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "favicon-png",
|
|
62
|
+
rel: "icon",
|
|
63
|
+
type: "image/png",
|
|
64
|
+
sizes: "96x96",
|
|
65
|
+
href: "/favicon-96x96.png",
|
|
66
|
+
},
|
|
67
|
+
{ key: "favicon-ico", rel: "icon", sizes: "any", href: "/favicon.ico" },
|
|
68
|
+
{ key: "apple-touch-icon", rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
|
|
45
69
|
{ key: "canonical", rel: "canonical", href: canonicalUrl.value },
|
|
46
70
|
...locales.value.map((entry) => {
|
|
47
71
|
const code = typeof entry === "string" ? entry : entry.code;
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
@import "tw-animate-css";
|
|
3
3
|
@import "./theme-palettes.css";
|
|
4
|
+
@import "./theme-presets.css";
|
|
4
5
|
@source "../../**/*.{vue,ts}";
|
|
5
6
|
|
|
6
7
|
@custom-variant dark (&:is(.dark *));
|
|
7
8
|
|
|
9
|
+
.docs-sidebar-shell,
|
|
10
|
+
.docs-toc-shell {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media (min-width: 48rem) {
|
|
15
|
+
.docs-sidebar-shell {
|
|
16
|
+
display: block;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (min-width: 80rem) {
|
|
21
|
+
.docs-toc-shell {
|
|
22
|
+
display: flex;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
@theme inline {
|
|
9
27
|
--font-sans: var(--font-body);
|
|
10
28
|
--font-serif: var(--font-heading);
|
|
@@ -29,6 +47,8 @@
|
|
|
29
47
|
--color-popover-foreground: var(--popover-foreground);
|
|
30
48
|
--color-primary: var(--primary);
|
|
31
49
|
--color-primary-foreground: var(--primary-foreground);
|
|
50
|
+
--color-brand: var(--brand);
|
|
51
|
+
--color-brand-foreground: var(--brand-foreground);
|
|
32
52
|
--color-secondary: var(--secondary);
|
|
33
53
|
--color-secondary-foreground: var(--secondary-foreground);
|
|
34
54
|
--color-muted: var(--muted);
|
|
@@ -147,6 +167,8 @@
|
|
|
147
167
|
--popover-foreground: var(--theme-neutral-950);
|
|
148
168
|
--primary: var(--theme-primary-light);
|
|
149
169
|
--primary-foreground: var(--theme-primary-light-foreground);
|
|
170
|
+
--brand: var(--primary);
|
|
171
|
+
--brand-foreground: var(--primary-foreground);
|
|
150
172
|
--secondary: var(--theme-neutral-100);
|
|
151
173
|
--secondary-foreground: var(--theme-neutral-900);
|
|
152
174
|
--muted: var(--theme-neutral-100);
|
|
@@ -223,6 +245,8 @@
|
|
|
223
245
|
--popover-foreground: var(--theme-neutral-50);
|
|
224
246
|
--primary: var(--theme-primary-dark);
|
|
225
247
|
--primary-foreground: var(--theme-primary-dark-foreground);
|
|
248
|
+
--brand: var(--primary);
|
|
249
|
+
--brand-foreground: var(--primary-foreground);
|
|
226
250
|
--secondary: var(--theme-neutral-800);
|
|
227
251
|
--secondary-foreground: var(--theme-neutral-50);
|
|
228
252
|
--muted: var(--theme-neutral-800);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* Complete visual systems. Presets intentionally override palette selection;
|
|
2
|
+
use the default preset when neutral and primary must be configured alone. */
|
|
3
|
+
|
|
4
|
+
html[data-theme-preset="nuxt"] {
|
|
5
|
+
/* Official Nuxt green scale. */
|
|
6
|
+
--nuxt-green-50: #effdf5;
|
|
7
|
+
--nuxt-green-100: #d9fbe8;
|
|
8
|
+
--nuxt-green-200: #b3f5d1;
|
|
9
|
+
--nuxt-green-300: #75edae;
|
|
10
|
+
--nuxt-green-400: #00dc82;
|
|
11
|
+
--nuxt-green-500: #00c16a;
|
|
12
|
+
--nuxt-green-600: #00a155;
|
|
13
|
+
--nuxt-green-700: #007f45;
|
|
14
|
+
--nuxt-green-800: #016538;
|
|
15
|
+
--nuxt-green-900: #0a5331;
|
|
16
|
+
--nuxt-green-950: #052e16;
|
|
17
|
+
|
|
18
|
+
/* Nuxt uses Slate for neutral surfaces. */
|
|
19
|
+
--theme-neutral-50: var(--color-slate-50);
|
|
20
|
+
--theme-neutral-100: var(--color-slate-100);
|
|
21
|
+
--theme-neutral-200: var(--color-slate-200);
|
|
22
|
+
--theme-neutral-300: var(--color-slate-300);
|
|
23
|
+
--theme-neutral-400: var(--color-slate-400);
|
|
24
|
+
--theme-neutral-500: var(--color-slate-500);
|
|
25
|
+
--theme-neutral-600: var(--color-slate-600);
|
|
26
|
+
--theme-neutral-700: var(--color-slate-700);
|
|
27
|
+
--theme-neutral-800: var(--color-slate-800);
|
|
28
|
+
--theme-neutral-900: var(--color-slate-900);
|
|
29
|
+
--theme-neutral-950: var(--color-slate-950);
|
|
30
|
+
|
|
31
|
+
/* Nuxt uses one recognizable brand green across interactive surfaces. */
|
|
32
|
+
--theme-primary-light: var(--nuxt-green-400);
|
|
33
|
+
--theme-primary-light-foreground: #020420;
|
|
34
|
+
--theme-primary-light-ring: var(--nuxt-green-400);
|
|
35
|
+
--theme-primary-dark: var(--nuxt-green-400);
|
|
36
|
+
--theme-primary-dark-foreground: var(--nuxt-green-950);
|
|
37
|
+
--theme-primary-dark-ring: var(--nuxt-green-400);
|
|
38
|
+
|
|
39
|
+
/* Brand surfaces can use Nuxt green with its high-contrast navy foreground. */
|
|
40
|
+
--brand: var(--nuxt-green-400);
|
|
41
|
+
--brand-foreground: #020420;
|
|
42
|
+
|
|
43
|
+
--background: #ffffff;
|
|
44
|
+
--foreground: var(--color-slate-900);
|
|
45
|
+
--card: #ffffff;
|
|
46
|
+
--card-foreground: var(--color-slate-900);
|
|
47
|
+
--popover: #ffffff;
|
|
48
|
+
--popover-foreground: var(--color-slate-900);
|
|
49
|
+
--primary: var(--theme-primary-light);
|
|
50
|
+
--primary-foreground: var(--theme-primary-light-foreground);
|
|
51
|
+
--secondary: var(--color-slate-50);
|
|
52
|
+
--secondary-foreground: var(--color-slate-900);
|
|
53
|
+
--muted: var(--color-slate-50);
|
|
54
|
+
--muted-foreground: var(--color-slate-500);
|
|
55
|
+
--accent: var(--color-slate-100);
|
|
56
|
+
--accent-foreground: var(--color-slate-900);
|
|
57
|
+
--border: var(--color-slate-200);
|
|
58
|
+
--input: var(--color-slate-200);
|
|
59
|
+
--ring: var(--nuxt-green-600);
|
|
60
|
+
--code: var(--color-slate-50);
|
|
61
|
+
--code-foreground: var(--color-slate-900);
|
|
62
|
+
--code-border: var(--color-slate-200);
|
|
63
|
+
|
|
64
|
+
--hero-bg: #ffffff;
|
|
65
|
+
--hero-bg-muted: var(--color-slate-50);
|
|
66
|
+
--hero-blue: var(--nuxt-green-400);
|
|
67
|
+
--hero-blue-light: var(--nuxt-green-300);
|
|
68
|
+
--hero-blue-dark: var(--nuxt-green-700);
|
|
69
|
+
--hero-blue-muted: var(--nuxt-green-50);
|
|
70
|
+
--hero-mint: var(--nuxt-green-400);
|
|
71
|
+
--hero-mint-dark: var(--nuxt-green-700);
|
|
72
|
+
--hero-mint-muted: var(--nuxt-green-50);
|
|
73
|
+
--hero-mint-text: var(--nuxt-green-700);
|
|
74
|
+
--hero-yellow: var(--nuxt-green-400);
|
|
75
|
+
--hero-yellow-hover: var(--nuxt-green-500);
|
|
76
|
+
--hero-yellow-dark: var(--nuxt-green-700);
|
|
77
|
+
--hero-yellow-muted: var(--nuxt-green-50);
|
|
78
|
+
--hero-coral: var(--nuxt-green-400);
|
|
79
|
+
--hero-coral-light: var(--nuxt-green-300);
|
|
80
|
+
--hero-coral-dark: var(--nuxt-green-700);
|
|
81
|
+
--hero-coral-muted: var(--nuxt-green-50);
|
|
82
|
+
--hero-coral-text: var(--nuxt-green-700);
|
|
83
|
+
--hero-check: var(--nuxt-green-400);
|
|
84
|
+
--hero-white: #ffffff;
|
|
85
|
+
|
|
86
|
+
--agent-background: var(--color-slate-950);
|
|
87
|
+
--agent-foreground: var(--color-slate-100);
|
|
88
|
+
--agent-muted: var(--color-slate-400);
|
|
89
|
+
|
|
90
|
+
/* Nuxt UI-compatible aliases for consumer-owned examples. */
|
|
91
|
+
--ui-bg: var(--background);
|
|
92
|
+
--ui-bg-muted: var(--muted);
|
|
93
|
+
--ui-bg-elevated: var(--card);
|
|
94
|
+
--ui-bg-accented: var(--accent);
|
|
95
|
+
--ui-border: var(--border);
|
|
96
|
+
--ui-border-muted: var(--border);
|
|
97
|
+
--ui-border-accented: var(--border);
|
|
98
|
+
--ui-text: var(--foreground);
|
|
99
|
+
--ui-text-default: var(--foreground);
|
|
100
|
+
--ui-text-highlighted: var(--foreground);
|
|
101
|
+
--ui-text-muted: var(--muted-foreground);
|
|
102
|
+
--ui-text-dimmed: var(--muted-foreground);
|
|
103
|
+
--ui-text-toned: var(--muted-foreground);
|
|
104
|
+
--ui-primary: var(--primary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
html.dark[data-theme-preset="nuxt"] {
|
|
108
|
+
--background: var(--color-slate-950);
|
|
109
|
+
--foreground: var(--color-slate-50);
|
|
110
|
+
--card: var(--color-slate-900);
|
|
111
|
+
--card-foreground: var(--color-slate-50);
|
|
112
|
+
--popover: var(--color-slate-900);
|
|
113
|
+
--popover-foreground: var(--color-slate-50);
|
|
114
|
+
--primary: var(--nuxt-green-400);
|
|
115
|
+
--primary-foreground: var(--nuxt-green-950);
|
|
116
|
+
--secondary: var(--color-slate-900);
|
|
117
|
+
--secondary-foreground: var(--color-slate-50);
|
|
118
|
+
--muted: var(--color-slate-900);
|
|
119
|
+
--muted-foreground: var(--color-slate-400);
|
|
120
|
+
--accent: var(--color-slate-800);
|
|
121
|
+
--accent-foreground: var(--color-slate-50);
|
|
122
|
+
--border: var(--color-slate-800);
|
|
123
|
+
--input: var(--color-slate-700);
|
|
124
|
+
--ring: var(--nuxt-green-400);
|
|
125
|
+
--code: var(--color-slate-900);
|
|
126
|
+
--code-foreground: var(--color-slate-50);
|
|
127
|
+
--code-border: var(--color-slate-800);
|
|
128
|
+
|
|
129
|
+
--hero-bg: var(--color-slate-950);
|
|
130
|
+
--hero-bg-muted: var(--color-slate-900);
|
|
131
|
+
--hero-blue-dark: var(--nuxt-green-400);
|
|
132
|
+
--hero-mint-dark: var(--nuxt-green-400);
|
|
133
|
+
--hero-yellow-dark: var(--nuxt-green-400);
|
|
134
|
+
--hero-coral-dark: var(--nuxt-green-400);
|
|
135
|
+
--hero-blue-muted: color-mix(in oklab, var(--nuxt-green-400) 12%, var(--color-slate-900));
|
|
136
|
+
--hero-mint-muted: var(--hero-blue-muted);
|
|
137
|
+
--hero-yellow-muted: var(--hero-blue-muted);
|
|
138
|
+
--hero-coral-muted: var(--hero-blue-muted);
|
|
139
|
+
--hero-mint-text: var(--nuxt-green-300);
|
|
140
|
+
--hero-coral-text: var(--nuxt-green-300);
|
|
141
|
+
}
|
|
@@ -22,25 +22,146 @@ const props = withDefaults(
|
|
|
22
22
|
siteName: undefined,
|
|
23
23
|
logo: undefined,
|
|
24
24
|
locale: "en",
|
|
25
|
-
colorBg: "#ffffff",
|
|
26
|
-
colorFg: "#0f172a",
|
|
27
|
-
colorMuted: "#64748b",
|
|
28
|
-
colorAccent: "#2563eb",
|
|
29
25
|
},
|
|
30
26
|
);
|
|
31
27
|
|
|
32
28
|
const config = useAppConfig().ginkoDocs as GinkoDocsAppConfig;
|
|
29
|
+
const isNuxtPreset = computed(() => config.theme.preset === "nuxt");
|
|
33
30
|
|
|
34
31
|
const resolvedSiteName = computed(
|
|
35
32
|
() => props.siteName ?? getLocalizedSiteText(config.site.name, props.locale),
|
|
36
33
|
);
|
|
37
34
|
const resolvedLogo = computed(() => props.logo ?? config.site.logo?.light);
|
|
35
|
+
const resolvedNuxtIcon = computed(() => props.logo ?? "/favicon-96x96.png");
|
|
36
|
+
const resolvedWordmark = computed(() =>
|
|
37
|
+
isNuxtPreset.value ? config.site.logo?.dark : config.site.logo?.light,
|
|
38
|
+
);
|
|
39
|
+
const resolvedColorBg = computed(
|
|
40
|
+
() => props.colorBg ?? (isNuxtPreset.value ? "#07111f" : "#ffffff"),
|
|
41
|
+
);
|
|
42
|
+
const resolvedColorFg = computed(
|
|
43
|
+
() => props.colorFg ?? (isNuxtPreset.value ? "#ffffff" : "#0f172a"),
|
|
44
|
+
);
|
|
45
|
+
const resolvedColorMuted = computed(
|
|
46
|
+
() => props.colorMuted ?? (isNuxtPreset.value ? "#aab8ca" : "#64748b"),
|
|
47
|
+
);
|
|
48
|
+
const resolvedColorAccent = computed(
|
|
49
|
+
() => props.colorAccent ?? (isNuxtPreset.value ? "#00dc82" : "#2563eb"),
|
|
50
|
+
);
|
|
38
51
|
const displayTitle = computed(() => (props.title || resolvedSiteName.value).slice(0, 120));
|
|
39
52
|
const displayDescription = computed(() => (props.description ?? "").slice(0, 180));
|
|
40
53
|
</script>
|
|
41
54
|
|
|
42
55
|
<template>
|
|
43
56
|
<div
|
|
57
|
+
v-if="isNuxtPreset"
|
|
58
|
+
:style="{
|
|
59
|
+
width: '100%',
|
|
60
|
+
height: '100%',
|
|
61
|
+
display: 'flex',
|
|
62
|
+
flexDirection: 'column',
|
|
63
|
+
backgroundColor: resolvedColorBg,
|
|
64
|
+
fontFamily: 'Public Sans',
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
position: 'relative',
|
|
67
|
+
}"
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
:style="{
|
|
71
|
+
position: 'absolute',
|
|
72
|
+
top: '-360px',
|
|
73
|
+
right: '-80px',
|
|
74
|
+
width: '800px',
|
|
75
|
+
height: '800px',
|
|
76
|
+
borderRadius: '9999px',
|
|
77
|
+
backgroundImage:
|
|
78
|
+
'radial-gradient(circle, rgba(0, 220, 130, 0.58) 0%, rgba(0, 220, 130, 0.08) 48%, rgba(0, 220, 130, 0) 72%)',
|
|
79
|
+
}"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<div
|
|
83
|
+
:style="{
|
|
84
|
+
display: 'flex',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
height: '72px',
|
|
87
|
+
margin: '72px 96px 0',
|
|
88
|
+
}"
|
|
89
|
+
>
|
|
90
|
+
<img :src="resolvedNuxtIcon" :width="72" :height="72" alt="" />
|
|
91
|
+
<div
|
|
92
|
+
:style="{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
height: '72px',
|
|
96
|
+
marginLeft: '20px',
|
|
97
|
+
color: resolvedColorFg,
|
|
98
|
+
fontSize: '30px',
|
|
99
|
+
fontWeight: 700,
|
|
100
|
+
letterSpacing: '-0.03em',
|
|
101
|
+
}"
|
|
102
|
+
>
|
|
103
|
+
{{ resolvedSiteName }}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
:style="{
|
|
109
|
+
display: 'flex',
|
|
110
|
+
flexDirection: 'column',
|
|
111
|
+
justifyContent: 'flex-end',
|
|
112
|
+
flex: 1,
|
|
113
|
+
margin: '32px 96px 0',
|
|
114
|
+
}"
|
|
115
|
+
>
|
|
116
|
+
<div
|
|
117
|
+
:style="{
|
|
118
|
+
display: 'flex',
|
|
119
|
+
maxWidth: '980px',
|
|
120
|
+
marginBottom: displayDescription ? '22px' : 0,
|
|
121
|
+
fontSize: displayTitle.length > 50 ? '54px' : '72px',
|
|
122
|
+
fontWeight: 700,
|
|
123
|
+
lineHeight: 1.02,
|
|
124
|
+
color: resolvedColorFg,
|
|
125
|
+
letterSpacing: '-0.05em',
|
|
126
|
+
}"
|
|
127
|
+
>
|
|
128
|
+
{{ displayTitle }}
|
|
129
|
+
</div>
|
|
130
|
+
<div
|
|
131
|
+
v-if="displayDescription"
|
|
132
|
+
:style="{
|
|
133
|
+
display: 'flex',
|
|
134
|
+
maxWidth: '900px',
|
|
135
|
+
fontSize: '27px',
|
|
136
|
+
lineHeight: 1.4,
|
|
137
|
+
color: resolvedColorMuted,
|
|
138
|
+
}"
|
|
139
|
+
>
|
|
140
|
+
{{ displayDescription }}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div
|
|
145
|
+
:style="{
|
|
146
|
+
display: 'flex',
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
height: '74px',
|
|
149
|
+
margin: '24px 96px 56px',
|
|
150
|
+
}"
|
|
151
|
+
>
|
|
152
|
+
<img
|
|
153
|
+
v-if="resolvedWordmark"
|
|
154
|
+
:src="resolvedWordmark"
|
|
155
|
+
:width="240"
|
|
156
|
+
:height="52"
|
|
157
|
+
alt=""
|
|
158
|
+
:style="{ objectFit: 'contain', objectPosition: 'left center' }"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
v-else
|
|
44
165
|
:style="{
|
|
45
166
|
width: '100%',
|
|
46
167
|
height: '100%',
|
|
@@ -48,7 +169,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
48
169
|
flexDirection: 'column',
|
|
49
170
|
justifyContent: 'space-between',
|
|
50
171
|
padding: '72px 80px',
|
|
51
|
-
backgroundColor:
|
|
172
|
+
backgroundColor: resolvedColorBg,
|
|
52
173
|
fontFamily: 'Public Sans',
|
|
53
174
|
}"
|
|
54
175
|
>
|
|
@@ -59,7 +180,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
59
180
|
display: 'flex',
|
|
60
181
|
fontSize: '30px',
|
|
61
182
|
fontWeight: 700,
|
|
62
|
-
color:
|
|
183
|
+
color: resolvedColorFg,
|
|
63
184
|
}"
|
|
64
185
|
>
|
|
65
186
|
{{ resolvedSiteName }}
|
|
@@ -73,7 +194,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
73
194
|
fontSize: displayTitle.length > 50 ? '56px' : '68px',
|
|
74
195
|
fontWeight: 700,
|
|
75
196
|
lineHeight: 1.15,
|
|
76
|
-
color:
|
|
197
|
+
color: resolvedColorFg,
|
|
77
198
|
letterSpacing: '-0.02em',
|
|
78
199
|
}"
|
|
79
200
|
>
|
|
@@ -85,7 +206,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
85
206
|
display: 'flex',
|
|
86
207
|
fontSize: '30px',
|
|
87
208
|
lineHeight: 1.4,
|
|
88
|
-
color:
|
|
209
|
+
color: resolvedColorMuted,
|
|
89
210
|
}"
|
|
90
211
|
>
|
|
91
212
|
{{ displayDescription }}
|
|
@@ -99,7 +220,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
99
220
|
width: '56px',
|
|
100
221
|
height: '8px',
|
|
101
222
|
borderRadius: '9999px',
|
|
102
|
-
backgroundColor:
|
|
223
|
+
backgroundColor: resolvedColorAccent,
|
|
103
224
|
}"
|
|
104
225
|
/>
|
|
105
226
|
<div
|
|
@@ -108,7 +229,7 @@ const displayDescription = computed(() => (props.description ?? "").slice(0, 180
|
|
|
108
229
|
width: '20px',
|
|
109
230
|
height: '8px',
|
|
110
231
|
borderRadius: '9999px',
|
|
111
|
-
backgroundColor:
|
|
232
|
+
backgroundColor: resolvedColorMuted,
|
|
112
233
|
opacity: 0.4,
|
|
113
234
|
}"
|
|
114
235
|
/>
|
|
@@ -73,7 +73,7 @@ onBeforeUnmount(() => {
|
|
|
73
73
|
ref="bannerElement"
|
|
74
74
|
role="region"
|
|
75
75
|
:aria-label="banner.text"
|
|
76
|
-
class="relative z-30 bg-
|
|
76
|
+
class="relative z-30 bg-brand px-10 py-2.5 text-center text-sm leading-5 font-medium text-brand-foreground"
|
|
77
77
|
>
|
|
78
78
|
<span class="mx-auto block max-w-[min(100%,52rem)]">
|
|
79
79
|
{{ banner.text }}
|
|
@@ -87,7 +87,7 @@ onBeforeUnmount(() => {
|
|
|
87
87
|
</span>
|
|
88
88
|
<button
|
|
89
89
|
type="button"
|
|
90
|
-
class="absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-
|
|
90
|
+
class="absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-brand-foreground/50 focus-visible:outline-none"
|
|
91
91
|
:aria-label="t('banner.dismiss')"
|
|
92
92
|
@click="dismissed = true"
|
|
93
93
|
>
|
|
@@ -22,10 +22,10 @@ const resources = computed(() => [
|
|
|
22
22
|
<div class="max-w-sm">
|
|
23
23
|
<NuxtLink
|
|
24
24
|
:to="localizedPath('home')"
|
|
25
|
+
:aria-label="site.name"
|
|
25
26
|
class="flex items-center gap-2 font-semibold text-foreground"
|
|
26
27
|
>
|
|
27
28
|
<SiteLogoMark />
|
|
28
|
-
<span>{{ site.name }}</span>
|
|
29
29
|
</NuxtLink>
|
|
30
30
|
<p class="mt-3 text-sm leading-relaxed text-muted-foreground">
|
|
31
31
|
{{ site.description }}
|
|
@@ -316,7 +316,7 @@ watch(
|
|
|
316
316
|
class="flex items-center rounded-md px-2 py-2 text-lg font-semibold transition-colors"
|
|
317
317
|
:class="
|
|
318
318
|
link.active
|
|
319
|
-
? 'bg-
|
|
319
|
+
? 'bg-muted text-foreground'
|
|
320
320
|
: 'text-foreground hover:bg-muted/60'
|
|
321
321
|
"
|
|
322
322
|
:aria-current="link.active ? 'true' : undefined"
|
|
@@ -19,7 +19,15 @@ defineEmits<{ navigate: [] }>();
|
|
|
19
19
|
:class="headerUtilityButtonClass"
|
|
20
20
|
@click="$emit('navigate')"
|
|
21
21
|
>
|
|
22
|
-
<Icon :name="link.icon ?? 'lucide:link'" class="
|
|
22
|
+
<Icon :name="link.icon ?? 'lucide:link'" class="social-icon" aria-hidden="true" />
|
|
23
23
|
</NuxtLink>
|
|
24
24
|
</div>
|
|
25
25
|
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.social-icon {
|
|
29
|
+
width: 1.25rem;
|
|
30
|
+
height: 1.25rem;
|
|
31
|
+
flex: none;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -8,7 +8,7 @@ export const badgeVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: "border-transparent bg-
|
|
11
|
+
default: "border-transparent bg-brand text-brand-foreground [a&]:hover:bg-brand/90",
|
|
12
12
|
secondary:
|
|
13
13
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
14
14
|
destructive:
|
|
@@ -8,7 +8,7 @@ export const buttonVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: "bg-
|
|
11
|
+
default: "bg-brand text-brand-foreground hover:bg-brand/90",
|
|
12
12
|
destructive:
|
|
13
13
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
14
14
|
outline:
|
|
@@ -66,14 +66,10 @@ export function resolveMainNav(
|
|
|
66
66
|
return items;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Brand names are not translated, so the labels are literals. The icons are
|
|
71
|
-
* bundled defaults — Lucide has no Discord mark, so chat is the closest generic
|
|
72
|
-
* stand-in and sites that ship the real one override `icon` per entry.
|
|
73
|
-
*/
|
|
69
|
+
/** Brand names are not translated, so the labels are literals. */
|
|
74
70
|
const socialDefaults: Record<GinkoDocsSocialPlatform, { label: string; icon: string }> = {
|
|
75
|
-
github: { label: "GitHub", icon: "
|
|
76
|
-
discord: { label: "Discord", icon: "
|
|
71
|
+
github: { label: "GitHub", icon: "ginko-social:github" },
|
|
72
|
+
discord: { label: "Discord", icon: "ginko-social:discord" },
|
|
77
73
|
linkedin: { label: "LinkedIn", icon: "lucide:linkedin" },
|
|
78
74
|
};
|
|
79
75
|
|
|
@@ -3,10 +3,11 @@ import { isLocaleCode, localizedPath } from "../../i18n/locales";
|
|
|
3
3
|
import type { LocalizedRouteKey } from "../../i18n/routes";
|
|
4
4
|
|
|
5
5
|
export function useLocalizedPath() {
|
|
6
|
-
const { locale, t } = useI18n();
|
|
6
|
+
const { defaultLocale, locale, t } = useI18n();
|
|
7
7
|
|
|
8
8
|
return (key: LocalizedRouteKey) => {
|
|
9
9
|
const path = t(`routes.${key}`);
|
|
10
|
-
|
|
10
|
+
const primaryLocale = isLocaleCode(defaultLocale) ? defaultLocale : undefined;
|
|
11
|
+
return isLocaleCode(locale.value) ? localizedPath(locale.value, path, primaryLocale) : path;
|
|
11
12
|
};
|
|
12
13
|
}
|