feat(SidebarMenu): create a shared sidebar menu that slides to the screen

This commit is contained in:
Ilia Mashkov
2026-02-15 23:08:22 +03:00
parent 99966d2de9
commit a0ac52a348

View 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>