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