Added sheet for sidebar mobile

This commit is contained in:
Alexis
2025-08-15 15:37:35 +02:00
parent f3d5db76ac
commit 6251499940
23 changed files with 424 additions and 195 deletions

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { PhInfo } from '@phosphor-icons/vue';
</script>
<template>
<div class="flex justify-between items-center gap-2">
<div class="grow">
<UserCTA />
</div>
<UiButton size="icon" class="size-8 hover:bg-secondary hover:text-secondary-foreground" variant="ghost" as-child>
<NuxtLink to="/about">
<PhInfo size="18" />
</NuxtLink>
</UiButton>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { PanelsLeftBottom } from 'lucide-vue-next';
</script>
<template>
<menu class="min-w-32 pt-16 flex flex-col gap-0.5">
<li class="w-full">
<UiButton
variant="ghost"
size="sm"
class="h-8 gap-1.5 hover:bg-secondary w-full justify-start"
as-child
>
<NuxtLink to="/my" active-class="active-link" exact-active-class="exact-link">
<PanelsLeftBottom :size="18" />
<span class="text-[.9em]">
{{ $t('ui.sidebarMenu.projects') }}
</span>
</NuxtLink>
</UiButton>
</li>
</menu>
</template>
<style lang="scss" scoped>
.active-link {
color: var(--color-secondary-foreground);
background-color: var(--color-secondary);
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
}
}
.exact-link {
color: var(--color-primary-foreground);
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
cursor: initial;
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
}
}
.dark .exact-link {
background-color: var(--color-primary);
}
</style>

View File

@@ -1,136 +1,32 @@
<script lang="ts" setup>
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue"
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
import { PanelsLeftBottom } from "lucide-vue-next"
import { cn } from "@/lib/utils";
import { breakpointsTailwind } from "@vueuse/core"
import { PhInfo } from "@phosphor-icons/vue";
const { revealAdvancedSearch } = useCalendar()
const { toggleSidebar } = useUiStore()
const { currentMenu, isSidebarOpened } = storeToRefs(useUiStore())
function handleMenuItemAction(actionType: SidebarMenuActionType) {
if (actionType === "event-search") {
revealAdvancedSearch()
}
}
function computeMenuItemIcon(iconString: SidebarMenuIcon) {
switch (iconString) {
case "universe":
return PhHurricane
case "world":
return PhGlobeHemisphereEast
default:
return PhCompass
}
}
const breakpoints = useBreakpoints(
breakpointsTailwind
)
// const sidebarRef = ref(null)
// onClickOutside(sidebarRef, () => {
// isSidebarOpened.value = false
// })
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<nav
ref="sidebarRef"
:class="cn(
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center md:transition-colors'], // Base appearance
['max-md:hidden w-fit relative isolate p-2 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center transition-colors'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // After styling
['border-r-[1px] border-r-border dark:border-r-border'], // Colours
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-40 max-md:max-w-full max-md:transition-all'], // Responsive behaviours
{
'max-md:-translate-x-40': !isSidebarOpened,
'max-md:-translate-x-0': isSidebarOpened
}
)"
>
<menu class="flex flex-col gap-4 max-md:items-center">
<li class="mb-12 mt-4 max-md:self-start">
<UiButton
variant="outline"
size="icon"
class="md:hidden size-9 border-background/30"
@click="toggleSidebar"
>
<PhX size="19" />
</UiButton>
</li>
<SidebarMenu />
<li class="max-md:self-start">
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-full max-md:hidden"
as-child
>
<NuxtLink to="/explore">
<PhCompass size="24" weight="fill" />
</NuxtLink>
</UiButton>
<NuxtLink
to="/explore"
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
>
<PhCompass size="22" weight="fill" />
<span class="text-[.9em]">
{{ $t('pages.explore.menuLabel') }}
</span>
</NuxtLink>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('pages.explore.menuLabel') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<ClientOnly>
<li v-for="(item, i) in currentMenu" :key="i">
<UiTooltipProvider :delay-duration="50">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
v-if="item.to"
variant="ghost"
size="icon"
class="rounded-full"
as-child
>
<NuxtLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</NuxtLink>
</UiButton>
<UiButton
v-if="item.action"
variant="ghost"
size="icon"
class="rounded-full"
@click="handleMenuItemAction(item.action!)"
>
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>{{ item.tooltip }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
</ClientOnly>
</menu>
<UserCTA />
<SidebarFooter />
</nav>
<UiSheet v-model:open="isSidebarOpened">
<UiSheetContent side="left" class="p-2 grid gap-4 grid-rows-[auto_1fr]">
<UiSheetHeader />
<SidebarMenu />
<SidebarFooter />
</UiSheetContent>
</UiSheet>
</template>

View File

@@ -8,8 +8,9 @@ const { toggleSidebar } = useUiStore()
<UiButton
size="icon"
variant="outline"
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
@click="toggleSidebar"
>
<PhList size="19" weight="light" />
<PhList size="14" weight="light" />
</UiButton>
</template>

View File

@@ -44,46 +44,43 @@ async function handleLogout() {
console.log(error.message)
}
}
type AvailableRoutes = "/my" | "/my/settings" | "/about"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script>
<template>
<ClientOnly>
<UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger class="aspect-square">
<TransitionGroup name="fade-group" appear>
<UiAvatar
v-if="user"
id="user-avatar"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<UiButton
v-else
variant="outline"
size="icon"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent border-none dark:bg-background rounded-full bg-primary transition-all cursor-pointer"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<PhUserCircle size="24" />
</UiButton>
</TransitionGroup>
<UiDropdownMenuTrigger as-child>
<UiButton
variant="ghost"
class="w-full justify-start p-2 h-fit hover:text-secondary-foreground hover:bg-secondary"
:class="cn({ 'bg-secondary': menuOpened })"
>
<TransitionGroup name="fade-group" appear>
<div
v-if="user"
class="flex gap-2 items-center"
>
<UiAvatar
id="user-avatar"
size="sm"
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<div class="text-xs">
{{ userMeta?.name }}
</div>
</div>
</TransitionGroup>
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
@@ -91,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }}
</p>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<PhUser size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.profile') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
</template>
<template v-else>
@@ -194,13 +182,6 @@ function pushRoute(to: AvailableRoutes) {
</UiDropdownMenuPortal>
</UiDropdownMenuSub>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/about')">
<PhInfo size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.about') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
<template v-if="user">

View File

@@ -9,9 +9,9 @@ export const avatarVariant = cva(
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
sm: "size-6 text-xs",
base: "size-14 text-2xl",
lg: "size-24 text-5xl",
},
shape: {
circle: "rounded-full",

View File

@@ -0,0 +1,18 @@
<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
data-slot="sheet"
v-bind="forwarded"
>
<slot />
</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,53 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
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 border-l 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-full border-r 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="{ ...forwarded, ...$attrs }"
>
<slot />
</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,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import {
DialogClose
} from "reka-ui"
import { PhX } from "@phosphor-icons/vue";
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 />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 disabled:pointer-events-none"
as-child
>
<UiButton
size="icon"
variant="outline"
class="size-7 border-border bg-background hover:bg-secondary hover:text-secondary-foreground"
>
<PhX size="14" />
<span class="sr-only">{{ $t('ui.actions.close') }}</span>
</UiButton>
</DialogClose>
</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/70', 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"