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