init commit

This commit is contained in:
unurled 2025-06-23 23:12:40 +02:00
commit c9d982669a
461 changed files with 30317 additions and 0 deletions

18
resources/js/app.ts Normal file
View file

@ -0,0 +1,18 @@
import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte';
import { hydrate, mount } from 'svelte';
import '../css/app.css';
import './bootstrap';
createInertiaApp({
resolve: (name: string) => {
const pages = import.meta.glob<ResolvedComponent>('./pages/**/*.svelte', { eager: true });
return pages[`./pages/${name}.svelte`];
},
setup({ el, App, props }) {
if (el && el.dataset.serverRendered === 'true') {
hydrate(App, { target: el, props });
} else if (el) {
mount(App, { target: el, props });
}
},
});

View file

@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { SidebarInset } from '@/components/ui/sidebar';
import type { Snippet } from 'svelte';
type Props = {
variant?: 'header' | 'sidebar';
class?: string;
children: Snippet;
};
let { variant, class: className, children }: Props = $props();
</script>
{#if variant === 'sidebar'}
<SidebarInset class={className}>
<div class="mx-auto w-full max-w-7xl">
{@render children?.()}
</div>
</SidebarInset>
{:else}
<main class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl {className}">
{@render children?.()}
</main>
{/if}

View file

@ -0,0 +1,199 @@
<script lang="ts">
import AppLogo from '@/components/AppLogo.svelte';
import AppLogoIcon from '@/components/AppLogoIcon.svelte';
import Breadcrumbs from '@/components/Breadcrumbs.svelte';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import UserMenuContent from '@/components/UserMenuContent.svelte';
import { getInitials } from '@/hooks/useInitials';
import type { BreadcrumbItem } from '@/types';
import { Link, page } from '@inertiajs/svelte';
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-svelte';
import { navigationMenuTriggerStyle } from './ui/navigation-menu/navigation-menu-trigger.svelte';
interface NavItem {
title: string;
href: string;
icon?: any;
}
interface Props {
breadcrumbs?: BreadcrumbItem[];
}
let { breadcrumbs = [] }: Props = $props();
let user = $derived($page.props.auth.user);
const isCurrentRoute = $derived((url: string) => $page.url === url);
const activeItemStyles = $derived((url: string) => (isCurrentRoute(url) ? 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : ''));
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
icon: LayoutGrid,
},
];
const rightNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/oseughu/svelte-starter-kit',
icon: Folder,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits',
icon: BookOpen,
},
];
</script>
<div>
<div class="border-b border-sidebar-border/80">
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
<div class="lg:hidden">
<Sheet>
<SheetTrigger>
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
<Menu class="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="w-[300px] p-6">
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
<SheetHeader class="flex justify-start text-left">
<AppLogoIcon class="size-6 fill-current text-black dark:text-white" />
</SheetHeader>
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
<nav class="-mx-3 space-y-1">
{#each mainNavItems as item (item.title)}
<a
href={item.href}
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent {activeItemStyles(
item.href,
)}"
>
{#if item.icon}
<item.icon class="h-5 w-5" />
{/if}
{item.title}
</a>
{/each}
</nav>
<div class="flex flex-col space-y-4">
{#each rightNavItems as item (item.title)}
<Link href={item.href} class="flex items-center space-x-2 text-sm font-medium">
{#if item.icon}
<item.icon class="h-5 w-5" />
{/if}
<span>{item.title}</span>
</Link>
{/each}
</div>
</div>
</SheetContent>
</Sheet>
</div>
<Link href={route('dashboard')} class="flex items-center gap-x-2">
<AppLogo />
</Link>
<!-- Desktop Menu -->
<div class="hidden h-full lg:flex lg:flex-1">
<Menubar class="ml-10 flex h-full items-center border-none bg-transparent">
{#each mainNavItems as item, index (index)}
<MenubarMenu>
<MenubarTrigger
value={item.href}
class="{navigationMenuTriggerStyle()} {activeItemStyles(
item.href,
)} relative flex h-full cursor-pointer items-center px-4 text-sm font-medium"
>
{#if item.icon}
<item.icon class="mr-2 h-4 w-4" />
{/if}
{item.title}
{#if isCurrentRoute(item.href)}
<div class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"></div>
{/if}
</MenubarTrigger>
<MenubarContent align="start">
<MenubarItem>
<Link href={item.href}>
{item.title}
</Link>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
{/each}
</Menubar>
</div>
<div class="ml-auto flex items-center space-x-2">
<div class="relative flex items-center space-x-1">
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
<Search class="size-5 opacity-80 group-hover:opacity-100" />
</Button>
<div class="hidden space-x-1 lg:flex">
{#each rightNavItems as item (item.title)}
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
<a href={item.href} target="_blank" rel="noopener noreferrer">
<span class="sr-only">{item.title}</span>
<item.icon class="size-5 opacity-80 group-hover:opacity-100" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/each}
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="ghost"
size="icon"
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
>
<Avatar class="size-8 overflow-hidden rounded-full">
{#if user.avatar}
<AvatarImage src={user.avatar} alt={user.name} />
{:else}
<AvatarFallback class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white">
{getInitials(user.name || '')}
</AvatarFallback>
{/if}
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-56">
<UserMenuContent {user} />
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
{#if breadcrumbs.length > 1}
<div class="flex w-full border-b border-sidebar-border/70">
<div class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
<Breadcrumbs {breadcrumbs} />
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import AppLogoIcon from '@/components/AppLogoIcon.svelte';
</script>
<div class="inline-flex items-center gap-3">
<div class="flex aspect-square size-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary">
<AppLogoIcon class="size-5 fill-current text-white! dark:text-black!" />
</div>
<div class="flex items-center">
<span class="truncate text-sm font-semibold">Laravel Starter Kit</span>
</div>
</div>

View file

@ -0,0 +1,12 @@
<script lang="ts">
let { ...attrs } = $props();
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {...attrs}>
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
/>
</svg>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { SidebarProvider } from '@/components/ui/sidebar';
import { page } from '@inertiajs/svelte';
import type { Snippet } from 'svelte';
interface Props {
variant?: 'header' | 'sidebar';
class?: string;
children?: Snippet;
}
let { variant = 'sidebar', class: className, children }: Props = $props();
const isOpen = $derived($page.props.sidebarOpen as boolean);
</script>
{#if variant === 'header'}
<div class="flex min-h-screen w-full flex-col {className}">
{@render children?.()}
</div>
{:else}
<SidebarProvider open={isOpen}>
{@render children?.()}
</SidebarProvider>
{/if}

View file

@ -0,0 +1,55 @@
<script lang="ts">
import NavFooter from '@/components/NavFooter.svelte';
import NavMain from '@/components/NavMain.svelte';
import NavUser from '@/components/NavUser.svelte';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/svelte';
import { BookOpen, Folder, LayoutGrid } from 'lucide-svelte';
import AppLogo from './AppLogo.svelte';
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
icon: LayoutGrid,
},
];
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/oseughu/svelte-starter-kit',
icon: Folder,
},
{
title: 'Admin Dashboard',
href: '/admin',
icon: LayoutGrid,
requireAdmin: true,
},
];
</script>
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg">
<Link href={route('dashboard')}>
<AppLogo />
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={mainNavItems} />
</SidebarContent>
<SidebarFooter>
<NavFooter items={footerNavItems} class="mt-auto" />
<NavUser />
</SidebarFooter>
</Sidebar>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import Breadcrumbs from '@/components/Breadcrumbs.svelte';
import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItem } from '@/types';
interface Props {
breadcrumbs?: BreadcrumbItem[];
}
let { breadcrumbs = [] }: Props = $props();
</script>
<header
class="flex h-16 shrink-0 items-center gap-2 border-b border-sidebar-border/70 px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
>
<div class="flex items-center gap-2">
<SidebarTrigger class="-ml-1" />
{#if breadcrumbs.length > 0}
<Breadcrumbs {breadcrumbs} />
{/if}
</div>
</header>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import type { Appearance } from '@/hooks/useAppearance.svelte';
import { useAppearance } from '@/hooks/useAppearance.svelte';
import { Monitor, Moon, Sun } from 'lucide-svelte';
interface Props {
class?: string;
}
const appearanceManager = useAppearance();
// Create a local reactive variable that tracks the hook's value
let currentAppearance = $state(appearanceManager.appearance);
// Update the local state when the appearance changes
function handleUpdateAppearance(value: Appearance) {
appearanceManager.updateAppearance(value);
// Immediately update local state to ensure UI reflects the change
currentAppearance = value;
}
const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
] as const;
let { class: className }: Props = $props();
</script>
<div class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800 {className}">
{#each tabs as { value, Icon, label } (value)}
<button
onclick={() => handleUpdateAppearance(value)}
class="flex items-center rounded-md px-3.5 py-1.5 transition-colors
{currentAppearance === value
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60'}"
>
<Icon class="-ml-1 h-4 w-4" />
<span class="ml-1.5 text-sm">{label}</span>
</button>
{/each}
</div>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { Breadcrumb, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Item } from '@/components/ui/breadcrumb';
import { Link } from '@inertiajs/svelte';
interface BreadcrumbItem {
title: string;
href?: string;
}
interface Props {
breadcrumbs: BreadcrumbItem[];
}
let { breadcrumbs }: Props = $props();
</script>
<Breadcrumb>
<BreadcrumbList>
{#each breadcrumbs as item, index (index)}
<Item>
{#if index === breadcrumbs.length - 1}
<BreadcrumbPage>{item.title}</BreadcrumbPage>
{:else}
<BreadcrumbLink>
<Link href={item.href ?? '#'}>{item.title}</Link>
</BreadcrumbLink>
{/if}
</Item>
{#if index !== breadcrumbs.length - 1}
<BreadcrumbSeparator />
{/if}
{/each}
</BreadcrumbList>
</Breadcrumb>

View file

@ -0,0 +1,82 @@
<script lang="ts">
import HeadingSmall from '@/components/HeadingSmall.svelte';
import InputError from '@/components/InputError.svelte';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useForm } from '@inertiajs/svelte';
let passwordInput = $state(null as unknown as HTMLInputElement);
const form = useForm({
password: '',
});
const deleteUser = (e: Event) => {
e.preventDefault();
$form.delete(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput?.focus(),
onFinish: () => $form.reset(),
});
};
const closeModal = () => {
$form.clearErrors();
$form.reset();
};
</script>
<div class="space-y-6">
<HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
<div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
<p class="font-medium">Warning</p>
<p class="text-sm">Please proceed with caution, this cannot be undone.</p>
</div>
<Dialog>
<DialogTrigger>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<form class="space-y-6" onsubmit={deleteUser}>
<DialogHeader class="space-y-3">
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password
to confirm you would like to permanently delete your account.
</DialogDescription>
</DialogHeader>
<div class="grid gap-2">
<Label for="password" class="sr-only">Password</Label>
<Input id="password" type="password" name="password" ref={passwordInput} bind:value={$form.password} placeholder="Password" />
<InputError message={$form.errors.password} />
</div>
<DialogFooter class="gap-2">
<DialogClose>
<Button variant="secondary" onclick={closeModal}>Cancel</Button>
</DialogClose>
<Button variant="destructive" disabled={$form.processing}>
<button type="submit">Delete account</button>
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
interface Props {
title: string;
description?: string;
}
let { title, description }: Props = $props();
</script>
<div class="mb-8 space-y-0.5">
<h2 class="text-xl font-semibold tracking-tight">{title}</h2>
{#if description}
<p class="text-sm text-muted-foreground">{description}</p>
{/if}
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
interface Props {
title: string;
description?: string;
}
let { title, description }: Props = $props();
</script>
<header>
<h3 class="mb-0.5 text-base font-medium">{title}</h3>
{#if description}
<p class="text-sm text-muted-foreground">{description}</p>
{/if}
</header>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import * as icons from 'lucide-svelte';
interface Props {
name: string;
class?: string;
size?: number | string;
color?: string;
strokeWidth?: number | string;
}
let { name, class: className, size = 16, color, strokeWidth = 2 }: Props = $props();
const Component = $derived(() => {
const iconName = name.charAt(0).toUpperCase() + name.slice(1);
return (icons as Record<string, any>)[iconName];
});
const styles = $derived(['h-4 w-4', className]);
</script>
<Component class={styles} {size} {color} {strokeWidth} />

View file

@ -0,0 +1,14 @@
<script lang="ts">
interface Props {
message?: string;
class?: string;
}
let { message, class: className }: Props = $props();
</script>
{#if message}
<p class="text-sm text-red-600 dark:text-red-500 {className}">
{message}
</p>
{/if}

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import type { NavItem, User } from '@/types';
import { page } from '@inertiajs/svelte';
import { onMount } from 'svelte';
interface Props {
items: NavItem[];
class?: string;
}
let { items, class: className }: Props = $props();
// user
const isAdmin: boolean = $page.props.auth.isAdmin ?? false;
</script>
<SidebarGroup class="group-data-[collapsible=icon]:p-0 {className}">
<SidebarGroupContent>
<SidebarMenu>
{#each items as item, index (index)}
{#if item.requireAdmin && !isAdmin}{:else}
<SidebarMenuItem>
<SidebarMenuButton class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100">
<a href={item.href} target="_blank" rel="noopener noreferrer" class="block w-full">
<div class="flex items-center gap-2 w-full">
{#if item.icon}
{@const Icon = item.icon}
<Icon class="h-4 w-4 shrink-0" />
{/if}
<span>{item.title}</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
{/if}
{/each}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import type { NavItem } from '@/types';
import { Link, page } from '@inertiajs/svelte';
interface Props {
items: NavItem[];
}
let { items = [] }: Props = $props();
</script>
<SidebarGroup class="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{#each items as item (item.title)}
<SidebarMenuItem>
<Link href={item.href} class="block w-full">
<SidebarMenuButton isActive={item.href === $page.url}>
{#snippet tooltipContent()}
{item.title}
{/snippet}
{#if item.icon}
{@const Icon = item.icon}
<Icon class="h-4 w-4 shrink-0" />
{/if}
<span>{item.title}</span>
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
{/each}
</SidebarMenu>
</SidebarGroup>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import UserInfo from '@/components/UserInfo.svelte';
import UserMenuContent from '@/components/UserMenuContent.svelte';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { page } from '@inertiajs/svelte';
import { ChevronsUpDown } from 'lucide-svelte';
const user = $derived($page.props.auth.user);
</script>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<SidebarMenuButton size="lg" class="text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent group">
<UserInfo {user} />
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" side="bottom" align="end" sideOffset={4}>
<UserMenuContent {user} />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>

View file

@ -0,0 +1,17 @@
<script lang="ts">
interface Props {
class?: string;
}
let { class: className }: Props = $props();
let patternId = $derived(`pattern-${Math.random().toString(36).substring(2, 9)}`);
</script>
<svg class={className} fill="none">
<defs>
<pattern id={patternId} x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M-3 13 15-5M-5 5l18-18M-1 21 17 3"></path>
</pattern>
</defs>
<rect stroke="none" fill={`url(#${patternId})`} width="100%" height="100%"></rect>
</svg>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import type { Method } from '@inertiajs/core';
import { Link } from '@inertiajs/svelte';
import type { Snippet } from 'svelte';
interface Props {
href: string;
tabindex?: number;
method?: Method;
as?: keyof HTMLElementTagNameMap;
class?: string;
children: Snippet;
}
let { href, tabindex, method, as, class: className, children }: Props = $props();
</script>
<Link
{href}
{tabindex}
{method}
{as}
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:!decoration-current dark:decoration-neutral-500 {className}"
>
{@render children()}
</Link>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/hooks/useInitials';
import type { User } from '@/types';
interface Props {
user: User;
showEmail?: boolean;
}
let { user, showEmail = false }: Props = $props();
const { getInitials } = useInitials();
let showAvatar = $derived(user.avatar && user.avatar !== '');
</script>
<Avatar class="h-8 w-8 overflow-hidden rounded-full">
{#if showAvatar}
<AvatarImage src={user.avatar} alt={user.name} />
{:else}
<AvatarFallback class="rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{getInitials(user.name)}
</AvatarFallback>
{/if}
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{user.name}</span>
{#if showEmail}
<span class="truncate text-xs text-muted-foreground">{user.email}</span>
{/if}
</div>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import UserInfo from '@/components/UserInfo.svelte';
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import type { User } from '@/types';
import { Link, router } from '@inertiajs/svelte';
import { LogOut, Settings } from 'lucide-svelte';
interface Props {
user: User;
}
let { user }: Props = $props();
const handleLogout = () => {
router.flushAll();
};
</script>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserInfo {user} showEmail={true} />
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Link class="block w-full" href={route('profile.edit')} prefetch as="button">
<div class="flex items-center">
<Settings class="mr-2 h-4 w-4" />
<span>Settings</span>
</div>
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link class="block w-full" method="post" onclick={handleLogout} href={route('logout')} as="button">
<div class="flex items-center">
<LogOut class="mr-2 h-4 w-4" />
<span>Log out</span>
</div>
</Link>
</DropdownMenuItem>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
data-slot="accordion-content"
class={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
className
)}
{...restProps}
>
<div class="pb-4 pt-0">
{@render children?.()}
</div>
</AccordionPrimitive.Content>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item
bind:ref
data-slot="accordion-item"
class={cn("border-b last:border-b-0", className)}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: AccordionPrimitive.RootProps = $props();
</script>
<AccordionPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="accordion"
{...restProps}
/>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn, type WithoutChild } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
bind:ref
class={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>

View file

@ -0,0 +1,16 @@
import Root from "./accordion-root.svelte";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "@/components/ui/button/index.js";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.ActionProps = $props();
</script>
<AlertDialogPrimitive.Action
bind:ref
data-slot="alert-dialog-action"
class={cn(buttonVariants(), className)}
{...restProps}
/>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "@/components/ui/button/index.js";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.CancelProps = $props();
</script>
<AlertDialogPrimitive.Cancel
bind:ref
data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant: "outline" }), className)}
{...restProps}
/>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
} = $props();
</script>
<AlertDialogPrimitive.Portal {...portalProps}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
bind:ref
data-slot="alert-dialog-content"
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...restProps}
/>
</AlertDialogPrimitive.Portal>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.DescriptionProps = $props();
</script>
<AlertDialogPrimitive.Description
bind:ref
data-slot="alert-dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.OverlayProps = $props();
</script>
<AlertDialogPrimitive.Overlay
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.TitleProps = $props();
</script>
<AlertDialogPrimitive.Title
bind:ref
data-slot="alert-dialog-title"
class={cn("text-lg font-semibold", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
</script>
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />

View file

@ -0,0 +1,39 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Portal = AlertDialogPrimitive.Portal;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
});
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
</div>

View file

@ -0,0 +1,14 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { AspectRatio as AspectRatioPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: AspectRatioPrimitive.RootProps = $props();
</script>
<AspectRatioPrimitive.Root bind:ref data-slot="aspect-ratio" {...restProps} />

View file

@ -0,0 +1,3 @@
import Root from "./aspect-ratio.svelte";
export { Root, Root as AspectRatio };

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn("aspect-square size-full", className)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
data-slot="avatar"
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
{...restProps}
/>

View file

@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

View file

@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View file

@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View file

@ -0,0 +1,23 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef, type WithoutChildren } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={ref}
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
class={cn("flex size-9 items-center justify-center", className)}
{...restProps}
>
<EllipsisIcon class="size-4" />
<span class="sr-only">More</span>
</span>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li
bind:this={ref}
data-slot="breadcrumb-item"
class={cn("inline-flex items-center gap-1.5", className)}
{...restProps}
>
{@render children?.()}
</li>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
href = undefined,
child,
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
} = $props();
const attrs = $derived({
"data-slot": "breadcrumb-link",
class: cn("hover:text-foreground transition-colors", className),
href,
...restProps,
});
</script>
{#if child}
{@render child({ props: attrs })}
{:else}
<a bind:this={ref} {...attrs}>
{@render children?.()}
</a>
{/if}

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLOlAttributes> = $props();
</script>
<ol
bind:this={ref}
data-slot="breadcrumb-list"
class={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className
)}
{...restProps}
>
{@render children?.()}
</ol>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
class={cn("text-foreground font-normal", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLLiAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li
bind:this={ref}
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
<ChevronRightIcon />
{/if}
</li>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav
bind:this={ref}
data-slot="breadcrumb"
class={className}
aria-label="breadcrumb"
{...restProps}
>
{@render children?.()}
</nav>

View file

@ -0,0 +1,25 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View file

@ -0,0 +1,80 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
outline:
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View file

@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.CellProps = $props();
</script>
<CalendarPrimitive.Cell
bind:ref
class={cn(
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-8 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { buttonVariants } from "@/components/ui/button/index.js";
import { cn } from "@/lib/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.DayProps = $props();
</script>
<CalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: "ghost" }),
"size-8 select-none p-0 font-normal",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground",
// Selected
"data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:bg-primary data-selected:hover:text-primary-foreground data-selected:focus:bg-primary data-selected:focus:text-primary-foreground data-selected:opacity-100 dark:data-selected:hover:bg-primary dark:data-selected:focus:bg-primary",
// Disabled
"data-disabled:text-muted-foreground data-disabled:opacity-50",
// Unavailable
"data-unavailable:text-destructive-foreground data-unavailable:line-through",
// Outside months
"data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridBodyProps = $props();
</script>
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridHeadProps = $props();
</script>
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridRowProps = $props();
</script>
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridProps = $props();
</script>
<CalendarPrimitive.Grid
bind:ref
class={cn("w-full border-collapse space-y-1", className)}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadCellProps = $props();
</script>
<CalendarPrimitive.HeadCell
bind:ref
class={cn("text-muted-foreground w-8 rounded-md text-[0.8rem] font-normal", className)}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeaderProps = $props();
</script>
<CalendarPrimitive.Header
bind:ref
class={cn("relative flex w-full items-center justify-between pt-1", className)}
{...restProps}
/>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading bind:ref class={cn("text-sm font-medium", className)} {...restProps} />

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "@/components/ui/button/index.js";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronRightIcon class="size-4" />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}
{...restProps}
/>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "@/components/ui/button/index.js";
import { cn } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronLeftIcon class="size-4" />
{/snippet}
<CalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}
{...restProps}
/>

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import * as Calendar from "./index.js";
import { cn, type WithoutChildrenOrChild } from "@/lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
class: className,
weekdayFormat = "short",
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> = $props();
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root
bind:value={value as never}
bind:ref
bind:placeholder
{weekdayFormat}
class={cn("p-3", className)}
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Header>
<Calendar.PrevButton />
<Calendar.Heading />
<Calendar.NextButton />
</Calendar.Header>
<Calendar.Months>
{#each months as month (month)}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday (weekday)}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)}
<Calendar.Cell {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>

View file

@ -0,0 +1,30 @@
import Root from "./calendar.svelte";
import Cell from "./calendar-cell.svelte";
import Day from "./calendar-day.svelte";
import Grid from "./calendar-grid.svelte";
import Header from "./calendar-header.svelte";
import Months from "./calendar-months.svelte";
import GridRow from "./calendar-grid-row.svelte";
import Heading from "./calendar-heading.svelte";
import GridBody from "./calendar-grid-body.svelte";
import GridHead from "./calendar-grid-head.svelte";
import HeadCell from "./calendar-head-cell.svelte";
import NextButton from "./calendar-next-button.svelte";
import PrevButton from "./calendar-prev-button.svelte";
export {
Day,
Cell,
Grid,
Header,
Months,
GridRow,
Heading,
GridBody,
GridHead,
HeadCell,
NextButton,
PrevButton,
//
Root as Calendar,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View file

@ -0,0 +1,43 @@
<script lang="ts">
import emblaCarouselSvelte from "embla-carousel-svelte";
import type { HTMLAttributes } from "svelte/elements";
import { getEmblaContext } from "./context.js";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Content/>");
</script>
<div
data-slot="carousel-content"
class="overflow-hidden"
use:emblaCarouselSvelte={{
options: {
container: "[data-embla-container]",
slides: "[data-embla-slide]",
...emblaCtx.options,
axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
},
plugins: emblaCtx.plugins,
}}
onemblaInit={emblaCtx.onInit}
>
<div
bind:this={ref}
class={cn(
"flex",
emblaCtx.orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
data-embla-container=""
{...restProps}
>
{@render children?.()}
</div>
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { getEmblaContext } from "./context.js";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Item/>");
</script>
<div
bind:this={ref}
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
emblaCtx.orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
data-embla-slide=""
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,38 @@
<script lang="ts">
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
import type { WithoutChildren } from "bits-ui";
import { getEmblaContext } from "./context.js";
import { cn } from "@/lib/utils.js";
import { Button, type Props } from "@/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Next/>");
</script>
<Button
data-slot="carousel-next"
{variant}
{size}
class={cn(
"absolute size-8 rounded-full",
emblaCtx.orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!emblaCtx.canScrollNext}
onclick={emblaCtx.scrollNext}
onkeydown={emblaCtx.handleKeyDown}
bind:ref
{...restProps}
>
<ArrowRightIcon class="size-4" />
<span class="sr-only">Next slide</span>
</Button>

View file

@ -0,0 +1,38 @@
<script lang="ts">
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import type { WithoutChildren } from "bits-ui";
import { getEmblaContext } from "./context.js";
import { cn } from "@/lib/utils.js";
import { Button, type Props } from "@/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Previous/>");
</script>
<Button
data-slot="carousel-previous"
{variant}
{size}
class={cn(
"absolute size-8 rounded-full",
emblaCtx.orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!emblaCtx.canScrollPrev}
onclick={emblaCtx.scrollPrev}
onkeydown={emblaCtx.handleKeyDown}
{...restProps}
bind:ref
>
<ArrowLeftIcon class="size-4" />
<span class="sr-only">Previous slide</span>
</Button>

View file

@ -0,0 +1,100 @@
<script lang="ts">
import {
type CarouselAPI,
type CarouselProps,
type EmblaContext,
setEmblaContext,
} from "./context.js";
import { cn, type WithElementRef } from "@/lib/utils.js";
let {
ref = $bindable(null),
opts = {},
plugins = [],
setApi = () => {},
orientation = "horizontal",
class: className,
children,
...restProps
}: WithElementRef<CarouselProps> = $props();
let carouselState = $state<EmblaContext>({
api: undefined,
scrollPrev,
scrollNext,
orientation,
canScrollNext: false,
canScrollPrev: false,
handleKeyDown,
options: opts,
plugins,
onInit,
scrollSnaps: [],
selectedIndex: 0,
scrollTo,
});
setEmblaContext(carouselState);
function scrollPrev() {
carouselState.api?.scrollPrev();
}
function scrollNext() {
carouselState.api?.scrollNext();
}
function scrollTo(index: number, jump?: boolean) {
carouselState.api?.scrollTo(index, jump);
}
function onSelect(api: CarouselAPI) {
if (!api) return;
carouselState.canScrollPrev = api.canScrollPrev();
carouselState.canScrollNext = api.canScrollNext();
carouselState.selectedIndex = api.selectedScrollSnap();
}
$effect(() => {
if (carouselState.api) {
onSelect(carouselState.api);
carouselState.api.on("select", onSelect);
carouselState.api.on("reInit", onSelect);
}
});
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "ArrowLeft") {
e.preventDefault();
scrollPrev();
} else if (e.key === "ArrowRight") {
e.preventDefault();
scrollNext();
}
}
$effect(() => {
setApi(carouselState.api);
});
function onInit(event: CustomEvent<CarouselAPI>) {
carouselState.api = event.detail;
carouselState.scrollSnaps = carouselState.api.scrollSnapList();
}
$effect(() => {
return () => {
carouselState.api?.off("select", onSelect);
};
});
</script>
<div
bind:this={ref}
data-slot="carousel"
class={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,56 @@
import type { WithElementRef } from "@/lib/utils.js";
import type { EmblaCarouselSvelteType } from "embla-carousel-svelte";
import type emblaCarouselSvelte from "embla-carousel-svelte";
import { getContext, hasContext, setContext } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
export type CarouselAPI =
NonNullable<NonNullable<EmblaCarouselSvelteType["$$_attributes"]>["on:emblaInit"]> extends (
evt: CustomEvent<infer CarouselAPI>
) => void
? CarouselAPI
: never;
type EmblaCarouselConfig = NonNullable<Parameters<typeof emblaCarouselSvelte>[1]>;
export type CarouselOptions = EmblaCarouselConfig["options"];
export type CarouselPlugins = EmblaCarouselConfig["plugins"];
////
export type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugins;
setApi?: (api: CarouselAPI | undefined) => void;
orientation?: "horizontal" | "vertical";
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT");
export type EmblaContext = {
api: CarouselAPI | undefined;
orientation: "horizontal" | "vertical";
scrollNext: () => void;
scrollPrev: () => void;
canScrollNext: boolean;
canScrollPrev: boolean;
handleKeyDown: (e: KeyboardEvent) => void;
options: CarouselOptions;
plugins: CarouselPlugins;
onInit: (e: CustomEvent<CarouselAPI>) => void;
scrollTo: (index: number, jump?: boolean) => void;
scrollSnaps: number[];
selectedIndex: number;
};
export function setEmblaContext(config: EmblaContext): EmblaContext {
setContext(EMBLA_CAROUSEL_CONTEXT, config);
return config;
}
export function getEmblaContext(name = "This component") {
if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
throw new Error(`${name} must be used within a <Carousel.Root> component`);
}
return getContext<ReturnType<typeof setEmblaContext>>(EMBLA_CAROUSEL_CONTEXT);
}

View file

@ -0,0 +1,19 @@
import Root from "./carousel.svelte";
import Content from "./carousel-content.svelte";
import Item from "./carousel-item.svelte";
import Previous from "./carousel-previous.svelte";
import Next from "./carousel-next.svelte";
export {
Root,
Content,
Item,
Previous,
Next,
//
Root as Carousel,
Content as CarouselContent,
Item as CarouselItem,
Previous as CarouselPrevious,
Next as CarouselNext,
};

View file

@ -0,0 +1,81 @@
<script lang="ts">
import { cn, type WithElementRef } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import ChartStyle from "./chart-style.svelte";
import { setChartContext, type ChartConfig } from "./chart-utils.js";
const uid = $props.id();
let {
ref = $bindable(null),
id = uid,
class: className,
children,
config,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
config: ChartConfig;
} = $props();
const chartId = `chart-${id || uid.replace(/:/g, "")}`;
setChartContext({
get config() {
return config;
},
});
</script>
<div
bind:this={ref}
data-chart={chartId}
data-slot="chart"
class={cn(
// "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
"flex aspect-video justify-center overflow-visible text-xs",
// Overrides
//
// Stroke around dots/marks when hovering
"[&_.stroke-white]:stroke-transparent",
// override the default stroke color of lines
"[&_.lc-line]:stroke-border/50",
// by default, layerchart shows a line intersecting the point when hovering, this hides that
"[&_.lc-highlight-line]:stroke-0",
// by default, when you hover a point on a stacked series chart, it will drop the opacity
// of the other series, this overrides that
"[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs",
// We don't want the little tick lines between the axis labels and the chart, so we remove
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
// chart.
"[&_.lc-axis-tick]:stroke-0",
// We don't want to display the rule on the x/y axis, as there is already going to be
// a grid line there and rule ends up overlapping the marks because it is rendered after
// the marks
"[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0",
"[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border",
"[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border",
// Legend adjustments
"[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5",
"[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4",
"[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]",
// Labels
"[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent",
// Tick labels on th x/y axes
"[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal",
"[&_.lc-tooltip-rects-g]:fill-transparent",
"[&_.lc-layout-svg-g]:fill-transparent",
"[&_.lc-root-container]:w-full",
className
)}
{...restProps}
>
<ChartStyle id={chartId} {config} />
{@render children?.()}
</div>

View file

@ -0,0 +1,37 @@
<script lang="ts">
import { THEMES, type ChartConfig } from "./chart-utils.js";
let { id, config }: { id: string; config: ChartConfig } = $props();
const colorConfig = $derived(
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
);
const styleOpen = ">elyts<".split("").reverse().join("");
const styleClose = ">elyts/<".split("").reverse().join("");
</script>
{#if colorConfig && colorConfig.length}
{@const themeContents = Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
)
.join("\n")}
{#key id}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html `${styleOpen}
${themeContents}
${styleClose}`}
{/key}
{/if}

View file

@ -0,0 +1,159 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from "@/lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from "./chart-utils.js";
import { getTooltipContext, Tooltip as TooltipPrimitive } from "layerchart";
import type { Snippet } from "svelte";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
return `${value}`;
}
let {
ref = $bindable(null),
class: className,
hideLabel = false,
indicator = "dot",
hideIndicator = false,
labelKey,
label,
labelFormatter = defaultFormatter,
labelClassName,
formatter,
nameKey,
color,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
hideLabel?: boolean;
label?: string;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
hideIndicator?: boolean;
labelClassName?: string;
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
formatter?: Snippet<
[
{
value: unknown;
name: string;
item: TooltipPayload;
index: number;
payload: TooltipPayload[];
},
]
>;
} = $props();
const chart = useChart();
const tooltipCtx = getTooltipContext();
const formattedLabel = $derived.by(() => {
if (hideLabel || !tooltipCtx.payload?.length) return null;
const [item] = tooltipCtx.payload;
const key = labelKey || item?.label || item?.name || "value";
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
const value =
!labelKey && typeof label === "string"
? chart.config[label as keyof typeof chart.config]?.label || label
: (itemConfig?.label ?? item.label);
if (!value) return null;
if (!labelFormatter) return value;
return labelFormatter(value, tooltipCtx.payload);
});
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== "dot");
</script>
{#snippet TooltipLabel()}
{#if formattedLabel}
<div class={cn("font-medium", labelClassName)}>
{#if typeof formattedLabel === "function"}
{@render formattedLabel()}
{:else}
{formattedLabel}
{/if}
</div>
{/if}
{/snippet}
<TooltipPrimitive.Root variant="none">
<div
class={cn(
"border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
{...restProps}
>
{#if !nestLabel}
{@render TooltipLabel()}
{/if}
<div class="grid gap-1.5">
{#each tooltipCtx.payload as item, i (item.key + i)}
{@const key = `${nameKey || item.key || item.name || "value"}`}
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
{@const indicatorColor = color || item.payload?.color || item.color}
<div
class={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5",
indicator === "dot" && "items-center"
)}
>
{#if formatter && item.value !== undefined && item.name}
{@render formatter({
value: item.value,
name: item.name,
item,
index: i,
payload: tooltipCtx.payload,
})}
{:else}
{#if itemConfig?.icon}
<itemConfig.icon />
{:else if !hideIndicator}
<div
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
class={cn(
"border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]",
{
"size-2.5": indicator === "dot",
"h-full w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
></div>
{/if}
<div
class={cn(
"flex flex-1 shrink-0 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div class="grid gap-1.5">
{#if nestLabel}
{@render TooltipLabel()}
{/if}
<span class="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{#if item.value}
<span class="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div>
</TooltipPrimitive.Root>

View file

@ -0,0 +1,66 @@
import type { Tooltip } from "layerchart";
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte";
export const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: string;
icon?: Component;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
export type TooltipPayload = ExtractSnippetParams<
ComponentProps<typeof Tooltip.Root>["children"]
>["payload"][number];
// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(
config: ChartConfig,
payload: TooltipPayload,
key: string
) {
if (typeof payload !== "object" || payload === null) return undefined;
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (payload.key === key) {
configLabelKey = payload.key;
} else if (payload.name === key) {
configLabelKey = payload.name;
} else if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
type ChartContextValue = {
config: ChartConfig;
};
const chartContextKey = Symbol("chart-context");
export function setChartContext(value: ChartContextValue) {
return setContext(chartContextKey, value);
}
export function useChart() {
return getContext<ChartContextValue>(chartContextKey);
}

View file

@ -0,0 +1,6 @@
import ChartContainer from "./chart-container.svelte";
import ChartTooltip from "./chart-tooltip.svelte";
export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js";
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "@/lib/utils.js";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="text-current transition-none">
{#if checked}
<CheckIcon class="size-3.5" />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>

View file

@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
</script>
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
</script>
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let {
ref = $bindable(null),
open = $bindable(false),
...restProps
}: CollapsiblePrimitive.RootProps = $props();
</script>
<CollapsiblePrimitive.Root bind:ref data-slot="collapsible" {...restProps} />

View file

@ -0,0 +1,15 @@
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
const Root = CollapsiblePrimitive.Root;
const Trigger = CollapsiblePrimitive.Trigger;
const Content = CollapsiblePrimitive.Content;
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};

Some files were not shown because too many files have changed in this diff Show more