This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / Button.svelte
8.0 kB 203 lines
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 items-center justify-center overflow-hidden rounded-l-(--btn-radius-l) rounded-r-(--btn-radius-r) no-underline select-none [--btn-radius-l:var(--btn-radius)] [--btn-radius-r:var(--btn-radius)] [--btn-radius:var(--radius-sm)]", 7 "transition-colors duration-150 ease-in-out", 8 "hover:no-underline", 9 "focus-visible:outline-2 focus-visible:outline-offset-2", 10 // each variant paints its own disabled surface (see the variant blocks) — 11 // Figma models disabled as a distinct fill/text pair, not a dimmed enabled state 12 "disabled:pointer-events-none aria-disabled:pointer-events-none" 13 ], 14 variants: { 15 variant: { 16 default: [ 17 "border border-border-default bg-background-default text-foreground-default", 18 "hover:bg-background-subtle", 19 "focus-visible:outline-border-focus", 20 "disabled:border-transparent disabled:bg-background-subtle disabled:text-foreground-subtle", 21 "aria-disabled:border-transparent aria-disabled:bg-background-subtle aria-disabled:text-foreground-subtle" 22 ], 23 primary: [ 24 "border border-border-success bg-background-success-emphasis text-foreground-on-emphasis", 25 "hover:bg-background-success-emphasis-hover", 26 "focus-visible:outline-background-success-emphasis", 27 "disabled:border-transparent disabled:bg-background-success-subtle disabled:text-foreground-success-disabled", 28 "aria-disabled:border-transparent aria-disabled:bg-background-success-subtle aria-disabled:text-foreground-success-disabled" 29 ], 30 ghost: [ 31 "border border-transparent bg-transparent", 32 "hover:bg-background-inset", 33 "focus-visible:outline-border-focus", 34 "disabled:text-foreground-disabled aria-disabled:text-foreground-disabled" 35 ], 36 // danger / success / warning are outline buttons in Figma: default surface + 37 // border, colored text only (bound to the fg-* tokens), no accent fill. They 38 // keep that surface when disabled and only fade the label — the outline is 39 // what carries the variant, so dropping it would read as a different button. 40 danger: [ 41 "border border-border-default bg-background-default text-foreground-danger", 42 "hover:bg-background-subtle", 43 "focus-visible:outline-foreground-danger", 44 "disabled:text-foreground-danger-disabled aria-disabled:text-foreground-danger-disabled" 45 ], 46 success: [ 47 "border border-border-default bg-background-default text-foreground-success", 48 "hover:bg-background-subtle", 49 "focus-visible:outline-foreground-success", 50 "disabled:text-foreground-success-disabled aria-disabled:text-foreground-success-disabled" 51 ], 52 warning: [ 53 "border border-border-default bg-background-default text-foreground-warning", 54 "hover:bg-background-subtle", 55 "focus-visible:outline-foreground-warning", 56 "disabled:text-foreground-warning-disabled aria-disabled:text-foreground-warning-disabled" 57 ] 58 }, 59 // the min-h is the button's real height (32/36/40 in Figma), so py has to stay 60 // small enough that line-height + padding + border never exceeds it — otherwise a 61 // button with a label grows past the min-h while an icon-only one stays on it, and 62 // the two render at different heights side by side. py only takes over as the 63 // floor when a label wraps to a second line. 64 size: { 65 sm: "min-h-8 gap-1.5 px-2 py-1 typography-paragraph-small", 66 md: "min-h-9 gap-2 px-3 py-1 typography-paragraph-regular", 67 lg: "min-h-10 gap-2.5 px-4 py-1 typography-paragraph-large" 68 }, 69 insetShadow: { 70 true: "", 71 false: "" 72 } 73 }, 74 compoundVariants: [ 75 // raised variants draw inset depth via a ::before layer. pressing shifts the 76 // content wrapper rather than the button itself, so the box stays put. 77 { 78 variant: "default", 79 insetShadow: true, 80 class: [ 81 "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:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)]", 82 "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]", 83 "active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]", 84 "disabled:before:shadow-none disabled:active:*:translate-y-0" 85 ] 86 }, 87 { 88 variant: "primary", 89 insetShadow: true, 90 class: [ 91 "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:shadow-[inset_0_-2px_0_0_var(--color-alpha-dark-25)] before:transition-all before:duration-150 before:content-[''] dark:before:shadow-[inset_0_-2px_0_0_var(--color-alpha-light-25)]", 92 "active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-dark-50)] dark:active:before:shadow-[inset_0_2px_2px_0_var(--color-alpha-light-50)]", 93 "disabled:before:shadow-none disabled:active:*:translate-y-0" 94 ] 95 } 96 ], 97 defaultVariants: { 98 variant: "default", 99 size: "md", 100 insetShadow: false 101 } 102 }); 103 104 export type ButtonVariants = VariantProps<typeof button>; 105</script> 106 107<script lang="ts"> 108 import type { ResolvedPathname } from "$app/types"; 109 import type { Component, Snippet } from "svelte"; 110 import type { 111 HTMLAnchorAttributes, 112 HTMLButtonAttributes, 113 SvelteHTMLElements 114 } from "svelte/elements"; 115 import Spinner from "./Spinner.svelte"; 116 117 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> { 118 variant?: ButtonVariants["variant"]; 119 size?: ButtonVariants["size"]; 120 insetShadow?: boolean; 121 href?: ResolvedPathname; 122 type?: "button" | "submit" | "reset"; 123 disabled?: boolean; 124 icon?: Component<SvelteHTMLElements["svg"]>; 125 iconSide?: string; 126 class?: string; 127 loading?: boolean; 128 spinnerClass?: string; 129 ref?: HTMLAnchorElement | HTMLButtonElement; 130 children?: Snippet; 131 } 132 133 let { 134 variant = "default", 135 size = "md", 136 insetShadow = false, 137 href, 138 type = "button", 139 disabled = false, 140 icon, 141 iconSide = "left", 142 class: className, 143 loading = false, 144 spinnerClass, 145 ref = $bindable(), 146 children, 147 ...rest 148 }: Props = $props(); 149 150 const classes = $derived(button({ variant, size, insetShadow, class: className })); 151 const spinnerSize = $derived(spinnerClass ?? (size === "lg" ? "size-5" : "size-4")); 152 const iconSize = $derived(size === "lg" ? "size-5" : "size-4"); 153 const Icon = $derived(icon); 154</script> 155 156{#snippet content()} 157 <!-- single wrapper so the press animation moves the label/icon, not the button box. 158 gap-[inherit] keeps the size variant's gap working across the extra level, and the 159 transition matches the ::before shadow's so both halves of the press ease together. --> 160 <span 161 class="inline-flex min-w-0 items-center justify-center gap-[inherit] transition-transform duration-150 ease-in-out" 162 > 163 {#if loading} 164 <Spinner class={spinnerSize} /> 165 {:else} 166 {#if Icon && iconSide == "left"} 167 <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 168 {/if} 169 {#if children} 170 {@render children()} 171 {/if} 172 {#if Icon && iconSide == "right"} 173 <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 174 {/if} 175 {/if} 176 </span> 177{/snippet} 178 179{#if href} 180 <a 181 bind:this={ref} 182 {href} 183 class={classes} 184 data-variant={variant} 185 aria-disabled={disabled || loading ? "true" : undefined} 186 tabindex={disabled ? -1 : undefined} 187 {...rest} 188 > 189 {@render content()} 190 </a> 191{:else} 192 <button 193 bind:this={ref} 194 {type} 195 disabled={disabled || loading} 196 class={classes} 197 data-variant={variant} 198 aria-busy={loading} 199 {...rest} 200 > 201 {@render content()} 202 </button> 203{/if}