feat: upgrade shadcn components

This commit is contained in:
Elias Schneider
2026-06-20 23:57:34 +02:00
parent 2e77d57282
commit 80509c83ee
162 changed files with 1427 additions and 737 deletions

View File

@@ -1,6 +1,5 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"css": "src/app.css",
"baseColor": "neutral"
@@ -13,5 +12,9 @@
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
"registry": "https://shadcn-svelte.com/registry",
"style": "luma",
"iconLibrary": "lucide",
"menuColor": "default-translucent",
"menuAccent": "subtle"
}

View File

@@ -49,6 +49,7 @@
"prettier-plugin-svelte": "^3.5.2",
"prettier-plugin-tailwindcss": "^0.8.0",
"rollup": "^4.60.4",
"shadcn-svelte": "^1.3.0",
"svelte": "^5.55.8",
"svelte-check": "^4.4.8",
"svelte-sonner": "^1.1.1",

View File

@@ -1,5 +1,6 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn-svelte/tailwind.css';
@variant dark (&:where(.dark, .dark *));
@@ -19,10 +20,19 @@
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
:root {
--radius: 1rem;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -135,6 +145,15 @@
/* Font */
--font-gloock: 'gloock', serif;
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
@layer base {

View File

@@ -42,6 +42,7 @@
case 'Enter':
if (selectedIndex >= 0) {
handleSuggestionClick(filteredSuggestions[selectedIndex]);
e.preventDefault();
}
break;
case 'Escape':
@@ -80,8 +81,8 @@
<Popover.Root open={isOpen}>
<Popover.Trigger tabindex={-1} class="h-0 w-full" aria-hidden />
<Popover.Content
class="gap-0! px-1 py-2"
sameWidth
class="p-0"
sideOffset={5}
trapFocus={false}
interactOutsideBehavior="ignore"
@@ -98,7 +99,7 @@
onkeydown={(e) => {
if (e.key === 'Enter') handleSuggestionClick(suggestion);
}}
class="hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
class="hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-default items-center rounded-full py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 {selectedIndex ===
index
? 'bg-accent text-accent-foreground'
: ''}"

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Calendar } from '$lib/components/ui/calendar';
import * as Popover from '$lib/components/ui/popover';
import { m } from '$lib/paraglide/messages';
@@ -68,11 +67,10 @@
<Popover.Root bind:open>
<Popover.Trigger {id} class="w-full">
{#snippet child({ props })}
<Button
<button
{...props}
variant="outline"
class={cn(
'w-full justify-start text-left font-normal',
'w-full flex justify-start items-center font-normal h-9 bg-input/50 rounded-full p-3',
!value && 'text-muted-foreground'
)}
aria-label={m.select_a_date()}
@@ -81,7 +79,7 @@
{calendarDisplayDate
? df.format(calendarDisplayDate.toDate(getLocalTimeZone()))
: m.select_a_date()}
</Button>
</button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">

View File

@@ -24,7 +24,7 @@
data-slot="alert-dialog-content"
data-size={size}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-4xl p-5 sm:p-8 sm:pb-6 ring-1 duration-100 data-[size=default]:max-w-sm data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 gap-6 rounded-4xl p-6 shadow-xl ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-md group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none',
className
)}
{...restProps}

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="alert-dialog-footer"
class={cn(
'-mx-4 -mb-4 rounded-b-xl p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
'cn-alert-dialog-footer flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
className
)}
{...restProps}

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="alert-dialog-header"
class={cn(
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
className
)}
{...restProps}

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="alert-dialog-media"
class={cn(
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
"bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-full sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
className
)}
{...restProps}

View File

@@ -13,7 +13,7 @@
bind:ref
data-slot="alert-dialog-overlay"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm fixed inset-0 z-50',
className
)}
{...restProps}

View File

@@ -13,7 +13,7 @@
bind:ref
data-slot="alert-dialog-title"
class={cn(
'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
'text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
className
)}
{...restProps}

View File

