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
+5 -2
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"
}
+1
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",
+21 -2
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 {
@@ -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'
: ''}"
@@ -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">
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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>
@@ -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?.()}
@@ -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?.()}
@@ -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}
@@ -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
};
@@ -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>
@@ -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}
/>
@@ -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>
@@ -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>
@@ -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}
/>
@@ -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}
/>
+10 -1
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
};
@@ -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',
@@ -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}
/>
@@ -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}
@@ -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>
@@ -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
};
@@ -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}
/>
@@ -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}
@@ -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}
@@ -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}
@@ -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}
/>
@@ -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}
/>
@@ -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}
/>
@@ -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}
/>
@@ -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>
@@ -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>
@@ -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?.()}
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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}
@@ -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
};
@@ -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?.()}
@@ -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>
@@ -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?.()}
@@ -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?.()}
@@ -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}
@@ -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}
@@ -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}
@@ -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>
@@ -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}
>
@@ -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>
@@ -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>
@@ -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}
/>
@@ -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} />
@@ -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}
/>
@@ -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?.()}
@@ -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}
@@ -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,
@@ -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} />
@@ -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>
@@ -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}
/>
@@ -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>
@@ -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?.()}
@@ -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}
@@ -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} />
@@ -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}
/>
@@ -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} />
@@ -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} />
@@ -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,
@@ -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>
@@ -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}
/>
@@ -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>
@@ -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}
/>
@@ -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>
@@ -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
};
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
@@ -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}
/>
@@ -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}
@@ -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}
@@ -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?.()}
@@ -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
)}
@@ -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>
@@ -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}
@@ -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}
@@ -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}
@@ -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()}
@@ -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}
@@ -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}
@@ -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: {
@@ -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} />
@@ -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';
@@ -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