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-l-(--btn-radius-l) rounded-r-(--btn-radius-r) [--btn-radius:var(--radius-sm)] [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] no-underline transition-colors duration-150 ease-in-out \
6 hover:no-underline \
7 focus-visible:outline-2 focus-visible:outline-offset-2 \
8 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
9 variants: {
10 variant: {
11 default:
12 "border border-border-default bg-background-default text-foreground-default \
13 hover:bg-background-subtle focus-visible:outline-border-focus",
14 primary:
15 "border border-border-success bg-background-success-emphasis text-foreground-on-emphasis \
16 hover:bg-background-success-emphasis-hover focus-visible:outline-background-success-emphasis",
17 ghost:
18 "bg-transparent text-foreground-muted \
19 hover:bg-background-inset focus-visible:outline-border-focus",
20 // danger / success / warning are outline buttons in Figma: default surface +
21 // border, colored text only (bound to the fg-* tokens), no accent fill.
22 danger:
23 "border border-border-default bg-background-default text-foreground-danger \
24 hover:bg-background-subtle focus-visible:outline-foreground-danger",
25 success:
26 "border border-border-default bg-background-default text-foreground-success \
27 hover:bg-background-subtle focus-visible:outline-foreground-success",
28 warning:
29 "border border-border-default bg-background-default text-foreground-warning \
30 hover:bg-background-subtle focus-visible:outline-foreground-warning"
31 },
32 size: {
33 sm: "min-h-8 gap-1.5 px-2 py-1.5 text-sm",
34 md: "min-h-9 gap-2 px-3 py-1.5 text-sm",
35 lg: "min-h-10 gap-2.5 px-4 py-2 text-base"
36 }
37 },
38 compoundVariants: [
39 // raised variants draw inset depth via a ::before layer.
40 {
41 variant: "default",
42 class:
43 "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] 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)] \
44 active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)] \
45 disabled:active:translate-y-0"
46 },
47 {
48 variant: "primary",
49 class:
50 "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-l-[max(calc(var(--btn-radius-l)-2px),0px)] before:rounded-r-[max(calc(var(--btn-radius-r)-2px),0px)] before:content-[''] before:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150 \
51 active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-dark-50)] \
52 disabled:active:translate-y-0"
53 }
54 ],
55 defaultVariants: {
56 variant: "default",
57 size: "md"
58 }
59 });
60
61 export type ButtonVariants = VariantProps<typeof button>;
62</script>
63
64<script lang="ts">
65 import type { ResolvedPathname } from "$app/types";
66 import type { Component, Snippet } from "svelte";
67 import type {
68 HTMLAnchorAttributes,
69 HTMLButtonAttributes,
70 SvelteHTMLElements
71 } from "svelte/elements";
72 import Spinner from "./Spinner.svelte";
73
74 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> {
75 variant?: ButtonVariants["variant"];
76 size?: ButtonVariants["size"];
77 href?: ResolvedPathname;
78 type?: "button" | "submit" | "reset";
79 disabled?: boolean;
80 icon?: Component<SvelteHTMLElements["svg"]>;
81 class?: string;
82 loading?: boolean;
83 spinnerClass?: string;
84 children?: Snippet;
85 }
86
87 let {
88 variant = "default",
89 size = "md",
90 href,
91 type = "button",
92 disabled = false,
93 icon,
94 class: className,
95 loading = false,
96 spinnerClass,
97 children,
98 ...rest
99 }: Props = $props();
100
101 const classes = $derived(button({ variant, size, class: className }));
102 const spinnerSize = $derived(spinnerClass ?? (size === "lg" ? "size-5" : "size-4"));
103 const iconSize = $derived(size === "lg" ? "size-5" : "size-4");
104 const Icon = $derived(icon);
105</script>
106
107{#snippet content()}
108 {#if loading}
109 <Spinner class={spinnerSize} />
110 {:else}
111 {#if Icon}
112 <Icon class="{iconSize} shrink-0" aria-hidden="true" />
113 {/if}
114 {#if children}
115 {@render children()}
116 {/if}
117 {/if}
118{/snippet}
119
120{#if href}
121 <a
122 {href}
123 class={classes}
124 aria-disabled={disabled || loading ? "true" : undefined}
125 tabindex={disabled ? -1 : undefined}
126 {...rest}
127 >
128 {@render content()}
129 </a>
130{:else}
131 <button {type} disabled={disabled || loading} class={classes} aria-busy={loading} {...rest}>
132 {@render content()}
133 </button>
134{/if}