Coves frontend - a photon fork
1<script module lang="ts">
2 import { Spinner } from 'mono-svelte'
3 import type { Snippet } from 'svelte'
4 import { type IconSource, Icon } from 'svelte-hero-icons/dist'
5 import type { ClassValue, HTMLButtonAttributes } from 'svelte/elements'
6
7 export type ButtonColor = keyof typeof buttonColor
8 export type ButtonAlignment = keyof typeof buttonAlignment
9 export type ButtonShadow = keyof typeof buttonShadow
10
11 export const buttonAlignment = {
12 left: 'justify-start text-left origin-left',
13 center: 'justify-center',
14 right: 'justify-end text-right origin-right',
15 }
16
17 export const buttonColor = {
18 primary: 'btn-primary',
19 secondary: 'btn-secondary',
20 tertiary: 'btn-tertiary',
21 danger: 'btn-danger',
22 ghost: 'btn-ghost',
23 'danger-subtle': 'text-red-500 hover:bg-red-500 hover:text-inherit!',
24 'success-subtle': 'text-green-500 hover:bg-green-500 hover:text-inherit!',
25 'warning-subtle': 'text-yellow-500 hover:bg-yellow-500 hover:text-inherit!',
26 'blue-subtle': `text-blue-500 hover:bg-blue-500 hover:text-inherit!`,
27
28 none: '',
29 }
30
31 export const buttonShadow = {
32 sm: 'shadow-xs',
33 none: 'shadow-none',
34 }
35
36 export type ButtonSize = keyof typeof buttonSize
37
38 export const buttonSize = {
39 xs: 'btn-xs',
40 sm: 'btn-sm',
41 md: 'btn-md',
42 lg: 'btn-lg',
43 xl: 'btn-xl',
44 'square-sm': 'btn-square-sm',
45 'square-md': 'btn-square-md',
46 'square-lg': 'btn-square-lg',
47 'square-xl': 'btn-square-xl',
48 custom: '',
49 }
50
51 const buttonRounding = {
52 pill: 'rounded-full',
53 '2xl': 'rounded-2xl',
54 xl: 'rounded-xl',
55 lg: 'rounded-lg',
56 md: 'rounded-md',
57 inherit: 'rounded-[inherit]',
58 none: '',
59 }
60 type ButtonRoundness = keyof typeof buttonRounding
61
62 const buttonWeight = {
63 md: 'font-medium',
64 none: '',
65 }
66 type ButtonWeight = keyof typeof buttonWeight
67
68 const buttonGap = {
69 xl: 'gap-3',
70 lg: 'gap-2',
71 md: 'gap-1.5',
72 none: '',
73 }
74 type ButtonGap = keyof typeof buttonGap
75
76 interface Props {
77 loading?: boolean
78 submit?: boolean
79 type?: 'button' | 'none'
80 color?: ButtonColor
81 size?: ButtonSize
82 rounding?: ButtonRoundness
83 alignment?: ButtonAlignment
84 shadow?: ButtonShadow
85 gap?: ButtonGap
86 loaderWidth?: number | undefined
87 href?: string | undefined
88 class?: ClassValue
89 prefix?: Snippet
90 children?: Snippet
91 suffix?: Snippet
92 icon?: IconSource
93 weight?: ButtonWeight
94 disabled?: boolean
95 onclick?: HTMLButtonAttributes['onclick']
96 // Common HTML attributes
97 id?: string
98 title?: string
99 tabindex?: number
100 'aria-label'?: string
101 'aria-describedby'?: string
102 'aria-expanded'?: boolean
103 'aria-haspopup'?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
104 'aria-pressed'?: boolean | 'mixed'
105 'aria-controls'?: string
106 target?: string
107 rel?: string
108 download?: string | boolean
109 // Allow rest props
110 [key: string]: unknown
111 }
112
113 export type { Props as ButtonProps }
114</script>
115
116<script lang="ts">
117 let {
118 loading = false,
119 submit = false,
120 type = 'button',
121 color = 'secondary',
122 size = 'md',
123 rounding = size == 'lg' || size == 'square-lg' ? '2xl' : 'xl',
124 alignment = 'center',
125 shadow = color == 'primary' || color == 'secondary' ? 'sm' : 'none',
126 gap = 'md',
127 disabled,
128 loaderWidth = undefined,
129 href = undefined,
130 class: clazz = '',
131 prefix,
132 children,
133 suffix,
134 icon,
135 weight = 'md',
136 ...rest
137 }: Props = $props()
138</script>
139
140<svelte:element
141 this={href ? 'a' : 'button'}
142 role={href ? 'link' : 'button'}
143 {href}
144 {...rest}
145 tabindex={disabled ? -1 : undefined}
146 class={[
147 type == 'button' && 'btn',
148 buttonSize[size],
149 buttonRounding[rounding],
150 buttonShadow[shadow],
151 buttonColor[color],
152 buttonAlignment[alignment],
153 buttonWeight[weight],
154 buttonGap[gap],
155 (disabled || loading) && 'btn-disabled',
156 alignment == 'center'
157 ? 'origin-center'
158 : alignment == 'left'
159 ? 'origin-left'
160 : 'origin-right',
161 clazz,
162 ]}
163 type={submit ? 'submit' : 'button'}
164>
165 {#if loading}
166 <Spinner width={loaderWidth ?? 16} />
167 {:else if prefix}
168 {@render prefix?.()}
169 {:else if icon}
170 <Icon
171 src={icon}
172 size="16"
173 mini
174 class={[color == 'secondary' && 'text-slate-600 dark:text-zinc-400']}
175 />
176 {/if}
177 {@render children?.()}
178 {@render suffix?.()}
179</svelte:element>
180
181<!--
182 @component
183
184 @slot `prefix` -- Will be replaced if `loading` is `true`.
185 @slot `suffix`
186-->
187
188<style>
189 @reference "../../../../app.css";
190
191 :global {
192 .btn {
193 display: flex;
194 flex-direction: row;
195 align-items: center;
196 font-size: var(--text-sm);
197
198 transition: 75ms cubic-bezier(0.075, 0.82, 0.165, 1);
199
200 @variant hover {
201 cursor: pointer;
202 }
203 }
204
205 .no-hover:hover {
206 cursor: normal !important;
207 }
208
209 .btn-primary {
210 border: 1px solid transparent;
211 background: radial-gradient(
212 circle at 20% 0%,
213 var(--color-primary-800),
214 var(--color-primary-900)
215 );
216 background-size: 110% 110% !important;
217 color: var(--color-slate-50);
218
219 @variant dark {
220 background: radial-gradient(
221 circle at center right,
222 var(--color-primary-200),
223 var(--color-primary-100)
224 );
225 color: var(--color-zinc-900);
226 }
227
228 @variant hover {
229 filter: brightness(120%);
230 @variant dark {
231 filter: brightness(90%);
232 }
233 }
234
235 @variant active {
236 filter: brightness(95%);
237 @variant dark {
238 filter: brightness(85%);
239 }
240 }
241 }
242
243 .btn-secondary {
244 border: 1px solid var(--color-slate-200);
245 border-bottom-color: var(--color-slate-300);
246 background-color: var(--color-white);
247
248 @variant dark {
249 border: 1px solid var(--color-zinc-800);
250 background-color: var(--color-zinc-900);
251 }
252
253 @variant hover {
254 background-color: color-mix(
255 in oklab,
256 var(--color-white),
257 var(--color-slate-50)
258 );
259 @variant dark {
260 background-color: color-mix(
261 in oklab,
262 var(--color-zinc-925),
263 var(--color-zinc-900)
264 );
265 }
266 }
267
268 @variant active {
269 background-color: var(--color-slate-100);
270
271 @variant dark {
272 background-color: var(--color-zinc-925);
273 }
274 }
275 }
276
277 .btn-tertiary {
278 background-color: transparent;
279
280 @variant hover {
281 background-color: --alpha(var(--color-slate-200) / 50%);
282 @variant dark {
283 background-color: --alpha(var(--color-zinc-700) / 30%);
284 }
285 }
286
287 @variant active {
288 background-color: --alpha(var(--color-slate-300) / 50%);
289 @variant dark {
290 background-color: --alpha(var(--color-zinc-800) / 30%);
291 }
292 }
293 }
294
295 .btn-danger {
296 /* border border-red-500 bg-red-500 hover:text-red-500 hover:bg-transparent text-white */
297
298 background-color: var(--color-red-600);
299 color: var(--color-white);
300
301 @variant dark {
302 background-color: var(--color-red-400);
303 color: var(--color-black);
304 }
305
306 @variant hover {
307 filter: brightness(120%);
308 @variant dark {
309 filter: brightness(90%);
310 }
311 }
312 }
313
314 .btn-ghost {
315 border: 1px solid var(--color-slate-200);
316
317 @variant hover {
318 background-color: var(--color-slate-100);
319 }
320
321 @variant active {
322 background-color: --alpha(var(--color-slate-200) / 75%);
323 }
324
325 @variant dark {
326 border-color: var(--color-zinc-800);
327 @variant hover {
328 background-color: var(--color-zinc-800);
329 border-color: var(--color-zinc-700);
330 color: var(--color-zinc-200);
331 }
332 @variant active {
333 background-color: var(--color-zinc-900);
334 }
335 }
336 }
337
338 .btn-xs {
339 padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2);
340 font-size: var(--text-xs);
341 }
342
343 .btn-sm {
344 padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 3);
345 font-size: 12px;
346 }
347
348 .btn-md {
349 padding-block: calc(var(--spacing) * 1.5);
350 padding-inline: calc(var(--spacing) * 3);
351 font-size: var(--text-sm);
352 }
353
354 .btn-lg {
355 padding-block: calc(var(--spacing) * 2);
356 padding-inline: calc(var(--spacing) * 5);
357 font-size: var(--text-sm);
358 }
359
360 .btn-xl {
361 padding: calc(var(--spacing) * 3) calc(var(--spacing) * 6);
362 font-size: var(--text-base);
363 }
364
365 .btn-square-sm {
366 width: calc(var(--spacing) * 6);
367 height: calc(var(--spacing) * 6);
368 padding: 0;
369 }
370
371 .btn-square-md {
372 width: calc(var(--spacing) * 8);
373 height: calc(var(--spacing) * 8);
374 padding: 0;
375 }
376
377 .btn-square-lg {
378 width: calc(var(--spacing) * 9.5);
379 height: calc(var(--spacing) * 9.5);
380 padding: 0;
381 }
382
383 .btn-square-xl {
384 width: calc(var(--spacing) * 12);
385 height: calc(var(--spacing) * 12);
386 padding: 0;
387 }
388
389 .btn-disabled {
390 pointer-events: none;
391 opacity: 0.6;
392 cursor: normal;
393
394 @variant dark {
395 opacity: 0.5;
396 }
397 }
398 }
399</style>