first commit
This commit is contained in:
commit
e687b4437b
418 files changed
+33577
No files matched your search
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
SIDEBAR_CONTEXT,
|
||||
SIDEBAR_WIDTH_MOBILE,
|
||||
type SidebarContext,
|
||||
} from './context';
|
||||
|
||||
let {
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
class: className = '',
|
||||
children,
|
||||
}: {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const { isMobile, state, openMobile, setOpenMobile } = getContext<SidebarContext>(SIDEBAR_CONTEXT);
|
||||
</script>
|
||||
|
||||
{#if collapsible === 'none'}
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
class={cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else if $isMobile}
|
||||
{#if $openMobile}
|
||||
<div class="fixed inset-0 z-50">
|
||||
<button
|
||||
type="button"
|
||||
class="fixed inset-0 bg-black/50"
|
||||
aria-label="Close"
|
||||
onclick={() => setOpenMobile(false)}
|
||||
transition:fade={{ duration: 200 }}
|
||||
></button>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
class={cn(
|
||||
'fixed inset-y-0 bg-sidebar text-sidebar-foreground h-svh w-(--sidebar-width) p-0',
|
||||
side === 'left' ? 'left-0 border-r' : 'right-0 border-l',
|
||||
className,
|
||||
)}
|
||||
style={`--sidebar-width: ${SIDEBAR_WIDTH_MOBILE};`}
|
||||
transition:fly={{ x: side === 'left' ? -320 : 320, duration: 300, opacity: 1, easing: cubicOut }}
|
||||
>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="group peer text-sidebar-foreground hidden md:block"
|
||||
data-slot="sidebar"
|
||||
data-state={$state}
|
||||
data-collapsible={$state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
class={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
class={cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="sidebar-footer" data-sidebar="footer" class={cn('flex flex-col gap-2 p-2', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="sidebar-group" data-sidebar="group" class={cn('relative flex w-full min-w-0 flex-col p-2', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div data-slot="sidebar-group-content" data-sidebar="group-content" class="w-full text-sm">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
class="text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="sidebar-header" data-sidebar="header" class={cn('flex flex-col gap-2 p-2', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
class={cn(
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<ul data-slot="sidebar-menu" data-sidebar="menu" class={cn('flex w-full min-w-0 flex-col gap-1', className)}>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { SIDEBAR_CONTEXT, type SidebarContext } from './context';
|
||||
|
||||
type Size = 'default' | 'lg';
|
||||
type AsChildProps = {
|
||||
class?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
let {
|
||||
asChild = false,
|
||||
class: className = '',
|
||||
isActive = false,
|
||||
size = 'default',
|
||||
tooltip,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
asChild?: boolean;
|
||||
class?: string;
|
||||
isActive?: boolean;
|
||||
size?: Size;
|
||||
tooltip?: string;
|
||||
children?: Snippet<[AsChildProps]>;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
||||
const { isMobile, state } = getContext<SidebarContext>(SIDEBAR_CONTEXT);
|
||||
|
||||
const base =
|
||||
'peer/menu-button ring-sidebar-ring flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0';
|
||||
const sizeClasses: Record<Size, string> = {
|
||||
default: 'h-8 text-sm',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
||||
};
|
||||
|
||||
const classes = () => {
|
||||
const activeClasses = isActive ? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground' : '';
|
||||
return cn(base, sizeClasses[size], activeClasses, className);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if tooltip}
|
||||
<Tooltip disabled={$state !== 'collapsed' || $isMobile}>
|
||||
<TooltipTrigger>
|
||||
{#snippet child({ props: triggerProps })}
|
||||
{#if asChild}
|
||||
{@render children?.({
|
||||
class: classes(),
|
||||
'data-slot': 'sidebar-menu-button',
|
||||
'data-sidebar': 'menu-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...rest,
|
||||
...triggerProps,
|
||||
})}
|
||||
{:else}
|
||||
<button
|
||||
class={classes()}
|
||||
type="button"
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
{...rest}
|
||||
{...triggerProps}
|
||||
>
|
||||
{@render children?.({})}
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" align="center">
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{#if asChild}
|
||||
{@render children?.({
|
||||
class: classes(),
|
||||
'data-slot': 'sidebar-menu-button',
|
||||
'data-sidebar': 'menu-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...rest,
|
||||
})}
|
||||
{:else}
|
||||
<button
|
||||
class={classes()}
|
||||
type="button"
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.({})}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,3 @@
|
||||
<li data-slot="sidebar-menu-item" data-sidebar="menu-item" class="group/menu-item relative">
|
||||
<slot />
|
||||
</li>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { derived, readable, writable } from 'svelte/store';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import {
|
||||
SIDEBAR_CONTEXT,
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_KEYBOARD_SHORTCUT,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON,
|
||||
type SidebarContext,
|
||||
} from './context';
|
||||
|
||||
let {
|
||||
defaultOpen = true,
|
||||
class: className = '',
|
||||
children,
|
||||
}: {
|
||||
defaultOpen?: boolean;
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const initialOpen = () => {
|
||||
if (typeof document === 'undefined') return defaultOpen;
|
||||
const hasCookie = document.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`);
|
||||
return hasCookie ? false : defaultOpen;
|
||||
};
|
||||
|
||||
const open = writable(initialOpen());
|
||||
const openMobile = writable(false);
|
||||
|
||||
const isMobile = readable(false, (set) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const media = window.matchMedia('(max-width: 768px)');
|
||||
const update = () => set(media.matches);
|
||||
update();
|
||||
media.addEventListener('change', update);
|
||||
return () => media.removeEventListener('change', update);
|
||||
});
|
||||
|
||||
const state = derived(open, ($open) => ($open ? 'expanded' : 'collapsed'));
|
||||
|
||||
const setOpen = (value: boolean) => {
|
||||
open.set(value);
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
}
|
||||
};
|
||||
|
||||
const setOpenMobile = (value: boolean) => {
|
||||
openMobile.set(value);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if ($isMobile) {
|
||||
setOpenMobile(!$openMobile);
|
||||
} else {
|
||||
setOpen(!$open);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
|
||||
const context: SidebarContext = {
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
};
|
||||
|
||||
setContext(SIDEBAR_CONTEXT, context);
|
||||
</script>
|
||||
|
||||
<TooltipProvider>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={`--sidebar-width: ${SIDEBAR_WIDTH}; --sidebar-width-icon: ${SIDEBAR_WIDTH_ICON};`}
|
||||
class={cn(
|
||||
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SIDEBAR_CONTEXT, type SidebarContext } from './context';
|
||||
|
||||
let { class: className = '' } = $props();
|
||||
|
||||
const { isMobile, state, toggleSidebar } = getContext<SidebarContext>(SIDEBAR_CONTEXT);
|
||||
</script>
|
||||
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('h-7 w-7', className)}
|
||||
onclick={toggleSidebar}
|
||||
>
|
||||
{#if $isMobile || $state === 'collapsed'}
|
||||
<PanelLeftOpen class="size-4" />
|
||||
{:else}
|
||||
<PanelLeftClose class="size-4" />
|
||||
{/if}
|
||||
<span class="sr-only">Toggle sidebar</span>
|
||||
</Button>
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
|
||||
export type SidebarContext = {
|
||||
state: Readable<'expanded' | 'collapsed'>;
|
||||
open: Writable<boolean>;
|
||||
setOpen: (value: boolean) => void;
|
||||
isMobile: Readable<boolean>;
|
||||
openMobile: Writable<boolean>;
|
||||
setOpenMobile: (value: boolean) => void;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
export const SIDEBAR_CONTEXT = Symbol('sidebar');
|
||||
@@ -0,0 +1,15 @@
|
||||
export { default as Sidebar } from './Sidebar.svelte';
|
||||
export { default as SidebarContent } from './SidebarContent.svelte';
|
||||
export { default as SidebarFooter } from './SidebarFooter.svelte';
|
||||
export { default as SidebarGroup } from './SidebarGroup.svelte';
|
||||
export { default as SidebarGroupContent } from './SidebarGroupContent.svelte';
|
||||
export { default as SidebarGroupLabel } from './SidebarGroupLabel.svelte';
|
||||
export { default as SidebarHeader } from './SidebarHeader.svelte';
|
||||
export { default as SidebarInset } from './SidebarInset.svelte';
|
||||
export { default as SidebarMenu } from './SidebarMenu.svelte';
|
||||
export { default as SidebarMenuButton } from './SidebarMenuButton.svelte';
|
||||
export { default as SidebarMenuItem } from './SidebarMenuItem.svelte';
|
||||
export { default as SidebarProvider } from './SidebarProvider.svelte';
|
||||
export { default as SidebarTrigger } from './SidebarTrigger.svelte';
|
||||
export { type SidebarContext } from './context';
|
||||
export { useSidebar } from './useSidebar';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { SIDEBAR_CONTEXT, type SidebarContext } from './context';
|
||||
|
||||
export function useSidebar(): SidebarContext {
|
||||
const context = getContext<SidebarContext>(SIDEBAR_CONTEXT);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Sidebar context is not available');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
Reference in new issue
Block a user