Monorepo for Tangled tangled.org
1

Configure Feed

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

core / web / src / lib / components / ui / Button.svelte
5.0 kB 222 lines
1<script lang="ts"> 2 import { resolve } from '$app/paths'; 3 import type { Snippet } from 'svelte'; 4 import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; 5 import Spinner from './Spinner.svelte'; 6 7 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, 'class' | 'children'> { 8 variant?: 'default' | 'primary' | 'ghost' | 'danger' | 'success'; 9 size?: 'sm' | 'md' | 'lg'; 10 href?: string; 11 type?: 'button' | 'submit' | 'reset'; 12 disabled?: boolean; 13 class?: string; 14 loading?: boolean; 15 spinnerClass?: string; 16 children?: Snippet; 17 } 18 19 let { 20 variant = 'default', 21 size = 'md', 22 href, 23 type = 'button', 24 disabled = false, 25 class: className = '', 26 loading = false, 27 spinnerClass, 28 children, 29 ...rest 30 }: Props = $props(); 31 32 const raised = $derived(variant !== 'ghost'); 33 const accentRaised = $derived(variant !== 'default' && variant !== 'ghost'); 34 const spinnerSize = $derived(spinnerClass ?? (size === 'lg' ? 'size-5' : 'size-4')); 35</script> 36 37{#if href} 38 <a 39 href={resolve(href as '/')} 40 class="btn {className}" 41 data-variant={variant} 42 data-size={size} 43 class:raised 44 class:accentRaised 45 aria-disabled={disabled || loading ? 'true' : undefined} 46 tabindex={disabled ? -1 : undefined} 47 {...rest} 48 > 49 {#if loading} 50 <Spinner class={spinnerSize} /> 51 {:else if children} 52 {@render children()} 53 {/if} 54 </a> 55{:else} 56 <button 57 {type} 58 disabled={disabled || loading} 59 class="btn {className}" 60 data-variant={variant} 61 data-size={size} 62 class:raised 63 class:accentRaised 64 aria-busy={loading} 65 {...rest} 66 > 67 {#if loading} 68 <Spinner class={spinnerSize} /> 69 {:else if children} 70 {@render children()} 71 {/if} 72 </button> 73{/if} 74 75<style> 76 .btn { 77 display: inline-flex; 78 cursor: pointer; 79 align-items: center; 80 justify-content: center; 81 overflow: hidden; 82 border-radius: 0.25rem; 83 text-decoration: none; 84 transition: 85 background-color 150ms cubic-bezier(0.4, 0, 0.2, 1), 86 border-color 150ms cubic-bezier(0.4, 0, 0.2, 1), 87 color 150ms cubic-bezier(0.4, 0, 0.2, 1); 88 user-select: none; 89 } 90 91 .btn:hover { 92 text-decoration: none; 93 } 94 95 .btn:focus-visible { 96 outline: 2px solid var(--color-ring); 97 outline-offset: 2px; 98 } 99 100 .btn:disabled { 101 pointer-events: none; 102 opacity: 0.5; 103 } 104 105 .btn[data-variant='default'] { 106 background-color: var(--color-surface); 107 border: 1px solid var(--color-border); 108 color: var(--color-fg); 109 } 110 .btn[data-variant='default']:hover { 111 background-color: var(--color-surface-hover); 112 } 113 .btn[data-variant='default']:focus-visible { 114 outline-color: var(--color-ring); 115 } 116 117 .btn[data-variant='primary'] { 118 background-color: var(--color-primary); 119 border: 1px solid var(--color-primary-border); 120 color: var(--color-primary-fg); 121 } 122 .btn[data-variant='primary']:hover { 123 background-color: var(--color-primary-hover); 124 } 125 .btn[data-variant='primary']:focus-visible { 126 outline-color: var(--color-primary); 127 } 128 129 .btn[data-variant='ghost'] { 130 color: var(--color-fg-muted); 131 background-color: transparent; 132 } 133 .btn[data-variant='ghost']:hover { 134 background-color: var(--color-surface-muted); 135 } 136 .btn[data-variant='ghost']:focus-visible { 137 outline-color: var(--color-ring); 138 } 139 140 .btn[data-variant='danger'] { 141 background-color: var(--color-danger); 142 border: 1px solid var(--color-danger-border); 143 color: var(--color-danger-fg); 144 } 145 .btn[data-variant='danger']:hover { 146 background-color: var(--color-danger-hover); 147 } 148 .btn[data-variant='danger']:focus-visible { 149 outline-color: var(--color-danger); 150 } 151 152 .btn[data-variant='success'] { 153 background-color: var(--color-success); 154 border: 1px solid var(--color-success-border); 155 color: var(--color-success-fg); 156 } 157 .btn[data-variant='success']:hover { 158 background-color: var(--color-success-hover); 159 } 160 .btn[data-variant='success']:focus-visible { 161 outline-color: var(--color-success); 162 } 163 164 .btn[data-size='sm'] { 165 min-height: 2rem; 166 padding: 0.375rem 0.5rem; 167 font-size: 0.875rem; 168 gap: 0.375rem; 169 } 170 .btn[data-size='md'] { 171 min-height: 2.25rem; 172 padding: 0.5rem 1rem; 173 font-size: 0.875rem; 174 gap: 0.5rem; 175 } 176 .btn[data-size='lg'] { 177 min-height: 2.5rem; 178 padding: 0.5rem 1.5rem; 179 font-size: 1rem; 180 gap: 0.625rem; 181 } 182 183 .raised { 184 position: relative; 185 z-index: 0; 186 } 187 188 .raised::before { 189 content: ''; 190 position: absolute; 191 inset: 0; 192 z-index: -10; 193 border-radius: inherit; 194 box-shadow: inset 0 -2px 0 0 var(--color-raised-depth); 195 transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1); 196 } 197 198 .raised:hover::before { 199 box-shadow: inset 0 -2px 0 0 var(--color-raised-depth-hover); 200 } 201 202 .raised:active { 203 transform: translateY(0.125rem); 204 } 205 206 .raised:active::before { 207 box-shadow: inset 0 2px 2px 0 var(--color-raised-depth); 208 } 209 210 .accentRaised::before, 211 .accentRaised:hover::before { 212 box-shadow: inset 0 -2px 0 0 var(--color-raised-accent-depth); 213 } 214 215 .accentRaised:active::before { 216 box-shadow: inset 0 2px 2px 0 var(--color-raised-accent-depth-active); 217 } 218 219 .raised:disabled:active { 220 transform: none; 221 } 222</style>