Files
pocket-id-pocket-id/frontend/src/lib/components/ui/button/button.svelte
2025-11-04 13:40:00 +01:00

120 lines
3.7 KiB
Svelte

<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';
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
outline:
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
autofocus?: boolean;
};
</script>
<script lang="ts">
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
import { onMount } from 'svelte';
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
disabled,
isLoading = false,
autofocus = false,
onclick,
usePromiseLoading = false,
children,
...restProps
}: ButtonProps & {
usePromiseLoading?: boolean;
} = $props();
onMount(async () => {
// Using autofocus can be bad for a11y, but in the case of Pocket ID is only used responsibly in places where there are not many choices, and on buttons only where there's descriptive text
if (autofocus) {
// Use setTimeout to make sure the element is showing
setTimeout(() => ref?.focus(), 100);
}
});
async function handleOnClick(event: any) {
if (usePromiseLoading && onclick) {
isLoading = true;
try {
await onclick(event);
} finally {
isLoading = false;
}
} else {
onclick?.(event);
}
}
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{#if isLoading}
<LoaderCircle class="size-4 animate-spin" />
{/if}
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
disabled={disabled || isLoading}
onclick={handleOnClick}
{...restProps}
>
{#if isLoading}
<LoaderCircle class="size-4 animate-spin" />
{/if}
{@render children?.()}
</button>
{/if}