fix: rewrite AppHeader — replace UDrawer with USlideover, clean design

UDrawer (vaul-vue bottom-sheet) rendered content in DOM even when closed,
causing mobile nav to show on desktop. Replaced with USlideover (proper
sidebar panel). Also: backdrop-blur header, UButton for actions, Lucide
icons, brand color active states.
This commit is contained in:
2026-04-08 18:55:58 +02:00
parent bd7e02f6ce
commit 578a0afa1a
+77 -63
View File
@@ -3,7 +3,7 @@ const { t, locale, setLocale } = useI18n()
const localePath = useLocalePath() const localePath = useLocalePath()
const colorMode = useColorMode() const colorMode = useColorMode()
const route = useRoute() const route = useRoute()
const drawerOpen = ref(false) const mobileOpen = ref(false)
const navLinks = computed(() => [ const navLinks = computed(() => [
{ key: 'home', path: '/' }, { key: 'home', path: '/' },
@@ -27,121 +27,135 @@ function isActive(path: string): boolean {
</script> </script>
<template> <template>
<header class="sticky top-0 z-[1020] bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800"> <header class="sticky top-0 z-50 backdrop-blur-xl bg-white/80 dark:bg-gray-950/80 border-b border-gray-200/50 dark:border-gray-800/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo --> <!-- Logo -->
<NuxtLink :to="localePath('/')" :aria-label="t('a11y.logoLabel')" class="flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded"> <NuxtLink
<NuxtImg src="/images/logo.webp" alt="Killian Dalcin" width="40" height="40" loading="eager" class="rounded" /> :to="localePath('/')"
<span class="text-lg font-semibold text-gray-900 dark:text-white">Killian</span> :aria-label="t('a11y.logoLabel')"
class="flex items-center gap-2.5 shrink-0"
>
<NuxtImg
src="/images/logo.webp"
alt="Killian Dalcin"
width="36"
height="36"
loading="eager"
class="rounded-lg"
/>
<span class="text-base font-semibold tracking-tight text-gray-900 dark:text-white">Killian</span>
</NuxtLink> </NuxtLink>
<!-- Desktop nav --> <!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-6" aria-label="Main navigation"> <nav class="hidden md:flex items-center gap-1" aria-label="Main navigation">
<NuxtLink <NuxtLink
v-for="link in navLinks" v-for="link in navLinks"
:key="link.key" :key="link.key"
:to="localePath(link.path)" :to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined" :aria-current="isActive(link.path) ? 'page' : undefined"
class="text-base transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded px-1 py-2" class="px-3 py-2 text-sm font-medium rounded-lg transition-colors"
:class="[ :class="[
isActive(link.path) isActive(link.path)
? 'border-b-2 border-primary-500 text-gray-900 dark:text-white' ? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40'
: 'text-gray-700 dark:text-gray-300 hover:text-primary-500' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60'
]" ]"
> >
{{ t(`nav.${link.key}`) }} {{ t(`nav.${link.key}`) }}
</NuxtLink> </NuxtLink>
</nav> </nav>
<!-- Toggles --> <!-- Right actions -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Language toggle --> <!-- Language toggle -->
<button <UButton
type="button" variant="ghost"
color="neutral"
size="sm"
:aria-label="t('a11y.langToggle')" :aria-label="t('a11y.langToggle')"
class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 font-medium hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors"
@click="toggleLocale" @click="toggleLocale"
> >
{{ locale === 'fr' ? 'EN' : 'FR' }} {{ locale === 'fr' ? 'EN' : 'FR' }}
</button> </UButton>
<!-- Theme toggle --> <!-- Theme toggle -->
<button <UButton
type="button" variant="ghost"
color="neutral"
size="sm"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')" :aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"
class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-600 dark:text-gray-400 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors duration-300"
@click="toggleTheme" @click="toggleTheme"
> />
<UIcon :name="colorMode.value === 'dark' ? 'heroicons:sun' : 'heroicons:moon'" class="w-5 h-5" />
</button>
<!-- Hamburger (mobile) --> <!-- Mobile hamburger -->
<button <UButton
type="button" variant="ghost"
color="neutral"
size="sm"
icon="i-lucide-menu"
class="md:hidden"
:aria-label="t('a11y.openMenu')" :aria-label="t('a11y.openMenu')"
class="md:hidden min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors" @click="mobileOpen = true"
@click="drawerOpen = true" />
> </div>
<UIcon name="heroicons:bars-3" class="w-6 h-6" />
</button>
</div> </div>
</div> </div>
<!-- Mobile Drawer --> <!-- Mobile slideover -->
<UDrawer v-model:open="drawerOpen" side="left"> <USlideover v-model:open="mobileOpen" side="left" class="md:hidden">
<div class="p-6 flex flex-col h-full"> <template #header>
<!-- Close button --> <div class="flex items-center gap-2.5">
<div class="flex justify-end mb-4"> <NuxtImg
<button src="/images/logo.webp"
type="button" alt="Killian Dalcin"
:aria-label="t('a11y.closeDrawer')" width="32"
class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors" height="32"
@click="drawerOpen = false" class="rounded-lg"
> />
<UIcon name="heroicons:x-mark" class="w-6 h-6" /> <span class="text-base font-semibold text-gray-900 dark:text-white">Killian</span>
</button>
</div> </div>
</template>
<!-- Nav links --> <template #body>
<nav class="flex flex-col gap-2 flex-1" aria-label="Mobile navigation"> <nav class="flex flex-col gap-1" aria-label="Mobile navigation">
<NuxtLink <NuxtLink
v-for="link in navLinks" v-for="link in navLinks"
:key="link.key" :key="link.key"
:to="localePath(link.path)" :to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined" :aria-current="isActive(link.path) ? 'page' : undefined"
class="min-h-11 flex items-center px-4 py-3 text-base rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2" class="px-4 py-3 text-base font-medium rounded-lg transition-colors"
:class="[ :class="[
isActive(link.path) isActive(link.path)
? 'text-primary-500 font-medium bg-primary-50 dark:bg-primary-900/20' ? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40'
: 'text-gray-700 dark:text-gray-300 hover:text-primary-500 hover:bg-gray-50 dark:hover:bg-gray-800' : 'text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60'
]" ]"
@click="drawerOpen = false" @click="mobileOpen = false"
> >
{{ t(`nav.${link.key}`) }} {{ t(`nav.${link.key}`) }}
</NuxtLink> </NuxtLink>
</nav> </nav>
</template>
<!-- Toggles at bottom --> <template #footer>
<div class="flex items-center gap-2 pt-4 border-t border-gray-200 dark:border-gray-700"> <div class="flex items-center gap-2">
<button <UButton
type="button" variant="ghost"
color="neutral"
:aria-label="t('a11y.langToggle')" :aria-label="t('a11y.langToggle')"
class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-700 dark:text-gray-300 font-medium hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors"
@click="toggleLocale" @click="toggleLocale"
> >
{{ locale === 'fr' ? 'EN' : 'FR' }} {{ locale === 'fr' ? 'EN' : 'FR' }}
</button> </UButton>
<UButton
<button variant="ghost"
type="button" color="neutral"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')" :aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"
class="min-w-11 min-h-11 inline-flex items-center justify-center text-gray-600 dark:text-gray-400 hover:text-primary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded transition-colors duration-300"
@click="toggleTheme" @click="toggleTheme"
> />
<UIcon :name="colorMode.value === 'dark' ? 'heroicons:sun' : 'heroicons:moon'" class="w-5 h-5" />
</button>
</div> </div>
</div> </template>
</UDrawer> </USlideover>
</header> </header>
</template> </template>