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
7.4 kB 186 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 size: { 60 sm: "min-h-8 gap-1.5 px-2 py-1.5 typography-paragraph-small", 61 md: "min-h-9 gap-2 px-3 py-1.5 typography-paragraph-regular", 62 lg: "min-h-10 gap-2.5 px-4 py-2 typography-paragraph-large" 63 }, 64 insetShadow: { 65 true: "", 66 false: "" 67 } 68 }, 69 compoundVariants: [ 70 // raised variants draw inset depth via a ::before layer. pressing shifts the 71 // content wrapper rather than the button itself, so the box stays put. 72 { 73 variant: "default", 74 insetShadow: true, 75 class: [ 76 "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)]", 77 "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]", 78 "active:*:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]", 79 "disabled:before:shadow-none disabled:active:*:translate-y-0" 80 ] 81 }, 82 { 83 variant: "primary", 84 insetShadow: true, 85 class: [ 86 "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)]", 87 "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)]", 88 "disabled:before:shadow-none disabled:active:*:translate-y-0" 89 ] 90 } 91 ], 92 defaultVariants: { 93 variant: "default", 94 size: "md", 95 insetShadow: false 96 } 97 }); 98 99 export type ButtonVariants = VariantProps<typeof button>; 100</script> 101 102<script lang="ts"> 103 import type { ResolvedPathname } from "$app/types"; 104 import type { Component, Snippet } from "svelte"; 105 import type { 106 HTMLAnchorAttributes, 107 HTMLButtonAttributes, 108 SvelteHTMLElements 109 } from "svelte/elements"; 110 import Spinner from "./Spinner.svelte"; 111 112 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> { 113 variant?: ButtonVariants["variant"]; 114 size?: ButtonVariants["size"]; 115 insetShadow?: boolean; 116 href?: ResolvedPathname; 117 type?: "button" | "submit" | "reset"; 118 disabled?: boolean; 119 icon?: Component<SvelteHTMLElements["svg"]>; 120 iconSide?: string; 121 class?: string; 122 loading?: boolean; 123 spinnerClass?: string; 124 children?: Snippet; 125 } 126 127 let { 128 variant = "default", 129 size = "md", 130 insetShadow = false, 131 href, 132 type = "button", 133 disabled = false, 134 icon, 135 iconSide = "left", 136 class: className, 137 loading = false, 138 spinnerClass, 139 children, 140 ...rest 141 }: Props = $props(); 142 143 const classes = $derived(button({ variant, size, insetShadow, class: className })); 144 const spinnerSize = $derived(spinnerClass ?? (size === "lg" ? "size-5" : "size-4")); 145 const iconSize = $derived(size === "lg" ? "size-5" : "size-4"); 146 const Icon = $derived(icon); 147</script> 148 149{#snippet content()} 150 <!-- single wrapper so the press animation moves the label/icon, not the button box. 151 gap-[inherit] keeps the size variant's gap working across the extra level, and the 152 transition matches the ::before shadow's so both halves of the press ease together. --> 153 <span 154 class="inline-flex min-w-0 items-center justify-center gap-[inherit] transition-transform duration-150 ease-in-out" 155 > 156 {#if loading} 157 <Spinner class={spinnerSize} /> 158 {:else} 159 {#if Icon && iconSide == "left"} 160 <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 161 {/if} 162 {#if children} 163 {@render children()} 164 {/if} 165 {#if Icon && iconSide == "right"} 166 <Icon class="{iconSize} shrink-0" aria-hidden="true" /> 167 {/if} 168 {/if} 169 </span> 170{/snippet} 171 172{#if href} 173 <a 174 {href} 175 class={classes} 176 aria-disabled={disabled || loading ? "true" : undefined} 177 tabindex={disabled ? -1 : undefined} 178 {...rest} 179 > 180 {@render content()} 181 </a> 182{:else} 183 <button {type} disabled={disabled || loading} class={classes} aria-busy={loading} {...rest}> 184 {@render content()} 185 </button> 186{/if}