feat(SidebarMenu): create a shared sidebar menu that slides to the screen
This commit is contained in:
99
src/shared/ui/SidebarMenu/SidebarMenu.svelte
Normal file
99
src/shared/ui/SidebarMenu/SidebarMenu.svelte
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
Component: SidebarMenu
|
||||||
|
Slides out from the right, closes on click outside
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import {
|
||||||
|
fade,
|
||||||
|
slide,
|
||||||
|
} from 'svelte/transition';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Children to render conditionally
|
||||||
|
*/
|
||||||
|
children?: Snippet;
|
||||||
|
/**
|
||||||
|
* Action (always visible) to render
|
||||||
|
*/
|
||||||
|
action?: Snippet;
|
||||||
|
/**
|
||||||
|
* Wrapper reference to bind
|
||||||
|
*/
|
||||||
|
wrapper?: HTMLElement | null;
|
||||||
|
/**
|
||||||
|
* Class to add to the wrapper
|
||||||
|
*/
|
||||||
|
class?: string;
|
||||||
|
/**
|
||||||
|
* Bindable visibility flag
|
||||||
|
*/
|
||||||
|
visible?: boolean;
|
||||||
|
/**
|
||||||
|
* Handler for click outside
|
||||||
|
*/
|
||||||
|
onClickOutside?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
children,
|
||||||
|
action,
|
||||||
|
wrapper = $bindable<HTMLElement | null>(null),
|
||||||
|
class: className,
|
||||||
|
visible = $bindable(false),
|
||||||
|
onClickOutside,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes menu on click outside
|
||||||
|
*/
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
if (!wrapper || !visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wrapper.contains(event.target as Node)) {
|
||||||
|
visible = false;
|
||||||
|
onClickOutside?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={handleClick} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'transition-all duration-300 delay-200 cubic-bezier-out',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
bind:this={wrapper}
|
||||||
|
>
|
||||||
|
{@render action?.()}
|
||||||
|
{#if visible}
|
||||||
|
<div
|
||||||
|
class="relative z-20 h-full w-auto flex flex-col gap-4"
|
||||||
|
in:fade={{ duration: 300, delay: 400, easing: cubicOut }}
|
||||||
|
out:fade={{ duration: 150, easing: cubicOut }}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Background Gradient -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
absolute inset-0 z-10 h-full transition-all duration-700
|
||||||
|
bg-linear-to-r from-white/75 via-white/45 to-white/10
|
||||||
|
bg-[radial-gradient(ellipse_at_left,_rgba(255,252,245,0.4)_0%,_transparent_70%)]
|
||||||
|
shadow-[_inset_-1px_0_0_rgba(0,0,0,0.04)]
|
||||||
|
border-r border-white/90
|
||||||
|
after:absolute after:right-[-1px] after:top-0 after:h-full after:w-[1px] after:bg-black/[0.05]
|
||||||
|
backdrop-blur-md
|
||||||
|
"
|
||||||
|
in:slide={{ axis: 'x', duration: 250, delay: 100, easing: cubicOut }}
|
||||||
|
out:slide={{ axis: 'x', duration: 150, easing: cubicOut }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user