@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.
Files changed (50) hide show
  1. package/README.md +12 -4
  2. package/app/app.config.ts +2 -0
  3. package/app/app.vue +26 -2
  4. package/app/assets/css/tailwind.css +24 -0
  5. package/app/assets/css/theme-presets.css +141 -0
  6. package/app/components/OgImage/GinkoDocs.satori.vue +131 -10
  7. package/app/components/site/SiteBanner.vue +2 -2
  8. package/app/components/site/SiteFooter.vue +1 -1
  9. package/app/components/site/SiteHeader.vue +1 -1
  10. package/app/components/site/SiteSocialLinks.vue +9 -1
  11. package/app/components/ui/badge/index.ts +1 -1
  12. package/app/components/ui/button/index.ts +1 -1
  13. package/app/composables/site-navigation.utils.ts +3 -7
  14. package/app/composables/useLocalizedPath.ts +3 -2
  15. package/app/error.vue +18 -8
  16. package/app/features/docs/components/DocsPageContent.vue +2 -2
  17. package/app/features/docs/components/DocsSidebarItem.vue +2 -2
  18. package/app/features/docs/components/DocsSidebarList.vue +1 -1
  19. package/app/features/docs/components/DocsToc.vue +2 -2
  20. package/app/features/search/command-center.ts +33 -0
  21. package/app/features/search/components/SiteCommandCenter.vue +25 -9
  22. package/app/features/search/components/SiteSearchPageHighlight.vue +4 -2
  23. package/app/features/search/useCommandCenter.ts +2 -2
  24. package/app/layouts/docs.vue +1 -1
  25. package/app/pages/index.vue +22 -3
  26. package/app/plugins/ginko-docs-theme.ts +1 -0
  27. package/content-collections.ts +13 -5
  28. package/content.js +16 -10
  29. package/content.ts +25 -8
  30. package/i18n/messages/global/command.ts +1 -0
  31. package/icon-bundle.ts +24 -4
  32. package/index.d.ts +1 -0
  33. package/modules/feature-routing.ts +47 -22
  34. package/modules/syntax-highlighting.ts +5 -0
  35. package/nuxt.config.ts +7 -7
  36. package/package.json +9 -4
  37. package/runtime/generate-favicons.mjs +124 -0
  38. package/server/plugins/sitemap-policy.ts +19 -0
  39. package/server/routes/.well-known/ai-catalog.json.get.ts +10 -0
  40. package/server/routes/[locale]/blog/rss.xml.get.ts +6 -1
  41. package/server/routes/blog/rss.xml.get.ts +10 -2
  42. package/server/routes/mcp/server-card.get.ts +14 -0
  43. package/server/utils/blog-feed.ts +24 -6
  44. package/server/utils/mcp-server-card.ts +45 -0
  45. package/server/utils/redirects.ts +10 -3
  46. package/server/utils/redirects.utils.ts +10 -7
  47. package/server/utils/sitemap.ts +50 -0
  48. package/shared/theme-presets.ts +3 -0
  49. package/shared/types/app-config.ts +20 -2
  50. 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/web-app-manifest-512x512.png" width="128" alt="Ginko Docs icon">
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.35 or later
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.3.0 @lupinum/ginko-content@0.4.0-rc.2
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
- url: "https://docs.example.com",
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: colorBg,
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: colorFg,
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: colorFg,
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: colorMuted,
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: colorAccent,
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: colorMuted,
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-primary px-10 py-2.5 text-center text-sm leading-5 font-medium text-primary-foreground"
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-primary-foreground/50 focus-visible:outline-none"
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-primary/10 text-primary'
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="size-[18px]" aria-hidden="true" />
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-primary text-primary-foreground [a&]:hover:bg-primary/90",
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-primary text-primary-foreground hover:bg-primary/90",
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: "lucide:github" },
76
- discord: { label: "Discord", icon: "lucide:message-circle" },
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
- return isLocaleCode(locale.value) ? localizedPath(locale.value, path) : path;
10
+ const primaryLocale = isLocaleCode(defaultLocale) ? defaultLocale : undefined;
11
+ return isLocaleCode(locale.value) ? localizedPath(locale.value, path, primaryLocale) : path;
11
12
  };
12
13
  }