first commit
linter / quality (push) Canceled after 0s
tests / ci (8.3) (push) Canceled after 0s
tests / ci (8.4) (push) Canceled after 0s
tests / ci (8.5) (push) Canceled after 0s

This commit is contained in:
salam committed 2026-10-06 18:53:03 +02:00
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;
}