mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
chore: mobile friendly navbar
This commit is contained in:
19
app/components/ui/sheet/Sheet.vue
Normal file
19
app/components/ui/sheet/Sheet.vue
Normal 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>
|
||||
15
app/components/ui/sheet/SheetClose.vue
Normal file
15
app/components/ui/sheet/SheetClose.vue
Normal 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>
|
||||
62
app/components/ui/sheet/SheetContent.vue
Normal file
62
app/components/ui/sheet/SheetContent.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetDescription.vue
Normal file
21
app/components/ui/sheet/SheetDescription.vue
Normal 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>
|
||||
16
app/components/ui/sheet/SheetFooter.vue
Normal file
16
app/components/ui/sheet/SheetFooter.vue
Normal 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>
|
||||
15
app/components/ui/sheet/SheetHeader.vue
Normal file
15
app/components/ui/sheet/SheetHeader.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
21
app/components/ui/sheet/SheetOverlay.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetTitle.vue
Normal file
21
app/components/ui/sheet/SheetTitle.vue
Normal 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>
|
||||
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
15
app/components/ui/sheet/SheetTrigger.vue
Normal 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>
|
||||
8
app/components/ui/sheet/index.ts
Normal file
8
app/components/ui/sheet/index.ts
Normal 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"
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user