This repository has no description
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 export const button = tv({
5 base: "inline-flex cursor-pointer select-none items-center justify-center overflow-hidden rounded no-underline transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
6 variants: {
7 variant: {
8 default:
9 "border border-border-default bg-background-default text-foreground-default hover:bg-background-subtle focus-visible:outline-border-focus",
10 primary:
11 "border border-border-success bg-background-success-emphasis text-foreground-on-emphasis hover:bg-background-success-emphasis-hover focus-visible:outline-background-success-emphasis",
12 ghost:
13 "bg-transparent text-foreground-muted hover:bg-background-inset focus-visible:outline-border-focus",
14 flat: "bg-gray-50 text-foreground-default hover:bg-gray-100 focus-visible:outline-border-focus dark:bg-gray-900 dark:hover:bg-gray-700",
15 // danger / success / warning are outline buttons in Figma: default surface +
16 // border, colored text only (bound to the fg-* tokens), no accent fill.
17 danger:
18 "border border-border-default bg-background-default text-foreground-danger hover:bg-background-subtle focus-visible:outline-foreground-danger",
19 success:
20 "border border-border-default bg-background-default text-foreground-success hover:bg-background-subtle focus-visible:outline-foreground-success",
21 warning:
22 "border border-border-default bg-background-default text-foreground-warning hover:bg-background-subtle focus-visible:outline-foreground-warning"
23 },
24 size: {
25 sm: "min-h-8 gap-1.5 px-2 py-1.5 text-sm",
26 md: "min-h-9 gap-2 px-3 py-1.5 text-sm",
27 lg: "min-h-10 gap-2.5 px-6 py-2 text-base"
28 }
29 },
30 compoundVariants: [
31 // raised variants draw inset depth via a ::before layer.
32 {
33 variant: "default",
34 class:
35 "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] before:content-[''] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)] active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)] disabled:active:translate-y-0"
36 },
37 {
38 variant: "primary",
39 class:
40 "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] before:content-[''] before:shadow-[inset_0_-2px_0_0_rgb(3_7_18/0.25)] before:transition-all before:duration-150 active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_rgb(3_7_18/0.5)] disabled:active:translate-y-0"
41 }
42 ],
43 defaultVariants: {
44 variant: "default",
45 size: "md"
46 }
47 });
48
49 export type ButtonVariants = VariantProps<typeof button>;
50</script>
51
52<script lang="ts">
53 import { resolve } from "$app/paths";
54 import type { Snippet } from "svelte";
55 import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
56 import Spinner from "./Spinner.svelte";
57
58 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> {
59 variant?: ButtonVariants["variant"];
60 size?: ButtonVariants["size"];
61 href?: string;
62 type?: "button" | "submit" | "reset";
63 disabled?: boolean;
64 class?: string;
65 loading?: boolean;
66 spinnerClass?: string;
67 children?: Snippet;
68 }
69
70 let {
71 variant = "default",
72 size = "md",
73 href,
74 type = "button",
75 disabled = false,
76 class: className,
77 loading = false,
78 spinnerClass,
79 children,
80 ...rest
81 }: Props = $props();
82
83 const classes = $derived(button({ variant, size, class: className }));
84 const spinnerSize = $derived(spinnerClass ?? (size === "lg" ? "size-5" : "size-4"));
85</script>
86
87{#if href}
88 <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- internal hrefs go through resolve(), external ones must pass through untouched -->
89 <a
90 href={href?.startsWith("/") ? resolve(href as "/") : href}
91 class={classes}
92 aria-disabled={disabled || loading ? "true" : undefined}
93 tabindex={disabled ? -1 : undefined}
94 {...rest}
95 >
96 {#if loading}
97 <Spinner class={spinnerSize} />
98 {:else if children}
99 {@render children()}
100 {/if}
101 </a>
102{:else}
103 <button {type} disabled={disabled || loading} class={classes} aria-busy={loading} {...rest}>
104 {#if loading}
105 <Spinner class={spinnerSize} />
106 {:else if children}
107 {@render children()}
108 {/if}
109 </button>
110{/if}