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 *));
@@ -7,7 +8,7 @@
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@@ -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,127 +1,115 @@
<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.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 class="select-none" {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
<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="select-none">
{#each weekdays as weekday, i (i)}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
</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}>
{#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" />
<CommandPrimitive.Input
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',
className
)}
bind:ref
{...restProps}
bind:value
/>
<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(
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...restProps}
>
{#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"
>
<XIcon />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
{#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