@lupinum/ginko-docs 0.3.0-rc.4 → 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.
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { Button } from "#ginko-docs/components/ui/button";
3
3
  import { Kbd } from "#ginko-docs/components/ui/kbd";
4
- import { Separator } from "#ginko-docs/components/ui/separator";
5
4
  import {
6
5
  Sheet,
7
6
  SheetContent,
@@ -43,9 +42,6 @@ const searchShortcut = computed(() => (metaKey.value === "⌘" ? "⌘K" : "Ctrl
43
42
  const homePath = computed(() => localizedPath("home"));
44
43
  const docsPath = computed(() => localizedPath("docs"));
45
44
 
46
- // The mobile menu has two levels: the root nav in hero-scale type, and a docs
47
- // level with the section entry links. The docs nav link drills in; every other
48
- // link navigates directly.
49
45
  const { data: docsNavData } = useDocsNavigationData();
50
46
  const docsSections = computed(() =>
51
47
  getDocsNavigationSections(
@@ -107,8 +103,6 @@ function closeMenu() {
107
103
  isMobileMenuOpen.value = false;
108
104
  }
109
105
 
110
- // Scroll lock, Escape handling, and focus trapping come from the Sheet
111
- // (reka-ui Dialog); only route-driven closing is ours.
112
106
  function openSearch() {
113
107
  isMobileMenuOpen.value = false;
114
108
  openCommandCenter();
@@ -116,7 +110,6 @@ function openSearch() {
116
110
 
117
111
  watch(isMobileMenuOpen, (open) => {
118
112
  if (open) {
119
- // Context-aware entry: docs readers land on their section links first.
120
113
  menuLevel.value = hasDocsLevel.value && isActive(docsPath.value) ? "docs" : "root";
121
114
  }
122
115
  });
@@ -133,20 +126,26 @@ watch(
133
126
  <header
134
127
  class="sticky top-0 z-50 flex h-14 w-full shrink-0 items-center border-b border-border bg-background/90 backdrop-blur-md"
135
128
  >
136
- <div class="mx-auto flex w-full max-w-screen-2xl flex-1 items-center gap-4 px-4 md:px-6">
129
+ <div
130
+ class="mx-auto flex w-full max-w-screen-2xl min-w-0 flex-1 items-center gap-3 px-4 min-[691px]:gap-4 min-[691px]:px-6"
131
+ >
137
132
  <NuxtLink
138
133
  :to="homePath"
139
- class="flex items-center gap-2 font-semibold text-foreground"
134
+ class="flex shrink-0 items-center gap-2 font-semibold text-foreground"
140
135
  :aria-label="t('nav.home')"
141
136
  >
142
137
  <SiteLogoMark />
143
138
  </NuxtLink>
144
- <nav class="ml-4 hidden items-center gap-1 md:flex" :aria-label="t('nav.main')">
139
+
140
+ <nav
141
+ class="ml-1 hidden shrink-0 items-center gap-1 min-[691px]:ml-2 min-[691px]:flex"
142
+ :aria-label="t('nav.main')"
143
+ >
145
144
  <NuxtLink
146
145
  v-for="item in mainNav"
147
146
  :key="item.href"
148
147
  :to="item.href"
149
- class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
148
+ class="rounded-lg px-3 py-1.5 text-sm font-medium transition-colors"
150
149
  :class="
151
150
  isActive(item.href)
152
151
  ? 'bg-muted text-foreground'
@@ -156,210 +155,198 @@ watch(
156
155
  {{ item.label }}
157
156
  </NuxtLink>
158
157
  </nav>
159
- <div class="ml-auto flex items-center gap-1.5">
158
+
159
+ <div class="ml-auto flex min-w-0 items-center gap-1 min-[691px]:gap-1.5">
160
160
  <Button
161
161
  variant="outline"
162
- class="hidden h-9 w-52 justify-start gap-2 px-3 text-muted-foreground md:flex"
163
- :aria-label="t('nav.search')"
162
+ class="hidden h-9 w-[22.5rem] min-w-0 max-w-[22.5rem] flex-1 justify-start gap-2 overflow-hidden rounded-lg border-border/70 bg-muted/35 px-3 whitespace-nowrap text-muted-foreground min-[691px]:flex"
163
+ :aria-label="t('nav.searchDocumentation')"
164
164
  type="button"
165
165
  @click="openSearch"
166
166
  >
167
167
  <Icon name="lucide:search" class="size-4 shrink-0" aria-hidden="true" />
168
- <span class="flex-1 text-left">{{ t("nav.search") }}...</span>
169
- <Kbd class="h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
170
- </Button>
171
- <Button
172
- variant="ghost"
173
- size="icon"
174
- class="md:hidden"
175
- :aria-label="t('nav.search')"
176
- type="button"
177
- @click="openSearch"
178
- >
179
- <Icon name="lucide:search" class="size-5" aria-hidden="true" />
168
+ <span class="min-w-0 flex-1 truncate text-left" aria-hidden="true">{{
169
+ t("nav.searchDocumentation")
170
+ }}</span>
171
+ <Kbd class="hidden h-5 shrink-0 bg-background text-[10px] min-[900px]:inline-flex">{{
172
+ searchShortcut
173
+ }}</Kbd>
180
174
  </Button>
181
175
 
182
- <Separator orientation="vertical" class="mx-0.5 hidden h-5 md:block" />
176
+ <div class="flex shrink-0 items-center gap-0.5">
177
+ <Button
178
+ variant="ghost"
179
+ size="icon"
180
+ class="size-9 rounded-lg min-[691px]:hidden"
181
+ :aria-label="t('nav.searchDocumentation')"
182
+ type="button"
183
+ @click="openSearch"
184
+ >
185
+ <Icon name="lucide:search" class="size-[18px]" aria-hidden="true" />
186
+ </Button>
183
187
 
184
- <ClientOnly>
185
- <div class="hidden md:block">
186
- <ModeToggle />
187
- </div>
188
+ <ClientOnly>
189
+ <ModeToggle class="hidden min-[691px]:inline-flex" />
188
190
 
189
- <template #fallback>
190
- <span
191
- class="hidden size-9 shrink-0 rounded-md border border-border bg-background md:block"
192
- aria-hidden="true"
193
- />
194
- </template>
195
- </ClientOnly>
191
+ <template #fallback>
192
+ <span
193
+ class="hidden size-9 shrink-0 rounded-lg min-[691px]:inline-flex"
194
+ aria-hidden="true"
195
+ />
196
+ </template>
197
+ </ClientOnly>
196
198
 
197
- <div v-if="config.nav.socialIcons" class="hidden md:block">
198
- <SiteSocialLinks />
199
- </div>
199
+ <SiteLocaleSwitcher class="hidden min-[691px]:flex" />
200
200
 
201
- <SiteLocaleSwitcher class="hidden md:flex" />
202
- <Sheet v-model:open="isMobileMenuOpen">
203
- <SheetTrigger as-child>
204
- <Button
205
- variant="ghost"
206
- size="icon"
207
- class="md:hidden"
208
- :aria-label="t('nav.openMenu')"
209
- type="button"
210
- >
211
- <Icon name="lucide:menu" class="size-5" aria-hidden="true" />
212
- </Button>
213
- </SheetTrigger>
214
- <SheetContent
215
- side="top"
216
- class="inset-0 h-dvh gap-0 overflow-hidden border-b-0 bg-background/95 p-0 backdrop-blur-md md:hidden"
217
- >
218
- <SheetTitle class="sr-only">{{ t("nav.mobile") }}</SheetTitle>
219
- <SheetDescription class="sr-only">{{ t("nav.main") }}</SheetDescription>
220
- <div class="flex h-full min-h-0 flex-col pt-16">
221
- <div
222
- class="flex min-h-0 w-[200%] flex-1 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none"
223
- :class="menuLevel === 'docs' ? '-translate-x-1/2' : ''"
201
+ <SiteSocialLinks v-if="config.nav.socialIcons" />
202
+
203
+ <Sheet v-model:open="isMobileMenuOpen">
204
+ <SheetTrigger as-child>
205
+ <Button
206
+ variant="ghost"
207
+ size="icon"
208
+ class="size-9 rounded-lg min-[691px]:hidden"
209
+ :aria-label="t('nav.openMenu')"
210
+ type="button"
224
211
  >
225
- <nav
226
- class="flex min-h-0 w-1/2 flex-col overflow-x-hidden overflow-y-auto px-6"
227
- :aria-label="t('nav.mobile')"
228
- :inert="menuLevel !== 'root'"
212
+ <Icon name="lucide:menu" class="size-[18px]" aria-hidden="true" />
213
+ </Button>
214
+ </SheetTrigger>
215
+ <SheetContent
216
+ side="top"
217
+ class="inset-0 h-dvh gap-0 overflow-hidden border-b-0 bg-background/95 p-0 backdrop-blur-md min-[691px]:hidden"
218
+ >
219
+ <SheetTitle class="sr-only">{{ t("nav.mobile") }}</SheetTitle>
220
+ <SheetDescription class="sr-only">{{ t("nav.main") }}</SheetDescription>
221
+ <div class="flex h-full min-h-0 flex-col pt-16">
222
+ <div
223
+ class="flex min-h-0 w-[200%] flex-1 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none"
224
+ :class="menuLevel === 'docs' ? '-translate-x-1/2' : ''"
229
225
  >
230
- <div class="m-auto w-full space-y-8 py-6">
231
- <template v-for="item in rootNavItems" :key="item.href">
232
- <button
233
- v-if="item.drill"
234
- :ref="setDrillButtonRef"
235
- type="button"
236
- class="group block w-full text-start"
237
- @click="drillIntoDocs"
238
- >
239
- <span
240
- class="flex items-center gap-2.5 text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
241
- :class="
242
- isActive(item.href)
243
- ? 'text-foreground'
244
- : 'text-foreground/70 transition-colors group-hover:text-foreground'
245
- "
226
+ <nav
227
+ class="flex min-h-0 w-1/2 flex-col overflow-x-hidden overflow-y-auto px-6"
228
+ :aria-label="t('nav.mobile')"
229
+ :inert="menuLevel !== 'root'"
230
+ >
231
+ <div class="m-auto w-full space-y-8 py-6">
232
+ <template v-for="item in rootNavItems" :key="item.href">
233
+ <button
234
+ v-if="item.drill"
235
+ :ref="setDrillButtonRef"
236
+ type="button"
237
+ class="group block w-full text-start"
238
+ @click="drillIntoDocs"
246
239
  >
247
- {{ item.label }}
248
- <Icon
249
- name="lucide:chevron-right"
250
- class="size-6 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
251
- aria-hidden="true"
252
- />
253
- </span>
254
- <span
255
- v-if="item.description"
256
- class="mt-1.5 block text-sm text-muted-foreground"
240
+ <span
241
+ class="flex items-center gap-2.5 text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
242
+ :class="
243
+ isActive(item.href)
244
+ ? 'text-foreground'
245
+ : 'text-foreground/70 transition-colors group-hover:text-foreground'
246
+ "
247
+ >
248
+ {{ item.label }}
249
+ <Icon
250
+ name="lucide:chevron-right"
251
+ class="size-6 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
252
+ aria-hidden="true"
253
+ />
254
+ </span>
255
+ <span
256
+ v-if="item.description"
257
+ class="mt-1.5 block text-sm text-muted-foreground"
258
+ >
259
+ {{ item.description }}
260
+ </span>
261
+ </button>
262
+ <NuxtLink v-else :to="item.href" class="group block" @click="closeMenu">
263
+ <span
264
+ class="block text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
265
+ :class="
266
+ isActive(item.href)
267
+ ? 'text-foreground'
268
+ : 'text-foreground/70 transition-colors group-hover:text-foreground'
269
+ "
270
+ >
271
+ {{ item.label }}
272
+ </span>
273
+ <span
274
+ v-if="item.description"
275
+ class="mt-1.5 block text-sm text-muted-foreground"
276
+ >
277
+ {{ item.description }}
278
+ </span>
279
+ </NuxtLink>
280
+ </template>
281
+ </div>
282
+ </nav>
283
+
284
+ <nav
285
+ v-if="hasDocsLevel"
286
+ class="flex min-h-0 w-1/2 flex-col px-6"
287
+ :aria-label="docsLevelTitle"
288
+ :inert="menuLevel !== 'docs'"
289
+ >
290
+ <button
291
+ ref="docsBackButton"
292
+ type="button"
293
+ class="flex items-center gap-1 self-start py-2 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
294
+ @click="backToRoot"
295
+ >
296
+ <Icon name="lucide:chevron-left" class="size-4" aria-hidden="true" />
297
+ {{ t("nav.menu") }}
298
+ </button>
299
+ <p
300
+ class="mt-1 text-[2rem] leading-[1.1] font-semibold tracking-[-0.03em] text-foreground"
301
+ >
302
+ {{ docsLevelTitle }}
303
+ </p>
304
+ <div class="-mx-2 mt-3 min-h-0 flex-1 overflow-x-hidden overflow-y-auto pb-4">
305
+ <template v-for="section in docsMenuSections" :key="section.id">
306
+ <p
307
+ v-if="section.title"
308
+ class="mt-6 mb-1 px-2 text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase first:mt-2"
257
309
  >
258
- {{ item.description }}
259
- </span>
260
- </button>
261
- <NuxtLink v-else :to="item.href" class="group block" @click="closeMenu">
262
- <span
263
- class="block text-[2.5rem] leading-[1.05] font-semibold tracking-[-0.03em] text-balance"
310
+ {{ section.title }}
311
+ </p>
312
+ <NuxtLink
313
+ v-for="link in section.links"
314
+ :key="link.id"
315
+ :to="link.href"
316
+ class="flex items-center rounded-md px-2 py-2 text-lg font-semibold transition-colors"
264
317
  :class="
265
- isActive(item.href)
266
- ? 'text-foreground'
267
- : 'text-foreground/70 transition-colors group-hover:text-foreground'
318
+ link.active
319
+ ? 'bg-primary/10 text-primary'
320
+ : 'text-foreground hover:bg-muted/60'
268
321
  "
322
+ :aria-current="link.active ? 'true' : undefined"
323
+ @click="closeMenu"
269
324
  >
270
- {{ item.label }}
271
- </span>
272
- <span
273
- v-if="item.description"
274
- class="mt-1.5 block text-sm text-muted-foreground"
275
- >
276
- {{ item.description }}
277
- </span>
278
- </NuxtLink>
279
- </template>
280
- </div>
281
- </nav>
282
-
283
- <nav
284
- v-if="hasDocsLevel"
285
- class="flex min-h-0 w-1/2 flex-col px-6"
286
- :aria-label="docsLevelTitle"
287
- :inert="menuLevel !== 'docs'"
288
- >
289
- <button
290
- ref="docsBackButton"
291
- type="button"
292
- class="flex items-center gap-1 self-start py-2 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
293
- @click="backToRoot"
294
- >
295
- <Icon name="lucide:chevron-left" class="size-4" aria-hidden="true" />
296
- {{ t("nav.menu") }}
297
- </button>
298
- <p
299
- class="mt-1 text-[2rem] leading-[1.1] font-semibold tracking-[-0.03em] text-foreground"
300
- >
301
- {{ docsLevelTitle }}
302
- </p>
303
- <div class="-mx-2 mt-3 min-h-0 flex-1 overflow-x-hidden overflow-y-auto pb-4">
304
- <template v-for="section in docsMenuSections" :key="section.id">
305
- <p
306
- v-if="section.title"
307
- class="mt-6 mb-1 px-2 text-xs font-semibold tracking-[0.08em] text-muted-foreground uppercase first:mt-2"
308
- >
309
- {{ section.title }}
310
- </p>
311
- <NuxtLink
312
- v-for="link in section.links"
313
- :key="link.id"
314
- :to="link.href"
315
- class="flex items-center rounded-md px-2 py-2 text-lg font-semibold transition-colors"
316
- :class="
317
- link.active
318
- ? 'bg-primary/10 text-primary'
319
- : 'text-foreground hover:bg-muted/60'
320
- "
321
- :aria-current="link.active ? 'true' : undefined"
322
- @click="closeMenu"
323
- >
324
- {{ link.label }}
325
- <span
326
- v-if="link.active"
327
- class="mr-1 ml-auto size-1.5 rounded-full bg-primary"
328
- aria-hidden="true"
329
- />
330
- </NuxtLink>
331
- </template>
332
- </div>
333
- </nav>
334
- </div>
325
+ {{ link.label }}
326
+ <span
327
+ v-if="link.active"
328
+ class="mr-1 ml-auto size-1.5 rounded-full bg-primary"
329
+ aria-hidden="true"
330
+ />
331
+ </NuxtLink>
332
+ </template>
333
+ </div>
334
+ </nav>
335
+ </div>
335
336
 
336
- <div class="shrink-0 space-y-3.5 px-6 pt-4 pb-8">
337
- <button
338
- type="button"
339
- class="flex h-11 w-full items-center gap-2.5 rounded-md border border-border bg-muted/40 px-4 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted"
340
- @click="openSearch"
341
- >
342
- <Icon name="lucide:search" class="size-4" aria-hidden="true" />
343
- {{ t("nav.search") }}...
344
- <Kbd class="ml-auto h-5 bg-background text-[10px]">{{ searchShortcut }}</Kbd>
345
- </button>
346
- <div class="flex flex-wrap items-center gap-2">
337
+ <div class="shrink-0 space-y-3 border-t border-border px-6 pt-4 pb-8">
347
338
  <ClientOnly>
348
- <ModeToggle variant="pill" />
339
+ <ModeToggle variant="menu-row" />
340
+ <SiteLocaleSwitcher variant="menu-row" @navigate="closeMenu" />
341
+
349
342
  <template #fallback>
350
- <span
351
- class="h-9 w-24 rounded-full border border-border bg-background"
352
- aria-hidden="true"
353
- />
343
+ <span class="block h-28" aria-hidden="true" />
354
344
  </template>
355
345
  </ClientOnly>
356
- <SiteLocaleSwitcher variant="pill" class="flex" @navigate="closeMenu" />
357
- <SiteSocialLinks
358
- v-if="config.nav.socialIcons"
359
- class="ml-auto"
360
- @navigate="closeMenu"
361
- />
362
- <span v-else-if="socialLinks.length" class="ml-auto flex items-center gap-2">
346
+ <div
347
+ v-if="!config.nav.socialIcons && socialLinks.length"
348
+ class="flex flex-wrap items-center gap-2"
349
+ >
363
350
  <NuxtLink
364
351
  v-for="link in socialLinks"
365
352
  :key="link.href"
@@ -372,12 +359,12 @@ watch(
372
359
  <Icon v-if="link.icon" :name="link.icon" class="size-4" aria-hidden="true" />
373
360
  {{ link.label }}
374
361
  </NuxtLink>
375
- </span>
362
+ </div>
376
363
  </div>
377
364
  </div>
378
- </div>
379
- </SheetContent>
380
- </Sheet>
365
+ </SheetContent>
366
+ </Sheet>
367
+ </div>
381
368
  </div>
382
369
  </div>
383
370
  </header>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, useId } from "vue";
3
- import { useAsyncData, useI18n } from "#imports";
3
+ import { useAsyncData, useI18n, useRuntimeConfig } from "#imports";
4
4
  import { cn } from "../../utils";
5
5
  import { filenameExtension, resolveFileIcon } from "../../utils/file-icons";
6
6
 
@@ -15,6 +15,14 @@ export type SiteHeroCodeTab = {
15
15
  const props = defineProps<{ tabs: SiteHeroCodeTab[] }>();
16
16
 
17
17
  const { t } = useI18n();
18
+ const runtimeConfig = useRuntimeConfig();
19
+ const syntaxThemes = computed(() => {
20
+ const themes = runtimeConfig.public.ginkoDocs?.syntaxHighlighting?.themes;
21
+ return {
22
+ light: themes?.light ?? "light-plus",
23
+ dark: themes?.dark ?? "dark-plus",
24
+ };
25
+ });
18
26
  const regionId = useId();
19
27
  const activeIndex = ref(0);
20
28
 
@@ -38,9 +46,10 @@ function plainLines(code: string): string {
38
46
  }
39
47
 
40
48
  // Config code arrives as raw strings, so it skips the build-time content
41
- // pipeline; highlight here with the same Shiki themes the docs use.
49
+ // pipeline; highlight here with the configured Shiki theme pair.
42
50
  const { data: highlighted } = await useAsyncData(`site-hero-code-${regionId}`, async () => {
43
51
  const { codeToHast, hastToHtml } = await import("shiki");
52
+ const themes = syntaxThemes.value;
44
53
 
45
54
  return Promise.all(
46
55
  props.tabs.map(async (tab) => {
@@ -49,7 +58,7 @@ const { data: highlighted } = await useAsyncData(`site-hero-code-${regionId}`, a
49
58
  try {
50
59
  const root = await codeToHast(tab.code, {
51
60
  lang,
52
- themes: { light: "light-plus", dark: "dark-plus" },
61
+ themes,
53
62
  });
54
63
  const pre = root.children.find((node) => node.type === "element");
55
64
  const code = pre?.children.find((node) => node.type === "element");
@@ -1,12 +1,19 @@
1
1
  <script setup lang="ts">
2
- import { defineAsyncComponent, onBeforeUnmount, onMounted } from "vue";
2
+ import { defineAsyncComponent, onBeforeUnmount, onMounted, watch } from "vue";
3
+ import { useRoute } from "#imports";
3
4
  import SiteSearchPageHighlight from "#ginko-docs/features/search/components/SiteSearchPageHighlight.vue";
4
5
  import { useCommandCenterState } from "#ginko-docs/features/search/useCommandCenter";
5
6
 
6
7
  const SiteCommandCenter = defineAsyncComponent(
7
8
  () => import("#ginko-docs/features/search/components/SiteCommandCenter.vue"),
8
9
  );
9
- const { open, openCommandCenter } = useCommandCenterState();
10
+ const route = useRoute();
11
+ const { open, openCommandCenter, closeCommandCenter } = useCommandCenterState();
12
+
13
+ watch(
14
+ () => route.fullPath,
15
+ () => closeCommandCenter(),
16
+ );
10
17
 
11
18
  function handleGlobalShortcut(event: KeyboardEvent) {
12
19
  const element = event.target as HTMLElement | null;