chore: mobile friendly navbar

This commit is contained in:
2026-01-14 22:24:34 +01:00
parent 8a538347d3
commit c2f843ce4c
13 changed files with 318 additions and 38 deletions

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"

View File

@@ -4,56 +4,64 @@ import ThemeDropdown from "~/components/ui/ThemeDropdown.vue";
import LanguageSwitcher from "~/components/app/LanguageSwitcher.vue";
import "vue-sonner/style.css";
import { Toaster } from "@/components/ui/sonner";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Menu } from "lucide-vue-next";
const { t } = useI18n();
const mobileMenuOpen = ref(false);
const navLinks = [
{ to: "/", label: "home" },
{ to: "/stream", label: "stream" },
{ to: "/about", label: "about" },
{ to: "/presets", label: "presets", requiresAuth: true },
];
</script>
<template>
<div>
<header class="flex justify-between items-center p-4">
<div class="flex items-center space-x-6">
<header class="flex justify-between items-center p-4 border-b border-border">
<div class="flex items-center space-x-4 md:space-x-6">
<NuxtLink
to="/"
class="text-xl font-semibold hover:opacity-80 transition-opacity"
>
helium
</NuxtLink>
<nav class="flex space-x-4">
<NuxtLink
to="/"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t("home") }}
</NuxtLink>
<NuxtLink
to="/stream"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t("stream") }}
</NuxtLink>
<ClientOnly>
<SignedIn>
<NuxtLink
to="/presets"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t("presets") }}
</NuxtLink>
</SignedIn>
</ClientOnly>
<NuxtLink
to="/about"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t("about") }}
</NuxtLink>
<!-- Desktop Navigation -->
<nav class="hidden md:flex space-x-4">
<template v-for="link in navLinks" :key="link.to">
<ClientOnly v-if="link.requiresAuth">
<SignedIn>
<NuxtLink
:to="link.to"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t(link.label) }}
</NuxtLink>
</SignedIn>
</ClientOnly>
<NuxtLink
v-else
:to="link.to"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
{{ t(link.label) }}
</NuxtLink>
</template>
</nav>
</div>
<div class="flex items-center space-x-4">
<!-- Desktop Right Side -->
<div class="hidden md:flex items-center space-x-4">
<LanguageSwitcher />
<ThemeDropdown />
<ClientOnly>
@@ -65,7 +73,64 @@ const { t } = useI18n();
</SignedIn>
</ClientOnly>
</div>
<!-- Mobile Menu -->
<div class="md:hidden">
<Sheet v-model:open="mobileMenuOpen">
<SheetTrigger as-child>
<button
class="p-2 hover:bg-muted rounded-md transition-colors"
aria-label="Toggle menu"
>
<Menu class="w-6 h-6" />
</button>
</SheetTrigger>
<SheetContent side="right" class="w-[300px] sm:w-[400px]">
<SheetHeader>
<SheetTitle>{{ t("menu") || "Menu" }}</SheetTitle>
</SheetHeader>
<nav class="flex flex-col space-y-4 mt-6">
<template v-for="link in navLinks" :key="link.to">
<ClientOnly v-if="link.requiresAuth">
<SignedIn>
<NuxtLink
:to="link.to"
class="text-sm font-medium hover:text-primary transition-colors py-2"
active-class="text-primary"
@click="mobileMenuOpen = false"
>
{{ t(link.label) }}
</NuxtLink>
</SignedIn>
</ClientOnly>
<NuxtLink
v-else
:to="link.to"
class="text-sm font-medium hover:text-primary transition-colors py-2"
active-class="text-primary"
@click="mobileMenuOpen = false"
>
{{ t(link.label) }}
</NuxtLink>
</template>
<div class="flex items-center space-x-4 pt-4 border-t border-border">
<LanguageSwitcher />
<ThemeDropdown />
<ClientOnly>
<SignedOut>
<SignInDialog />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</ClientOnly>
</div>
</nav>
</SheetContent>
</Sheet>
</div>
</header>
<slot />
<Toaster />
</div>

View File

@@ -41,5 +41,6 @@
"sharePresetConfirm": "Do you want to share this preset?",
"failedToGenerateShareableLink": "Failed to generate shareable link",
"linkCopiedToClipboard": "Link copied to clipboard",
"failedToSharePreset": "Failed to share preset"
"failedToSharePreset": "Failed to share preset",
"menu": "Menu"
}

View File

@@ -41,5 +41,6 @@
"sharePresetConfirm": "¿Desea compartir este ajuste?",
"failedToGenerateShareableLink": "Error al generar el enlace para compartir",
"linkCopiedToClipboard": "Enlace copiado al portapapeles",
"failedToSharePreset": "Error al compartir el ajuste"
"failedToSharePreset": "Error al compartir el ajuste",
"menu": "Menú"
}