@@ -1,19 +1,20 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
export type DropdownButtonProps = WithElementRef<HTMLAttributes<HTMLDivElement>>;
</script>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
class: className,
ref = $bindable(null),
class: className,
children,
...restProps
}: DropdownButtonProps = $props();
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="dropdown-button" class={cn('flex', className)} {...restProps}>
<div
bind:this={ref}
data-slot="alert-action"
class={cn('absolute top-2.5 right-3', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
@@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="alert-description"
class={cn('col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed', className)}
class={cn(
'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3',
className
)}
{...restProps}
>
{@render children?.()}

View File

@@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="alert-title"
class={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
class={cn(
'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3',
className
)}
{...restProps}
>
{@render children?.()}

View File

@@ -1,18 +1,17 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
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-3xl 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',
base: "grid gap-0.5 rounded-2xl border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full",
variants: {
variant: {
default: 'bg-card text-card-foreground',
success:
'bg-green-100 text-green-900 dark:bg-green-900 dark:text-green-100 [&>svg]:text-green-900 dark:[&>svg]:text-green-100',
info: 'bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100 [&>svg]:text-blue-900 dark:[&>svg]:text-blue-100',
destructive:
'bg-red-100 text-red-900 dark:bg-red-900 dark:text-red-100 [&>svg]:text-red-900 dark:[&>svg]:text-red-100',
warning:
'bg-warning text-warning-foreground border-warning/40 [&>svg]:text-warning-foreground'
'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
success:
'bg-green-100 text-green-900 dark:bg-green-900 dark:text-green-100 *:[svg]:text-current',
info: 'bg-blue-100 text-blue-900 dark:bg-blue-900 dark:text-blue-100 *:[svg]:text-current',
warning: 'bg-warning text-warning-foreground border-warning/40 *:[svg]:text-current'
}
},
defaultVariants: {
@@ -24,10 +23,10 @@
</script>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
import { LucideX } from '@lucide/svelte';
import { onMount } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -66,15 +65,15 @@
<div
bind:this={ref}
data-slot="alert"
role="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
{#if dismissibleId || onDismiss}
<button onclick={dismiss} class="absolute right-0 top-0 m-3 text-black dark:text-white"
><LucideX class="size-4" /></button
>
<button onclick={dismiss} class="absolute top-2.5 right-3 text-current">
<LucideX class="size-4" />
</button>
{/if}
</div>
{/if}

View File

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

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="avatar-badge"
class={cn(
'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className
)}
{...restProps}
>
{@render children?.()}
</span>

View File

@@ -12,6 +12,9 @@
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn('bg-muted flex size-full items-center justify-center rounded-full', className)}
class={cn(
'bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs',
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="avatar-group-count"
class={cn(
'bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2',
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/style.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="avatar-group"
class={cn(
'cn-avatar-group *:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -12,6 +12,6 @@
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn('aspect-square size-full', className)}
class={cn('rounded-full aspect-square size-full object-cover', className)}
{...restProps}
/>

View File

@@ -4,14 +4,23 @@
let {
ref = $bindable(null),
loadingStatus = $bindable('loading'),
size = 'default',
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
}: AvatarPrimitive.RootProps & {
size?: 'default' | 'sm' | 'lg';
} = $props();
</script>
<AvatarPrimitive.Root
bind:ref
bind:loadingStatus
data-slot="avatar"
class={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full border', className)}
data-size={size}
class={cn(
'size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten',
className
)}
{...restProps}
/>

View File

@@ -1,13 +1,22 @@
import Root from './avatar.svelte';
import Image from './avatar-image.svelte';
import Fallback from './avatar-fallback.svelte';
import Badge from './avatar-badge.svelte';
import Group from './avatar-group.svelte';
import GroupCount from './avatar-group-count.svelte';
export {
Root,
Image,
Fallback,
Badge,
Group,
GroupCount,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback
Fallback as AvatarFallback,
Badge as AvatarBadge,
Group as AvatarGroup,
GroupCount as AvatarGroupCount
};

View File

@@ -2,7 +2,7 @@
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
base: 'h-5 gap-1 rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none',
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from '$lib/utils/style.js';
import type { ComponentProps } from 'svelte';
import { Separator } from '$lib/components/ui/separator/index.js';
let {
ref = $bindable(null),
class: className,
orientation = 'vertical',
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-slot="button-group-separator"
{orientation}
class={cn(
'bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto',
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
child,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const mergedProps = $derived({
...restProps,
class: cn(
"bg-muted gap-2 rounded-4xl border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
className
),
'data-slot': 'button-group-text'
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render mergedProps.children?.()}
</div>
{/if}

View File

@@ -0,0 +1,46 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const buttonGroupVariants = tv({
base: "has-[>[data-variant=outline]]:[&>input]:border-border has-[>[data-variant=outline]]:[&>input:focus-visible]:border-ring has-[>[data-variant=outline]]:*:data-[slot=input-group]:border-border has-[>[data-variant=outline]]:[&>[data-slot=input-group]:has(:focus-visible)]:border-ring has-[>[data-variant=outline]]:*:data-[slot=select-trigger]:border-border has-[>[data-variant=outline]]:[&>[data-slot=select-trigger]:focus-visible]:border-ring has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-4xl flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
variants: {
orientation: {
horizontal:
'[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-4xl! [&>[data-slot]]:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
vertical:
'[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-4xl! flex-col [&>[data-slot]]:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0'
}
},
defaultVariants: {
orientation: 'horizontal'
}
});
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>['orientation'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
orientation = 'horizontal',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
orientation?: ButtonGroupOrientation;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="button-group"
data-orientation={orientation}
class={cn(buttonGroupVariants({ orientation }), className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,15 @@
import Root, { buttonGroupVariants, type ButtonGroupOrientation } from './button-group.svelte';
import Text from './button-group-text.svelte';
import Separator from './button-group-separator.svelte';
export {
Root,
Text,
Separator,
buttonGroupVariants,
type ButtonGroupOrientation,
//
Root as ButtonGroup,
Text as ButtonGroupText,
Separator as ButtonGroupSeparator
};

View File

@@ -1,15 +1,15 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { tv, type VariantProps } from 'tailwind-variants';
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-full 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 [&_svg]:shrink-0 transition active:scale-95",
base: "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
outline:
'border border-border bg-background hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent',
'border-border bg-background hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
@@ -19,10 +19,15 @@
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3 py-2',
lg: 'h-11 px-8',
icon: 'h-10 w-10'
default:
'h-9.5 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5',
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
lg: 'h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
icon: 'size-9.5',
'icon-xs': "size-6 [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-8',
'icon-lg': 'size-10'
}
},
defaultVariants: {
@@ -110,11 +115,11 @@
onclick={handleOnClick}
{...restProps}
>
<span class="flex items-center w-full justify-center">
<span class="flex w-full items-center justify-center">
<Spinner
class={cn(
'grid overflow-hidden transition-[width,opacity,margin-right] duration-400 ease-[linear(0,0.897_14.4%,1.311_31.2%,1.338_46%,1.054_80.4%,1)]',
isLoading ? 'w-4 opacity-100 mr-2' : 'w-0 opacity-0'
isLoading ? 'mr-2 w-4 opacity-100' : 'w-0 opacity-0'
)}
aria-hidden={!isLoading}
/>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import type { ComponentProps } from 'svelte';
import type Calendar from './calendar.svelte';
import CalendarMonthSelect from './calendar-month-select.svelte';
import CalendarYearSelect from './calendar-year-select.svelte';
import { DateFormatter, getLocalTimeZone, type DateValue } from '@internationalized/date';
let {
captionLayout,
months,
monthFormat,
years,
yearFormat,
month,
locale,
placeholder = $bindable(),
monthIndex = 0
}: {
captionLayout: ComponentProps<typeof Calendar>['captionLayout'];
months: ComponentProps<typeof CalendarMonthSelect>['months'];
monthFormat: ComponentProps<typeof CalendarMonthSelect>['monthFormat'];
years: ComponentProps<typeof CalendarYearSelect>['years'];
yearFormat: ComponentProps<typeof CalendarYearSelect>['yearFormat'];
month: DateValue;
placeholder: DateValue | undefined;
locale: string;
monthIndex: number;
} = $props();
function formatYear(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof yearFormat === 'function') return yearFormat(dateObj.getFullYear());
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
}
function formatMonth(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof monthFormat === 'function') return monthFormat(dateObj.getMonth() + 1);
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
}
</script>
{#snippet MonthSelect()}
<CalendarMonthSelect
{months}
{monthFormat}
value={month.month}
onchange={(e) => {
if (!placeholder) return;
const v = Number.parseInt(e.currentTarget.value);
const newPlaceholder = placeholder.set({ month: v });
placeholder = newPlaceholder.subtract({ months: monthIndex });
}}
/>
{/snippet}
{#snippet YearSelect()}
<CalendarYearSelect {years} {yearFormat} value={month.year} />
{/snippet}
{#if captionLayout === 'dropdown'}
{@render MonthSelect()}
{@render YearSelect()}
{:else if captionLayout === 'dropdown-months'}
{@render MonthSelect()}
{#if placeholder}
{formatYear(placeholder)}
{/if}
{:else if captionLayout === 'dropdown-years'}
{#if placeholder}
{formatMonth(placeholder)}
{/if}
{@render YearSelect()}
{:else}
{formatMonth(month)} {formatYear(month)}
{/if}

View File

@@ -12,7 +12,7 @@
<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',
'relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-(--cell-radius) [&:last-child[data-selected]_[data-bits-day]]:rounded-e-(--cell-radius)',
className
)}
{...restProps}

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js';
import { Calendar as CalendarPrimitive } from 'bits-ui';
@@ -13,17 +12,21 @@
<CalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: 'ghost' }),
'size-8 p-0 font-normal select-none',
'[&[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 dark:data-selected:hover:bg-primary dark:data-selected:focus:bg-primary data-selected:opacity-100',
// Disabled
'data-disabled:text-muted-foreground data-disabled:opacity-50',
// Unavailable
'data-unavailable:text-destructive-foreground data-unavailable:line-through',
'flex size-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) p-0 leading-none font-normal whitespace-nowrap select-none',
'[&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
'not-data-selected:hover:bg-accent/50 not-data-selected:hover:text-accent-foreground',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground',
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:text-foreground',
// 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',
'[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-muted-foreground data-[unavailable]:line-through',
// focus
'focus:border-ring focus:ring-ring/50 focus:relative',
// inner spans
'[&>span]:text-xs [&>span]:opacity-70',
className
)}
{...restProps}

View File

@@ -11,6 +11,6 @@
<CalendarPrimitive.Grid
bind:ref
class={cn('w-full border-collapse space-y-1', className)}
class={cn('flex w-full border-collapse flex-col', className)}
{...restProps}
/>

View File

@@ -11,6 +11,9 @@
<CalendarPrimitive.HeadCell
bind:ref
class={cn('text-muted-foreground w-8 rounded-md text-[0.8rem] font-normal', className)}
class={cn(
'text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal',
className
)}
{...restProps}
/>

View File

@@ -11,6 +11,9 @@
<CalendarPrimitive.Header
bind:ref
class={cn('relative flex w-full items-center justify-between pt-1', className)}
class={cn(
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
className
)}
{...restProps}
/>

View File

@@ -9,4 +9,8 @@
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading bind:ref class={cn('text-sm font-medium', className)} {...restProps} />
<CalendarPrimitive.Heading
bind:ref
class={cn('px-(--cell-size) text-sm font-medium', className)}
{...restProps}
/>

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let {
ref = $bindable(null),
class: className,
value,
onchange,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
</script>
<span
class={cn(
'has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]',
className
)}
>
<CalendarPrimitive.MonthSelect
bind:ref
class="bg-background dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)}
<option
value={monthItem.value}
selected={value !== undefined
? monthItem.value === value
: monthItem.value === selectedMonthItem.value}
>
{monthItem.label}
</option>
{/each}
</select>
<span
class="[&>svg]:text-muted-foreground flex h-(--cell-size) items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true"
>
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
<ChevronDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</CalendarPrimitive.MonthSelect>
</span>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { type WithElementRef, cn } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div {...restProps} bind:this={ref} class={cn('flex w-full flex-col gap-4', className)}>
{@render children?.()}
</div>

View File

@@ -12,7 +12,7 @@
<div
bind:this={ref}
class={cn('mt-4 flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4', className)}
class={cn('relative flex flex-col gap-4 md:flex-row', className)}
{...restProps}
>
{@render children?.()}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav
{...restProps}
bind:this={ref}
class={cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', className)}
>
{@render children?.()}
</nav>

View File

@@ -1,28 +1,36 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { buttonVariants, type ButtonVariant } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils/style.js';
let {
ref = $bindable(null),
class: className,
children,
variant = 'ghost',
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
}: CalendarPrimitive.NextButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<ChevronRightIcon class="size-4" />
<ChevronRightIcon class={cn('size-4', className)} />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant: 'outline' }),
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
buttonVariants({ variant }),
'size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180',
className
)}
children={children || Fallback}
{...restProps}
/>
>
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</CalendarPrimitive.NextButton>

View File

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

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
</script>
<span
class={cn(
'has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]',
className
)}
>
<CalendarPrimitive.YearSelect
bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)}
<option
value={yearItem.value}
selected={value !== undefined
? yearItem.value === value
: yearItem.value === selectedYearItem.value}
>
{yearItem.label}
</option>
{/each}
</select>
<span
class="[&>svg]:text-muted-foreground flex h-(--cell-size) items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true"
>
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
<ChevronDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</CalendarPrimitive.YearSelect>
</span>

View File

@@ -1,109 +1,89 @@
<script lang="ts">
import * as Calendar from '$lib/components/ui/calendar/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import { Calendar as CalendarPrimitive } from 'bits-ui';
import * as Calendar from './index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import { getLocale } from '$lib/paraglide/runtime';
import { cn } from '$lib/utils/style';
import { CalendarDate, DateFormatter, getLocalTimeZone, today } from '@internationalized/date';
import { Calendar as CalendarPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import type { ButtonVariant } from '../button/button.svelte';
import { isEqualMonth, type DateValue } from '@internationalized/date';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable()
placeholder = $bindable(),
class: className,
weekdayFormat = 'short',
buttonVariant = 'ghost',
captionLayout = 'dropdown',
locale = getLocale(),
months: monthsProp,
years,
monthFormat: monthFormatProp,
yearFormat = 'numeric',
day,
disableDaysOutsideMonth = false,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
value: CalendarDate | undefined;
buttonVariant?: ButtonVariant;
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
months?: CalendarPrimitive.MonthSelectProps['months'];
years?: CalendarPrimitive.YearSelectProps['years'];
monthFormat?: CalendarPrimitive.MonthSelectProps['monthFormat'];
yearFormat?: CalendarPrimitive.YearSelectProps['yearFormat'];
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
} = $props();
const locale = getLocale();
const currentDate = today(getLocalTimeZone());
const monthFmt = new DateFormatter(locale, {
month: 'long'
const monthFormat = $derived.by(() => {
if (monthFormatProp) return monthFormatProp;
if (captionLayout.startsWith('dropdown')) return 'short';
return 'long';
});
const monthOptions = Array.from({ length: 12 }, (_, i) => {
const month = currentDate.set({ month: i + 1 });
return {
value: month.month,
label: monthFmt.format(month.toDate(getLocalTimeZone()))
};
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() + i),
value: new Date().getFullYear() + i
}));
const defaultYear = $derived(
placeholder ? { value: placeholder.year, label: String(placeholder.year) } : undefined
);
const defaultMonth = $derived(
placeholder
? {
value: placeholder.month,
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined
);
const monthLabel = $derived(
monthOptions.find((m) => m.value === defaultMonth?.value)?.label ?? 'Select a month'
);
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root
type="single"
weekdayFormat="short"
class={cn('rounded-md border p-3')}
{locale}
bind:value
bind:value={value as never}
bind:ref
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
'p-3 [--cell-radius:var(--radius-4xl)] [--cell-size:--spacing(8)] bg-background group/calendar in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
className
)}
{locale}
{monthFormat}
{yearFormat}
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Header class="flex w-full items-center justify-between gap-2">
<Select.Root
type="single"
value={`${defaultMonth?.value}`}
onValueChange={(v) => {
if (!placeholder) return;
if (v === `${placeholder.month}`) return;
placeholder = placeholder.set({ month: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
{monthLabel}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label } (value)}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={`${defaultYear?.value}`}
onValueChange={(v) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.year}`) return;
placeholder = placeholder.set({ year: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
{defaultYear?.label ?? 'Select year'}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label } (value)}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
</Calendar.Header>
<Calendar.Months>
{#each months as month (month)}
<Calendar.Nav>
<Calendar.PrevButton variant={buttonVariant} />
<Calendar.NextButton variant={buttonVariant} />
</Calendar.Nav>
{#each months as month, monthIndex (month)}
<Calendar.Month>
<Calendar.Header>
<Calendar.Caption
{captionLayout}
months={monthsProp}
{monthFormat}
{years}
{yearFormat}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
/>
</Calendar.Header>
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="flex">
{#each weekdays as weekday (weekday)}
<Calendar.GridRow class="select-none">
{#each weekdays as weekday, i (i)}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
@@ -114,14 +94,22 @@
{#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)}
<Calendar.Cell class="select-none" {date} month={month.value}>
<Calendar.Cell {date} month={month.value}>
{#if day}
{@render day({
day: date,
outsideMonth: !isEqualMonth(date, month.value)
})}
{:else}
<Calendar.Day />
{/if}
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar.Month>
{/each}
</Calendar.Months>
{/snippet}

View File

@@ -11,6 +11,11 @@ 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';
import MonthSelect from './calendar-month-select.svelte';
import YearSelect from './calendar-year-select.svelte';
import Month from './calendar-month.svelte';
import Nav from './calendar-nav.svelte';
import Caption from './calendar-caption.svelte';
export {
Day,
@@ -25,6 +30,11 @@ export {
HeadCell,
NextButton,
PrevButton,
Nav,
Month,
YearSelect,
MonthSelect,
Caption,
//
Root as Calendar
};

View File

@@ -13,7 +13,10 @@
<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)}
class={cn(
'cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className
)}
{...restProps}
>
{@render children?.()}

View File

@@ -10,6 +10,11 @@
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn('px-6', className)} {...restProps}>
<div
bind:this={ref}
data-slot="card-content"
class={cn('px-6 group-data-[size=sm]/card:px-4', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -13,7 +13,7 @@
<p
bind:this={ref}
data-slot="card-description"
class={cn('text-muted-foreground mt-1 text-sm', className)}
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
>
{@render children?.()}

View File

@@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="card-footer"
class={cn('flex items-center px-6 [.border-t]:pt-6', className)}
class={cn(
'rounded-b-4xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',
className
)}
{...restProps}
>
{@render children?.()}

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="card-header"
class={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
'gap-1.5 rounded-t-4xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
className
)}
{...restProps}

View File

@@ -6,15 +6,17 @@
ref = $bindable(null),
class: className,
children,
size = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props();
</script>
<div
bind:this={ref}
data-slot="card"
data-size={size}
class={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-4xl border py-6 shadow-sm',
'bg-card text-card-foreground border-foreground/5 dark:border-foreground/10 gap-6 overflow-hidden rounded-4xl py-6 text-sm shadow-sm border-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl group/card flex flex-col',
className
)}
{...restProps}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
let {
ref = $bindable(null),
@@ -17,7 +17,7 @@
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 peer size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
'bg-input/90 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[5px] border border-transparent transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:checked
@@ -27,12 +27,12 @@
{#snippet children({ checked, indeterminate })}
<div
data-slot="checkbox-indicator"
class="flex items-center justify-center text-current transition-none"
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
{#if checked}
<CheckIcon class="size-3.5" />
<CheckIcon />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
<MinusIcon />
{/if}
</div>
{/snippet}

View File

@@ -3,16 +3,18 @@
import type { Snippet } from 'svelte';
import Command from './command.svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import type { WithoutChildrenOrChild } from '$lib/utils/style.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(''),
title = 'Command Palette',
description = 'Search for a command to run',
description = 'Search for a command to run...',
showCloseButton = false,
portalProps,
children,
class: className,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
@@ -20,6 +22,8 @@
children: Snippet;
title?: string;
description?: string;
showCloseButton?: boolean;
class?: string;
} = $props();
</script>
@@ -28,13 +32,11 @@
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
<Command
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]]:px-2 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
<Dialog.Content
class={cn('rounded-4xl! p-0 top-1/3 translate-y-0 overflow-hidden p-0', className)}
{showCloseButton}
{portalProps}
>
<Command {...restProps} bind:value bind:ref {children} />
</Dialog.Content>
</Dialog.Root>

View File

@@ -17,7 +17,10 @@
<CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn('text-foreground overflow-hidden p-1', className)}
class={cn(
'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden py-2 px-1 **:[[cmdk-group-heading]]:px-3 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className
)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import SearchIcon from '@lucide/svelte/icons/search';
import { cn } from '$lib/utils/style.js';
import * as InputGroup from '$lib/components/ui/input-group/index.js';
import SearchIcon from '@lucide/svelte/icons/search';
let {
ref = $bindable(null),
@@ -11,16 +12,23 @@
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
<SearchIcon class="size-4 shrink-0 opacity-50" />
<div data-slot="command-input-wrapper" class="p-1 pb-0">
<InputGroup.Root class="bg-input/50 h-9">
<CommandPrimitive.Input
{value}
data-slot="command-input"
class={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:ref
{...restProps}
bind:value
/>
>
{#snippet child({ props })}
<InputGroup.Input {...props} bind:value bind:ref />
{/snippet}
</CommandPrimitive.Input>
<InputGroup.Addon>
<SearchIcon class="size-4 shrink-0 opacity-50" />
</InputGroup.Addon>
</InputGroup.Root>
</div>

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
import CheckIcon from '@lucide/svelte/icons/check';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
@@ -13,8 +15,13 @@
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/command-item data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-full px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>
>
{@render children?.()}
<CheckIcon
class="cn-command-item-indicator ml-auto opacity-0 group-has-[[data-slot=command-shortcut]]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</CommandPrimitive.Item>

View File

@@ -12,6 +12,9 @@
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', className)}
class={cn(
'no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto',
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
</script>
<CommandPrimitive.Loading bind:ref {...restProps} />

View File

@@ -12,6 +12,6 @@
<CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn('bg-border -mx-1 h-px', className)}
class={cn('bg-border/50 my-1.5 h-px', className)}
{...restProps}
/>

View File

@@ -13,7 +13,10 @@
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
class={cn(
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps}
>
{@render children?.()}

View File

@@ -1,21 +1,27 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/style.js';
import { Command as CommandPrimitive } from 'bits-ui';
export type CommandRootApi = CommandPrimitive.Root;
let {
api = $bindable(null),
ref = $bindable(null),
value = $bindable(''),
class: className,
...restProps
}: CommandPrimitive.RootProps = $props();
}: CommandPrimitive.RootProps & {
api?: CommandRootApi | null;
} = $props();
</script>
<CommandPrimitive.Root
bind:this={api}
bind:value
bind:ref
data-slot="command"
class={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
'bg-popover text-popover-foreground rounded-4xl p-1 flex size-full flex-col overflow-hidden',
className
)}
{...restProps}

View File

@@ -1,6 +1,5 @@
import { Command as CommandPrimitive } from 'bits-ui';
import Root from './command.svelte';
import Loading from './command-loading.svelte';
import Dialog from './command-dialog.svelte';
import Empty from './command-empty.svelte';
import Group from './command-group.svelte';
@@ -11,8 +10,6 @@ import Separator from './command-separator.svelte';
import Shortcut from './command-shortcut.svelte';
import LinkItem from './command-link-item.svelte';
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,

View File

@@ -1,7 +1,11 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
let {
ref = $bindable(null),
type = 'button',
...restProps
}: DialogPrimitive.CloseProps = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />

View File

@@ -1,39 +1,53 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import XIcon from '@lucide/svelte/icons/x';
import DialogPortal from './dialog-portal.svelte';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import type { ComponentProps } from 'svelte';
import { Button } from '$lib/components/ui/button/index.js';
import XIcon from '@lucide/svelte/icons/x';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<DialogPortal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
'bg-popover 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 top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-4xl border p-6 shadow-lg duration-200 sm:max-w-lg',
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 dark:ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="ring-offset-popover focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
{#if showCloseButton}
<DialogPrimitive.Close data-slot="dialog-close">
{#snippet child({ props })}
<Button
variant="ghost"
class="bg-secondary absolute top-4 right-4"
size="icon-sm"
{...props}
>
<XIcon />
<span class="sr-only">Close</span>
</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
</DialogPortal>

View File

@@ -12,6 +12,9 @@
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn('text-muted-foreground text-sm', className)}
class={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
className
)}
{...restProps}
/>

View File

@@ -1,20 +1,32 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Button } from '$lib/components/ui/button/index.js';
let {
ref = $bindable(null),
class: className,
children,
showCloseButton = false,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
showCloseButton?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close>
{#snippet child({ props })}
<Button variant="outline" {...props}>Close</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</div>

View File

@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="dialog-header"
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
class={cn('gap-1.5 flex flex-col', className)}
{...restProps}
>
{@render children?.()}

View File

@@ -13,7 +13,7 @@
bind:ref
data-slot="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',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 duration-100 supports-backdrop-filter:backdrop-blur-sm fixed inset-0 isolate z-50',
className
)}
{...restProps}

View File

@@ -1,14 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
interface Props {
children?: import('svelte').Snippet;
[key: string]: any;
}
let { children, ...rest }: Props = $props();
type $$Props = DialogPrimitive.PortalProps;
let { ...restProps }: DialogPrimitive.PortalProps = $props();
</script>
<DialogPrimitive.Portal {...rest}>
{@render children?.()}
</DialogPrimitive.Portal>
<DialogPrimitive.Portal {...restProps} />

View File

@@ -12,6 +12,6 @@
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn('text-lg leading-none font-semibold', className)}
class={cn('text-base leading-none font-medium', className)}
{...restProps}
/>

View File

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

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
</script>
<DialogPrimitive.Root bind:open {...restProps} />

View File

@@ -1,5 +1,5 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
import Root from './dialog.svelte';
import Portal from './dialog-portal.svelte';
import Title from './dialog-title.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
@@ -9,9 +9,6 @@ import Description from './dialog-description.svelte';
import Trigger from './dialog-trigger.svelte';
import Close from './dialog-close.svelte';
const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,

View File

@@ -1,34 +0,0 @@
<script lang="ts" module>
import { cn } from '$lib/utils/style.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
export type DropdownButtonContentProps = DropdownMenuPrimitive.ContentProps;
</script>
<script lang="ts">
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
children,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{sideOffset}
class={cn(
'bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md outline-none',
className
)}
{...restProps}
>
<DropdownMenuPrimitive.Arrow />
{@render children?.()}
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>

View File

@@ -1,19 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils/style.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.ItemProps = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
'data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
className
)}
{...restProps}
/>

View File

@@ -1,38 +0,0 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLButtonAttributes } from 'svelte/elements';
import {
buttonVariants,
type ButtonVariant,
type ButtonSize
} from '$lib/components/ui/button/button.svelte';
export type DropdownButtonMainProps = WithElementRef<HTMLButtonAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
type = 'button',
disabled,
children,
...restProps
}: DropdownButtonMainProps = $props();
</script>
<button
bind:this={ref}
data-slot="dropdown-button-main"
class={cn(buttonVariants({ variant, size }), 'rounded-r-none border-r-0', className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>

View File

@@ -1,21 +0,0 @@
<script lang="ts" module>
import { cn } from '$lib/utils/style.js';
export type DropdownButtonSeparatorProps = DropdownMenuPrimitive.SeparatorProps;
</script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
class={cn('bg-muted -mx-1 my-1 h-px', className)}
{...restProps}
/>

View File

@@ -1,51 +0,0 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils/style.js';
import type { HTMLButtonAttributes } from 'svelte/elements';
import {
buttonVariants,
type ButtonVariant,
type ButtonSize
} from '$lib/components/ui/button/button.svelte';
export type DropdownButtonTriggerProps = WithElementRef<HTMLButtonAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
builders?: any[];
};
</script>
<script lang="ts">
import ChevronDown from '@lucide/svelte/icons/chevron-down';
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
type = 'button',
disabled,
builders = [],
children,
...restProps
}: DropdownButtonTriggerProps = $props();
</script>
<button
bind:this={ref}
use:builders[0]
data-slot="dropdown-button-trigger"
class={cn(
buttonVariants({ variant, size }),
'border-l-background/20 rounded-l-none border-l px-2',
className
)}
{type}
{disabled}
{...restProps}
>
{#if children}
{@render children()}
{:else}
<ChevronDown class="size-4" />
{/if}
</button>

View File

@@ -1,30 +0,0 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import Root from './dropdown-button.svelte';
import Main from './dropdown-button-main.svelte';
import Trigger from './dropdown-button-trigger.svelte';
import Content from './dropdown-button-content.svelte';
import Item from './dropdown-button-item.svelte';
import Separator from './dropdown-button-separator.svelte';
const DropdownRoot = DropdownMenuPrimitive.Root;
const DropdownTrigger = DropdownMenuPrimitive.Trigger;
export {
Root,
Main,
Trigger,
Content,
Item,
Separator,
DropdownRoot,
DropdownTrigger,
//
Root as DropdownButton,
Main as DropdownButtonMain,
Trigger as DropdownButtonTrigger,
Content as DropdownButtonContent,
Item as DropdownButtonItem,
Separator as DropdownButtonSeparator,
DropdownRoot as DropdownButtonRoot,
DropdownTrigger as DropdownButtonPrimitiveTrigger
};

View File

@@ -23,7 +23,7 @@
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1.5 pr-9 pl-2 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
let {
ref = $bindable(null),
@@ -23,7 +23,7 @@
{sideOffset}
{align}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground min-w-48 rounded-3xl p-1.5 shadow-lg ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!',
className
)}
{...restProps}

View File

@@ -20,7 +20,7 @@
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-2 py-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2.5 rounded-2xl px-3 py-1.5 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}

View File

@@ -18,7 +18,7 @@
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8',
'text-muted-foreground px-3 py-2.5 text-xs data-inset:pl-9.5 data-[inset]:pl-8',
className
)}
{...restProps}

View File

@@ -15,14 +15,14 @@
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2.5 rounded-2xl py-2 pr-8 pl-3 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span
class="absolute right-2 flex items-center justify-center"
class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-radio-item-indicator"
>
{#if checked}

View File

@@ -12,6 +12,6 @@
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn('bg-border -mx-1 my-1 h-px', className)}
class={cn('bg-border/50 -mx-1.5 my-1.5 h-px', className)}
{...restProps}
/>

View File

@@ -13,7 +13,7 @@
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto',
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground min-w-36 rounded-3xl p-1.5 shadow-lg ring-1 duration-100 w-auto animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!',
className
)}
{...restProps}

View File

@@ -19,7 +19,7 @@
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-2xl px-3 py-1.5 text-sm font-medium data-inset:pl-9.5 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}

View File

@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="field-content"
class={cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', className)}
class={cn('gap-1 group/field-content flex flex-1 flex-col leading-snug', className)}
{...restProps}
>
{@render children?.()}

View File

@@ -14,7 +14,8 @@
bind:this={ref}
data-slot="field-description"
class={cn(
'text-muted-foreground -mt-1 mb-0 text-xs leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
)}

View File

@@ -19,7 +19,7 @@
if (children) return true;
// no errors
if (!errors) return false;
if (!errors || errors.length === 0) return false;
// has an error but no message
if (errors.length === 1 && !errors[0]?.message) {
@@ -46,7 +46,7 @@
{:else if singleErrorMessage}
{singleErrorMessage}
{:else if isMultipleErrors}
<ul class="ms-4 flex list-disc flex-col gap-1">
<ul class="ml-4 flex list-disc flex-col gap-1">
{#each errors ?? [] as error, index (index)}
{#if error?.message}
<li>{error.message}</li>

View File

@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="field-group"
class={cn(
'group/field-group @container/field-group flex w-full flex-col gap-4 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
'gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col',
className
)}
{...restProps}

View File

@@ -17,11 +17,10 @@
<Label
bind:ref
data-slot="field-label"
{required}
class={cn(
'group/field-label peer/field-label mb-0 flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
'has-data-checked:bg-input/30 gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-2xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-4 group/field-label peer/field-label flex w-fit leading-snug',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
required && "after:text-red-500 after:content-['*']",
className
)}
{...restProps}

View File

@@ -18,9 +18,7 @@
data-slot="field-legend"
data-variant={variant}
class={cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
'mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
className
)}
{...restProps}

View File

@@ -20,13 +20,13 @@
bind:this={ref}
data-slot="field-separator"
data-content={hasContent}
class={cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', className)}
class={cn('-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative', className)}
{...restProps}
>
<Separator class="absolute inset-0 top-1/2" />
{#if children}
<span
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
class="text-muted-foreground px-2 bg-background relative mx-auto block w-fit"
data-slot="field-separator-content"
>
{@render children()}

View File

@@ -14,8 +14,7 @@
bind:this={ref}
data-slot="field-set"
class={cn(
'flex flex-col gap-4',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
'gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col',
className
)}
{...restProps}

View File

@@ -12,9 +12,9 @@
<div
bind:this={ref}
data-slot="field-title"
data-slot="field-label"
class={cn(
'flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50',
'gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug',
className
)}
{...restProps}

View File

@@ -2,20 +2,14 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const fieldVariants = tv({
base: 'group/field data-[invalid=true]:text-destructive flex w-full gap-1.5',
base: 'data-[invalid=true]:text-destructive gap-3 group/field flex w-full',
variants: {
orientation: {
vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start'
],
responsive: [
'@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
]
vertical: 'cn-field-orientation-vertical flex-col [&>*]:w-full [&>.sr-only]:w-auto',
horizontal:
'cn-field-orientation-horizontal flex-row items-center has-[>[data-slot=field-content]]:items-start [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
responsive:
'cn-field-orientation-responsive flex-col @md/field-group:flex-row @md/field-group:items-center @md/field-group:has-[>[data-slot=field-content]]:items-start [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
}
},
defaultVariants: {

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button/index.js';
import { Button, type ButtonProps } from '$lib/components/ui/button/index.js';
let { ref = $bindable(null), ...restProps }: Button.Props = $props();
let { ref = $bindable(null), ...restProps }: ButtonProps = $props();
</script>
<Button.Root bind:ref type="submit" {...restProps} />
<Button bind:ref type="submit" {...restProps} />

View File

@@ -1,11 +1,6 @@
<script lang="ts" module>
import type { FormPathLeaves as _FormPathLeaves } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = _FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPathLeaves<T>">
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import * as FormPrimitive from 'formsnap';
import type { FormPathLeaves } from 'sveltekit-superforms';
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils/style.js';

View File

@@ -1,11 +1,6 @@
<script lang="ts" module>
import type { FormPath as _FormPath } from 'sveltekit-superforms';
type T = Record<string, unknown>;
type U = _FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends _FormPath<T>">
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from 'formsnap';
import type { FormPath } from 'sveltekit-superforms';
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils/style.js';
import type { HTMLAttributes } from 'svelte/elements';

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