refactor(ui): update shared components and add ControlGroup, SidebarContainer
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<!--
|
||||
Component: SidebarContainer
|
||||
Wraps <Sidebar> and handles show/hide behaviour for both breakpoints.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import {
|
||||
fade,
|
||||
fly,
|
||||
} from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Sidebar open state
|
||||
*/
|
||||
isOpen: boolean;
|
||||
/**
|
||||
* Sidebar snippet
|
||||
*/
|
||||
sidebar?: Snippet<[{ onClose: () => void }]>;
|
||||
/**
|
||||
* CSS classes
|
||||
*/
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
sidebar,
|
||||
class: className,
|
||||
}: Props = $props();
|
||||
|
||||
const responsive = getContext<ResponsiveManager>('responsive');
|
||||
|
||||
function close() {
|
||||
isOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if responsive.isMobile}
|
||||
<!--
|
||||
── MOBILE: fixed overlay ─────────────────────────────────────────────
|
||||
Only rendered when open. Both backdrop and panel use Svelte transitions
|
||||
so they animate in and out independently.
|
||||
-->
|
||||
{#if isOpen}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
|
||||
transition:fade={{ duration: 200 }}
|
||||
onclick={close}
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Panel -->
|
||||
<div
|
||||
class="fixed left-0 top-0 bottom-0 w-80 z-50 shadow-2xl"
|
||||
in:fly={{ x: -320, duration: 300, easing: cubicOut }}
|
||||
out:fly={{ x: -320, duration: 250, easing: cubicOut }}
|
||||
>
|
||||
{#if sidebar}
|
||||
{@render sidebar({ onClose: close })}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!--
|
||||
── DESKTOP: collapsible column ───────────────────────────────────────
|
||||
Always in the DOM — width transitions between 320px and 0.
|
||||
overflow-hidden clips the w-80 inner div during the collapse.
|
||||
|
||||
transition-[width] is on the outer shell.
|
||||
duration-300 + ease-out approximates the spring(300, 30) feel.
|
||||
The inner div stays w-80 so Sidebar layout never reflows mid-animation.
|
||||
-->
|
||||
<div
|
||||
class={cn(
|
||||
'shrink-0 z-30 h-full relative',
|
||||
'overflow-hidden',
|
||||
'will-change-[width]',
|
||||
'transition-[width] duration-300 ease-out',
|
||||
'border-r border-black/5 dark:border-white/10',
|
||||
'bg-[#f3f0e9] dark:bg-[#121212]',
|
||||
isOpen ? 'w-80 opacity-100' : 'w-0 opacity-0',
|
||||
'transition-[width,opacity] duration-300 ease-out',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<!-- Fixed-width inner so content never reflows during width animation -->
|
||||
<div class="w-80 h-full">
|
||||
{#if sidebar}
|
||||
{@render sidebar({ onClose: close })}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user