@lupinum/ginko-docs 0.3.0-rc.5 → 0.3.0
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 +2 -2
- package/app/app.config.ts +1 -0
- package/app/assets/css/prose.css +33 -1
- package/app/components/site/ModeToggle.vue +55 -106
- package/app/components/site/SiteHeader.vue +182 -195
- package/app/components/site/SiteHeroCode.vue +12 -3
- package/app/components/site/SiteInteractionLayer.vue +9 -2
- package/app/components/site/SiteLocaleSwitcher.vue +28 -122
- package/app/components/site/SiteSocialLinks.vue +10 -16
- package/app/components/site/header-utils.ts +3 -0
- package/app/components/site/mode-toggle.utils.ts +18 -0
- package/app/features/search/useCommandCenter.ts +1 -9
- package/app/plugins/ginko-docs-theme.ts +1 -0
- package/i18n/messages/global/nav.ts +5 -0
- package/i18n/messages/global/theme.ts +2 -0
- package/icon-bundle.ts +2 -0
- package/index.d.ts +18 -0
- package/modules/syntax-highlighting.ts +120 -0
- package/nuxt.config.ts +2 -1
- package/package.json +3 -2
- package/shared/types/app-config.ts +2 -0
- package/shared/types/nuxt-config.ts +14 -0
|
@@ -11,9 +11,10 @@ import { cn } from "#ginko-docs/utils";
|
|
|
11
11
|
import { computed } from "vue";
|
|
12
12
|
import { useI18n } from "#imports";
|
|
13
13
|
import { useLocalizedRouteSwitch } from "#ginko-docs/composables/useLocalizedRouteSwitch";
|
|
14
|
+
import { headerUtilityButtonClass } from "#ginko-docs/components/site/header-utils";
|
|
14
15
|
import { locales as configuredLocales } from "../../../i18n/locales";
|
|
15
16
|
|
|
16
|
-
type LocaleSwitcherVariant = "dropdown" | "menu-row"
|
|
17
|
+
type LocaleSwitcherVariant = "dropdown" | "menu-row";
|
|
17
18
|
|
|
18
19
|
type LocaleEntry = {
|
|
19
20
|
code: string;
|
|
@@ -21,7 +22,6 @@ type LocaleEntry = {
|
|
|
21
22
|
name?: string;
|
|
22
23
|
nativeName?: string;
|
|
23
24
|
shortLabel?: string;
|
|
24
|
-
flagIcon?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const props = withDefaults(
|
|
@@ -56,7 +56,6 @@ function normalizeLocale(entry: string | LocaleEntry) {
|
|
|
56
56
|
name: configured?.name ?? entry.toUpperCase(),
|
|
57
57
|
nativeName: configured?.nativeName ?? configured?.name ?? entry.toUpperCase(),
|
|
58
58
|
shortLabel: configured?.shortLabel ?? entry.toUpperCase(),
|
|
59
|
-
flagIcon: configured?.flagIcon,
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -68,7 +67,6 @@ function normalizeLocale(entry: string | LocaleEntry) {
|
|
|
68
67
|
nativeName:
|
|
69
68
|
entry.nativeName ?? entry.name ?? configured?.nativeName ?? entry.code.toUpperCase(),
|
|
70
69
|
shortLabel: entry.shortLabel ?? configured?.shortLabel ?? entry.code.toUpperCase(),
|
|
71
|
-
flagIcon: entry.flagIcon ?? configured?.flagIcon,
|
|
72
70
|
};
|
|
73
71
|
}
|
|
74
72
|
|
|
@@ -86,11 +84,6 @@ const localeLinks = computed(() =>
|
|
|
86
84
|
const currentLocale = computed(
|
|
87
85
|
() => localeLinks.value.find((entry) => entry.current) ?? localeLinks.value[0],
|
|
88
86
|
);
|
|
89
|
-
const currentMobileLocaleLabel = computed(() => {
|
|
90
|
-
const code = currentLocale.value?.code;
|
|
91
|
-
if (!code) return "";
|
|
92
|
-
return currentLocale.value?.name ?? code.toUpperCase();
|
|
93
|
-
});
|
|
94
87
|
|
|
95
88
|
function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unknown }) {
|
|
96
89
|
emit("navigate");
|
|
@@ -105,22 +98,11 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
105
98
|
<ClientOnly>
|
|
106
99
|
<DropdownMenu>
|
|
107
100
|
<DropdownMenuTrigger as-child>
|
|
108
|
-
<
|
|
109
|
-
<Icon
|
|
110
|
-
|
|
111
|
-
:name="currentLocale.flagIcon"
|
|
112
|
-
class="size-4 rounded-full"
|
|
113
|
-
aria-hidden="true"
|
|
114
|
-
/>
|
|
115
|
-
<span class="text-xs font-semibold">{{ currentLocale?.shortLabel }}</span>
|
|
116
|
-
<Icon
|
|
117
|
-
name="lucide:chevron-down"
|
|
118
|
-
class="size-3.5 text-muted-foreground"
|
|
119
|
-
aria-hidden="true"
|
|
120
|
-
/>
|
|
121
|
-
</Button>
|
|
101
|
+
<button type="button" :class="headerUtilityButtonClass" :aria-label="t('nav.language')">
|
|
102
|
+
<Icon name="lucide:languages" class="size-[18px]" aria-hidden="true" />
|
|
103
|
+
</button>
|
|
122
104
|
</DropdownMenuTrigger>
|
|
123
|
-
<DropdownMenuContent align="end" class="w-
|
|
105
|
+
<DropdownMenuContent align="end" class="w-52">
|
|
124
106
|
<DropdownMenuItem v-for="entry in localeLinks" :key="entry.code" as-child>
|
|
125
107
|
<NuxtLink
|
|
126
108
|
:to="entry.to"
|
|
@@ -128,17 +110,13 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
128
110
|
:lang="entry.code"
|
|
129
111
|
:aria-label="entry.name"
|
|
130
112
|
:aria-current="entry.current ? 'page' : undefined"
|
|
131
|
-
class="flex w-full items-center gap-
|
|
113
|
+
class="flex w-full items-center gap-3"
|
|
132
114
|
@click="trackLocaleNavigation(entry)"
|
|
133
115
|
>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class="size-4 rounded-full"
|
|
138
|
-
aria-hidden="true"
|
|
139
|
-
/>
|
|
116
|
+
<span class="w-7 text-xs font-semibold tracking-wide text-muted-foreground">{{
|
|
117
|
+
entry.shortLabel
|
|
118
|
+
}}</span>
|
|
140
119
|
<span class="flex-1">{{ entry.nativeName }}</span>
|
|
141
|
-
<span class="text-xs text-muted-foreground">{{ entry.shortLabel }}</span>
|
|
142
120
|
<Icon
|
|
143
121
|
v-if="entry.current"
|
|
144
122
|
name="lucide:check"
|
|
@@ -151,76 +129,39 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
151
129
|
</DropdownMenu>
|
|
152
130
|
|
|
153
131
|
<template #fallback>
|
|
154
|
-
|
|
155
|
-
<span class="inline-flex h-9 w-[4.25rem] rounded-md" aria-hidden="true" />
|
|
132
|
+
<span class="inline-flex size-9 shrink-0 rounded-lg" aria-hidden="true" />
|
|
156
133
|
</template>
|
|
157
134
|
</ClientOnly>
|
|
158
135
|
</div>
|
|
159
136
|
|
|
160
137
|
<div
|
|
161
|
-
v-else-if="
|
|
162
|
-
localeLinks.length > 1 &&
|
|
163
|
-
(variant === 'menu-row' || variant === 'menu-tile' || variant === 'pill')
|
|
164
|
-
"
|
|
138
|
+
v-else-if="localeLinks.length > 1 && variant === 'menu-row'"
|
|
165
139
|
:class="cn('items-center', props.class)"
|
|
166
140
|
>
|
|
167
141
|
<DropdownMenu>
|
|
168
142
|
<DropdownMenuTrigger as-child>
|
|
169
143
|
<Button
|
|
170
|
-
v-if="variant === 'pill'"
|
|
171
|
-
variant="outline"
|
|
172
|
-
class="h-9 gap-2 rounded-full px-4 text-sm font-semibold"
|
|
173
|
-
:aria-label="t('nav.language')"
|
|
174
|
-
>
|
|
175
|
-
<Icon
|
|
176
|
-
v-if="currentLocale?.flagIcon"
|
|
177
|
-
:name="currentLocale.flagIcon"
|
|
178
|
-
class="size-4 rounded-full"
|
|
179
|
-
aria-hidden="true"
|
|
180
|
-
/>
|
|
181
|
-
<span>{{ currentLocale?.shortLabel }}</span>
|
|
182
|
-
</Button>
|
|
183
|
-
<Button
|
|
184
|
-
v-else
|
|
185
144
|
variant="ghost"
|
|
186
|
-
|
|
187
|
-
cn(
|
|
188
|
-
variant === 'menu-tile'
|
|
189
|
-
? 'h-14 w-full justify-between rounded-none px-3.5 text-sm font-semibold hover:bg-muted/40'
|
|
190
|
-
: 'h-[4.25rem] w-full justify-between rounded-none px-5 text-base font-semibold hover:bg-transparent',
|
|
191
|
-
)
|
|
192
|
-
"
|
|
145
|
+
class="h-14 w-full justify-between rounded-none px-5 text-base font-semibold hover:bg-transparent"
|
|
193
146
|
:aria-label="t('nav.language')"
|
|
194
147
|
>
|
|
195
|
-
<span
|
|
196
|
-
class="flex min-w-0 items-center"
|
|
197
|
-
:class="variant === 'menu-tile' ? 'gap-3' : 'gap-4'"
|
|
198
|
-
>
|
|
148
|
+
<span class="flex min-w-0 items-center gap-4">
|
|
199
149
|
<span
|
|
200
|
-
|
|
201
|
-
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted/60"
|
|
150
|
+
class="flex size-11 shrink-0 items-center justify-center rounded-lg bg-muted/60 text-foreground"
|
|
202
151
|
>
|
|
203
|
-
<Icon
|
|
204
|
-
</span>
|
|
205
|
-
<Icon
|
|
206
|
-
v-else-if="currentLocale?.flagIcon"
|
|
207
|
-
:name="currentLocale.flagIcon"
|
|
208
|
-
class="size-7 shrink-0 rounded-full"
|
|
209
|
-
aria-hidden="true"
|
|
210
|
-
/>
|
|
211
|
-
<span class="truncate">
|
|
212
|
-
{{ variant === "menu-tile" ? currentMobileLocaleLabel : currentLocale?.shortLabel }}
|
|
152
|
+
<Icon name="lucide:languages" class="size-5" aria-hidden="true" />
|
|
213
153
|
</span>
|
|
154
|
+
<span class="truncate">{{ t("nav.languageLabel") }}</span>
|
|
155
|
+
</span>
|
|
156
|
+
<span class="flex items-center gap-2 text-muted-foreground">
|
|
157
|
+
<span class="text-sm font-semibold text-foreground">{{
|
|
158
|
+
currentLocale?.nativeName
|
|
159
|
+
}}</span>
|
|
160
|
+
<Icon name="lucide:chevron-down" class="size-5" aria-hidden="true" />
|
|
214
161
|
</span>
|
|
215
|
-
<Icon
|
|
216
|
-
name="lucide:chevron-down"
|
|
217
|
-
class="text-muted-foreground"
|
|
218
|
-
:class="variant === 'menu-tile' ? 'size-4' : 'size-5'"
|
|
219
|
-
aria-hidden="true"
|
|
220
|
-
/>
|
|
221
162
|
</Button>
|
|
222
163
|
</DropdownMenuTrigger>
|
|
223
|
-
<DropdownMenuContent align="end" class="w-
|
|
164
|
+
<DropdownMenuContent align="end" class="w-52" portal-disabled>
|
|
224
165
|
<DropdownMenuItem v-for="entry in localeLinks" :key="entry.code" as-child>
|
|
225
166
|
<NuxtLink
|
|
226
167
|
:to="entry.to"
|
|
@@ -228,17 +169,13 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
228
169
|
:lang="entry.code"
|
|
229
170
|
:aria-label="entry.name"
|
|
230
171
|
:aria-current="entry.current ? 'page' : undefined"
|
|
231
|
-
class="flex w-full items-center gap-
|
|
172
|
+
class="flex w-full items-center gap-3"
|
|
232
173
|
@click="trackLocaleNavigation(entry)"
|
|
233
174
|
>
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class="size-4 rounded-full"
|
|
238
|
-
aria-hidden="true"
|
|
239
|
-
/>
|
|
175
|
+
<span class="w-7 text-xs font-semibold tracking-wide text-muted-foreground">{{
|
|
176
|
+
entry.shortLabel
|
|
177
|
+
}}</span>
|
|
240
178
|
<span class="flex-1">{{ entry.nativeName }}</span>
|
|
241
|
-
<span class="text-xs text-muted-foreground">{{ entry.shortLabel }}</span>
|
|
242
179
|
<Icon
|
|
243
180
|
v-if="entry.current"
|
|
244
181
|
name="lucide:check"
|
|
@@ -250,35 +187,4 @@ function trackLocaleNavigation(_entry: { code: string; current: boolean; to: unk
|
|
|
250
187
|
</DropdownMenuContent>
|
|
251
188
|
</DropdownMenu>
|
|
252
189
|
</div>
|
|
253
|
-
|
|
254
|
-
<div
|
|
255
|
-
v-else-if="localeLinks.length > 1"
|
|
256
|
-
:class="cn('items-center gap-1 rounded-md border border-border p-0.5', props.class)"
|
|
257
|
-
:aria-label="t('nav.language')"
|
|
258
|
-
>
|
|
259
|
-
<NuxtLink
|
|
260
|
-
v-for="entry in localeLinks"
|
|
261
|
-
:key="entry.code"
|
|
262
|
-
:to="entry.to"
|
|
263
|
-
:hreflang="entry.language"
|
|
264
|
-
:lang="entry.code"
|
|
265
|
-
:aria-label="entry.name"
|
|
266
|
-
:aria-current="entry.current ? 'page' : undefined"
|
|
267
|
-
class="inline-flex h-8 items-center gap-1.5 rounded px-2 text-xs font-medium transition-colors"
|
|
268
|
-
:class="
|
|
269
|
-
entry.current
|
|
270
|
-
? 'bg-muted text-foreground'
|
|
271
|
-
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
|
272
|
-
"
|
|
273
|
-
@click="trackLocaleNavigation(entry)"
|
|
274
|
-
>
|
|
275
|
-
<Icon
|
|
276
|
-
v-if="entry.flagIcon"
|
|
277
|
-
:name="entry.flagIcon"
|
|
278
|
-
class="size-4 rounded-full"
|
|
279
|
-
aria-hidden="true"
|
|
280
|
-
/>
|
|
281
|
-
<span>{{ entry.shortLabel }}</span>
|
|
282
|
-
</NuxtLink>
|
|
283
|
-
</div>
|
|
284
190
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Button } from "#ginko-docs/components/ui/button";
|
|
3
2
|
import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
|
|
3
|
+
import { headerUtilityButtonClass } from "#ginko-docs/components/site/header-utils";
|
|
4
4
|
|
|
5
5
|
const { socialLinks } = useSiteNavigation();
|
|
6
6
|
|
|
@@ -9,23 +9,17 @@ defineEmits<{ navigate: [] }>();
|
|
|
9
9
|
|
|
10
10
|
<template>
|
|
11
11
|
<div v-if="socialLinks.length" class="flex items-center">
|
|
12
|
-
<
|
|
12
|
+
<NuxtLink
|
|
13
13
|
v-for="link in socialLinks"
|
|
14
14
|
:key="link.href"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
:to="link.href"
|
|
16
|
+
target="_blank"
|
|
17
|
+
rel="noopener noreferrer"
|
|
18
|
+
:aria-label="link.label"
|
|
19
|
+
:class="headerUtilityButtonClass"
|
|
20
|
+
@click="$emit('navigate')"
|
|
19
21
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
target="_blank"
|
|
23
|
-
rel="noopener noreferrer"
|
|
24
|
-
:aria-label="link.label"
|
|
25
|
-
@click="$emit('navigate')"
|
|
26
|
-
>
|
|
27
|
-
<Icon :name="link.icon ?? 'lucide:link'" class="size-[18px]" aria-hidden="true" />
|
|
28
|
-
</NuxtLink>
|
|
29
|
-
</Button>
|
|
22
|
+
<Icon :name="link.icon ?? 'lucide:link'" class="size-[18px]" aria-hidden="true" />
|
|
23
|
+
</NuxtLink>
|
|
30
24
|
</div>
|
|
31
25
|
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Shared ghost geometry for header utility controls (theme, locale, social). */
|
|
2
|
+
export const headerUtilityButtonClass =
|
|
3
|
+
"inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring outline-none";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type RenderedColorMode = "light" | "dark";
|
|
2
|
+
/**
|
|
3
|
+
* Next stored preference for a two-state theme toggle.
|
|
4
|
+
*
|
|
5
|
+
* The page follows the operating system until the first interaction. After
|
|
6
|
+
* that interaction, the reader's explicit light or dark choice remains fixed.
|
|
7
|
+
*/
|
|
8
|
+
export function nextColorModePreference(currentRendered: RenderedColorMode): RenderedColorMode {
|
|
9
|
+
return currentRendered === "dark" ? "light" : "dark";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function themeToggleIcon(isDark: boolean) {
|
|
13
|
+
return isDark ? "lucide:moon" : "lucide:sun";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function themeToggleLabelKey(isDark: boolean) {
|
|
17
|
+
return isDark ? "theme.switchToLight" : "theme.switchToDark";
|
|
18
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocalStorage } from "@vueuse/core";
|
|
2
2
|
import { useContentSearch } from "@lupinum/ginko-content/client";
|
|
3
3
|
import { computed, watch } from "vue";
|
|
4
|
-
import { navigateTo, useI18n,
|
|
4
|
+
import { navigateTo, useI18n, useState } from "#imports";
|
|
5
5
|
import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
|
|
6
6
|
import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
|
|
7
7
|
import {
|
|
@@ -56,7 +56,6 @@ function flattenNavItems(items: DocsNavigationItem[]): DocsNavigationItem[] {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export async function useCommandCenter() {
|
|
59
|
-
const route = useRoute();
|
|
60
59
|
const { locale, t } = useI18n();
|
|
61
60
|
const localizedPath = useLocalizedPath();
|
|
62
61
|
const {
|
|
@@ -279,13 +278,6 @@ export async function useCommandCenter() {
|
|
|
279
278
|
searchQuery.value = value;
|
|
280
279
|
});
|
|
281
280
|
|
|
282
|
-
watch(
|
|
283
|
-
() => route.fullPath,
|
|
284
|
-
() => {
|
|
285
|
-
closeCommandCenter();
|
|
286
|
-
},
|
|
287
|
-
);
|
|
288
|
-
|
|
289
281
|
return {
|
|
290
282
|
open,
|
|
291
283
|
query,
|
|
@@ -13,6 +13,11 @@ export const nav = {
|
|
|
13
13
|
openMenu: { de: "Menü öffnen", en: "Open menu" },
|
|
14
14
|
closeMenu: { de: "Menü schließen", en: "Close menu" },
|
|
15
15
|
search: { de: "Suchen", en: "Search" },
|
|
16
|
+
searchDocumentation: {
|
|
17
|
+
de: "Dokumentation durchsuchen…",
|
|
18
|
+
en: "Search documentation…",
|
|
19
|
+
},
|
|
20
|
+
languageLabel: { de: "Sprache", en: "Language" },
|
|
16
21
|
resources: { de: "Ressourcen", en: "Resources" },
|
|
17
22
|
company: { de: "Unternehmen", en: "Company" },
|
|
18
23
|
documentation: { de: "Dokumentation", en: "Documentation" },
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const theme = {
|
|
2
2
|
label: { de: "Darstellung", en: "Appearance" },
|
|
3
3
|
toggle: { de: "Darstellung umschalten", en: "Toggle theme" },
|
|
4
|
+
switchToDark: { de: "Zu Dunkelmodus wechseln", en: "Switch to dark mode" },
|
|
5
|
+
switchToLight: { de: "Zu Hellmodus wechseln", en: "Switch to light mode" },
|
|
4
6
|
light: { de: "Hell", en: "Light" },
|
|
5
7
|
dark: { de: "Dunkel", en: "Dark" },
|
|
6
8
|
system: { de: "System", en: "System" },
|
package/icon-bundle.ts
CHANGED
|
@@ -4,6 +4,7 @@ export const layerIconNames = [
|
|
|
4
4
|
"circle-flags:de",
|
|
5
5
|
"circle-flags:us",
|
|
6
6
|
"logos:bun",
|
|
7
|
+
"logos:discord-icon",
|
|
7
8
|
"logos:npm-icon",
|
|
8
9
|
"logos:nuxt-icon",
|
|
9
10
|
"logos:pnpm",
|
|
@@ -52,6 +53,7 @@ export const layerIconNames = [
|
|
|
52
53
|
"lucide:info",
|
|
53
54
|
"lucide:lightbulb",
|
|
54
55
|
"lucide:link",
|
|
56
|
+
"lucide:languages",
|
|
55
57
|
"lucide:linkedin",
|
|
56
58
|
"lucide:loader-circle",
|
|
57
59
|
"lucide:list",
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { GinkoDocsAppConfig } from "./shared/types/app-config";
|
|
2
|
+
import type {
|
|
3
|
+
GinkoDocsNuxtConfig,
|
|
4
|
+
GinkoDocsSyntaxHighlightingConfig,
|
|
5
|
+
} from "./shared/types/nuxt-config";
|
|
2
6
|
|
|
3
7
|
type GinkoDocsAppConfigInput<T> = T extends readonly unknown[]
|
|
4
8
|
? T
|
|
@@ -14,6 +18,20 @@ declare module "nuxt/schema" {
|
|
|
14
18
|
interface AppConfigInput {
|
|
15
19
|
ginkoDocs?: GinkoDocsAppConfigInput<GinkoDocsAppConfig>;
|
|
16
20
|
}
|
|
21
|
+
|
|
22
|
+
interface NuxtConfig {
|
|
23
|
+
ginkoDocs?: GinkoDocsNuxtConfig;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NuxtOptions {
|
|
27
|
+
ginkoDocs?: GinkoDocsNuxtConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PublicRuntimeConfig {
|
|
31
|
+
ginkoDocs?: {
|
|
32
|
+
syntaxHighlighting?: GinkoDocsSyntaxHighlightingConfig;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
17
35
|
}
|
|
18
36
|
|
|
19
37
|
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { defineNuxtModule } from "@nuxt/kit";
|
|
2
|
+
import { bundledThemes } from "shiki";
|
|
3
|
+
import type { BundledTheme } from "shiki";
|
|
4
|
+
import type {
|
|
5
|
+
GinkoDocsSyntaxHighlightingConfig,
|
|
6
|
+
GinkoDocsNuxtConfig,
|
|
7
|
+
} from "../shared/types/nuxt-config";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_SYNTAX_THEMES = {
|
|
10
|
+
light: "light-plus",
|
|
11
|
+
dark: "dark-plus",
|
|
12
|
+
} as const satisfies GinkoDocsSyntaxHighlightingConfig["themes"];
|
|
13
|
+
|
|
14
|
+
type ShikiPluginOptions = {
|
|
15
|
+
preStyles?: boolean;
|
|
16
|
+
transformers?: unknown[];
|
|
17
|
+
themes?: { light: unknown; dark: unknown };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MarkdownPlugin = string | [string, Record<string, unknown>?];
|
|
21
|
+
|
|
22
|
+
export async function resolveBundledTheme(name: string, configPath: string) {
|
|
23
|
+
const loader = bundledThemes[name as BundledTheme];
|
|
24
|
+
if (!loader) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid Ginko Docs syntax theme "${name}".\n\nExpected a bundled Shiki theme name at:\n${configPath}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mod = await loader();
|
|
31
|
+
return mod.default ?? mod;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findShikiPlugin(plugins: MarkdownPlugin[]) {
|
|
35
|
+
for (let index = 0; index < plugins.length; index += 1) {
|
|
36
|
+
const plugin = plugins[index];
|
|
37
|
+
if (plugin === "shiki") {
|
|
38
|
+
return { index, options: {} as ShikiPluginOptions };
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(plugin) && plugin[0] === "shiki") {
|
|
41
|
+
return { index, options: (plugin[1] ?? {}) as ShikiPluginOptions };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function countShikiPlugins(plugins: MarkdownPlugin[]) {
|
|
48
|
+
return plugins.filter(
|
|
49
|
+
(plugin) => plugin === "shiki" || (Array.isArray(plugin) && plugin[0] === "shiki"),
|
|
50
|
+
).length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function patchShikiThemes(
|
|
54
|
+
plugins: MarkdownPlugin[],
|
|
55
|
+
themes: GinkoDocsSyntaxHighlightingConfig["themes"],
|
|
56
|
+
) {
|
|
57
|
+
const shiki = findShikiPlugin(plugins);
|
|
58
|
+
if (!shiki) {
|
|
59
|
+
throw new Error("Ginko Docs could not locate the Shiki Markdown plugin to patch.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const [light, dark] = await Promise.all([
|
|
63
|
+
resolveBundledTheme(themes.light, "ginkoDocs.syntaxHighlighting.themes.light"),
|
|
64
|
+
resolveBundledTheme(themes.dark, "ginkoDocs.syntaxHighlighting.themes.dark"),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
shiki.options.themes = { light, dark };
|
|
68
|
+
|
|
69
|
+
const plugin = plugins[shiki.index];
|
|
70
|
+
if (Array.isArray(plugin)) {
|
|
71
|
+
plugin[1] = shiki.options;
|
|
72
|
+
} else {
|
|
73
|
+
plugins[shiki.index] = ["shiki", shiki.options];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function mergeSyntaxRuntimeConfig(
|
|
78
|
+
current: Record<string, unknown> | undefined,
|
|
79
|
+
themes: GinkoDocsSyntaxHighlightingConfig["themes"],
|
|
80
|
+
) {
|
|
81
|
+
return {
|
|
82
|
+
...current,
|
|
83
|
+
syntaxHighlighting: {
|
|
84
|
+
themes: {
|
|
85
|
+
light: themes.light,
|
|
86
|
+
dark: themes.dark,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default defineNuxtModule<GinkoDocsNuxtConfig>({
|
|
93
|
+
meta: {
|
|
94
|
+
name: "ginko-docs-syntax-highlighting",
|
|
95
|
+
configKey: "ginkoDocs",
|
|
96
|
+
},
|
|
97
|
+
async setup(options, nuxt) {
|
|
98
|
+
const themes = options.syntaxHighlighting?.themes ?? DEFAULT_SYNTAX_THEMES;
|
|
99
|
+
|
|
100
|
+
nuxt.options.runtimeConfig.public.ginkoDocs = mergeSyntaxRuntimeConfig(
|
|
101
|
+
nuxt.options.runtimeConfig.public.ginkoDocs,
|
|
102
|
+
themes,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!options.syntaxHighlighting) return;
|
|
106
|
+
|
|
107
|
+
const content = nuxt.options.content;
|
|
108
|
+
if (!content) {
|
|
109
|
+
throw new Error("Ginko Docs could not locate the content module configuration.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
content.markdown ??= {};
|
|
113
|
+
content.markdown.plugins ??= [];
|
|
114
|
+
|
|
115
|
+
await patchShikiThemes(
|
|
116
|
+
content.markdown.plugins as MarkdownPlugin[],
|
|
117
|
+
options.syntaxHighlighting.themes,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
});
|
package/nuxt.config.ts
CHANGED
|
@@ -34,6 +34,7 @@ export default defineNuxtConfig({
|
|
|
34
34
|
alias: { "#ginko-docs": app },
|
|
35
35
|
css: [join(app, "assets/css/tailwind.css"), join(app, "assets/css/prose.css")],
|
|
36
36
|
modules: [
|
|
37
|
+
join(root, "modules/syntax-highlighting"),
|
|
37
38
|
join(root, "modules/feature-routing"),
|
|
38
39
|
"@nuxt/icon",
|
|
39
40
|
"@nuxt/fonts",
|
|
@@ -55,7 +56,7 @@ export default defineNuxtConfig({
|
|
|
55
56
|
},
|
|
56
57
|
mcp: {
|
|
57
58
|
name: "Ginko Docs",
|
|
58
|
-
version: "0.3.0
|
|
59
|
+
version: "0.3.0",
|
|
59
60
|
},
|
|
60
61
|
components: {
|
|
61
62
|
dirs: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lupinum/ginko-docs",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A Nuxt documentation layer powered by Ginko Content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"content",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"default": "./content.js"
|
|
49
49
|
},
|
|
50
50
|
"./app-config": "./shared/types/app-config.ts",
|
|
51
|
-
"./components": "./components.ts"
|
|
51
|
+
"./components": "./components.ts",
|
|
52
|
+
"./nuxt-config": "./shared/types/nuxt-config.ts"
|
|
52
53
|
},
|
|
53
54
|
"publishConfig": {
|
|
54
55
|
"access": "public"
|
|
@@ -4,6 +4,7 @@ import type { GinkoDocsNeutralPalette, GinkoDocsPrimaryPalette } from "../theme-
|
|
|
4
4
|
export type GinkoDocsLocalizedText = string | { en: string; de?: string };
|
|
5
5
|
|
|
6
6
|
export type GinkoDocsProseAppearance = "quiet" | "tint";
|
|
7
|
+
export type GinkoDocsCodeBlockTheme = "dark" | "adaptive";
|
|
7
8
|
export type GinkoDocsProseFamily =
|
|
8
9
|
| "callout"
|
|
9
10
|
| "aside"
|
|
@@ -62,6 +63,7 @@ export interface GinkoDocsAppConfig {
|
|
|
62
63
|
theme: {
|
|
63
64
|
neutral: GinkoDocsNeutralPalette;
|
|
64
65
|
primary: GinkoDocsPrimaryPalette;
|
|
66
|
+
codeBlocks: GinkoDocsCodeBlockTheme;
|
|
65
67
|
};
|
|
66
68
|
prose: GinkoDocsProseConfig;
|
|
67
69
|
site: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BundledTheme } from "shiki";
|
|
2
|
+
|
|
3
|
+
export type GinkoDocsSyntaxTheme = BundledTheme;
|
|
4
|
+
|
|
5
|
+
export interface GinkoDocsSyntaxHighlightingConfig {
|
|
6
|
+
themes: {
|
|
7
|
+
light: GinkoDocsSyntaxTheme;
|
|
8
|
+
dark: GinkoDocsSyntaxTheme;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GinkoDocsNuxtConfig {
|
|
13
|
+
syntaxHighlighting?: GinkoDocsSyntaxHighlightingConfig;
|
|
14
|
+
}
|