|
1 | 1 | <script lang="ts" generics="T">
|
2 | 2 | import { cn } from '$lib/utils'
|
3 | 3 | import type { HTMLButtonAttributes } from 'svelte/elements'
|
| 4 | + import { HugeiconsIcon, type IconSvgElement } from '@hugeicons/svelte' |
4 | 5 |
|
5 | 6 | interface IButtonProps extends HTMLButtonAttributes {
|
6 |
| - variant?: 'white' | 'clear' |
| 7 | + variant?: 'white' | 'clear-on-light' | 'clear-on-dark' |
7 | 8 | isLoading?: boolean
|
8 |
| - cb?: () => Promise<void> |
| 9 | + callback?: () => Promise<void> |
| 10 | + onclick?: () => void |
9 | 11 | blockingClick?: boolean
|
10 | 12 | type?: 'button' | 'submit' | 'reset'
|
| 13 | + size?: 'sm' | 'md' | 'lg' |
| 14 | + iconSize?: 'sm' | 'md' | 'lg' | number |
| 15 | + icon: IconSvgElement |
| 16 | + isActive?: boolean |
11 | 17 | }
|
12 | 18 |
|
13 | 19 | let {
|
14 | 20 | variant = 'white',
|
15 | 21 | isLoading,
|
16 |
| - cb, |
| 22 | + callback, |
| 23 | + onclick, |
17 | 24 | blockingClick,
|
18 | 25 | type = 'button',
|
| 26 | + size = 'md', |
| 27 | + icon, |
| 28 | + iconSize = undefined, |
| 29 | + isActive = false, |
19 | 30 | children = undefined,
|
20 | 31 | ...restProps
|
21 | 32 | }: IButtonProps = $props()
|
|
24 | 35 | let disabled = $derived(restProps.disabled || isLoading || isSubmitting)
|
25 | 36 |
|
26 | 37 | const handleClick = async () => {
|
27 |
| - if (typeof cb !== 'function') return |
| 38 | + if (typeof callback !== 'function') return |
28 | 39 |
|
29 | 40 | if (blockingClick) isSubmitting = true
|
30 | 41 | try {
|
31 |
| - await cb() |
| 42 | + await callback() |
32 | 43 | } catch (error) {
|
33 | 44 | console.error('Error in button callback:', error)
|
34 | 45 | } finally {
|
|
38 | 49 |
|
39 | 50 | const variantClasses = {
|
40 | 51 | white: { background: 'bg-white-900', text: 'text-black' },
|
41 |
| - clear: { background: 'transparent', text: 'text-white-900' }, |
| 52 | + 'clear-on-light': { background: 'transparent', text: 'text-black' }, |
| 53 | + 'clear-on-dark': { background: 'transparent', text: 'text-white' }, |
42 | 54 | }
|
43 | 55 |
|
44 |
| - const disabledVariantClasses = { |
45 |
| - white: { background: 'bg-white-300', text: 'text-black' }, |
46 |
| - clear: { background: 'bg-transparent', text: 'text-white-300' }, |
| 56 | + const disabledClasses = { |
| 57 | + white: { background: 'bg-white-900', text: 'text-black-500' }, |
| 58 | + 'clear-on-light': { background: 'bg-transparent', text: 'text-black-500' }, |
| 59 | + 'clear-on-dark': { background: 'bg-transparent', text: 'text-black-500' }, |
47 | 60 | }
|
48 | 61 |
|
| 62 | + const isActiveClasses = { |
| 63 | + white: { background: 'bg-secondary-900', text: 'text-black' }, |
| 64 | + 'clear-on-light': { background: 'bg-secondary-900', text: 'text-black' }, |
| 65 | + 'clear-on-dark': { background: 'bg-secondary-900', text: 'text-black' }, |
| 66 | + } |
| 67 | +
|
| 68 | + const sizeVariant = { |
| 69 | + sm: 'h-8 w-8', |
| 70 | + md: 'h-[54px] w-[54px]', |
| 71 | + lg: 'h-[108px] w-[108px]', |
| 72 | + } |
| 73 | +
|
| 74 | + const iconSizeVariant = { |
| 75 | + sm: 24, |
| 76 | + md: 24, |
| 77 | + lg: 36, |
| 78 | + } |
| 79 | +
|
| 80 | + let resolvedIconSize = |
| 81 | + typeof iconSize === 'number' ? iconSize : iconSizeVariant[iconSize ?? size] |
| 82 | +
|
49 | 83 | let classes = $derived({
|
50 |
| - common: |
51 |
| - 'cursor-pointer flex items-center justify-center p-8 rounded-full text-xl font-semibold h-[56px] duration-100', |
| 84 | + common: cn( |
| 85 | + 'cursor-pointer w-min flex items-center justify-center rounded-full font-semibold duration-100', |
| 86 | + sizeVariant[size] |
| 87 | + ), |
52 | 88 | background: disabled
|
53 |
| - ? disabledVariantClasses[variant].background || |
54 |
| - variantClasses[variant].background |
55 |
| - : variantClasses[variant].background, |
| 89 | + ? disabledClasses[variant].background |
| 90 | + : isActive |
| 91 | + ? isActiveClasses[variant].background |
| 92 | + : variantClasses[variant].background, |
56 | 93 | text: disabled
|
57 |
| - ? disabledVariantClasses[variant].text || variantClasses[variant].text |
58 |
| - : variantClasses[variant].text, |
| 94 | + ? disabledClasses[variant].text |
| 95 | + : isActive |
| 96 | + ? isActiveClasses[variant].text |
| 97 | + : variantClasses[variant].text, |
59 | 98 | disabled: 'cursor-not-allowed',
|
60 | 99 | })
|
61 | 100 | </script>
|
|
72 | 111 | ].join(' ')
|
73 | 112 | )}
|
74 | 113 | {disabled}
|
75 |
| - onclick={handleClick} |
| 114 | + onclick={callback ? handleClick : onclick} |
76 | 115 | {type}
|
77 | 116 | >
|
78 |
| - <div class="relative flex items-center justify-center"> |
79 |
| - {#if isLoading || isSubmitting} |
80 |
| - <div class="loading loading-spinner loading-md absolute -left-4"></div> |
81 |
| - {/if} |
| 117 | + {#if isLoading || isSubmitting} |
82 | 118 | <div
|
83 |
| - class="flex items-center justify-center duration-100" |
84 |
| - class:translate-x-4={isLoading || isSubmitting} |
85 |
| - > |
86 |
| - {@render children?.()} |
87 |
| - </div> |
88 |
| - </div> |
| 119 | + class="loading loading-spinner absolute loading-lg {variantClasses[ |
| 120 | + variant |
| 121 | + ].text}" |
| 122 | + ></div> |
| 123 | + {:else} |
| 124 | + <HugeiconsIcon {icon} size={resolvedIconSize} /> |
| 125 | + {/if} |
89 | 126 | </button>
|
90 | 127 |
|
91 | 128 | <!--
|
92 |
| - @component |
93 |
| - export default ButtonAction |
94 |
| - @description |
95 |
| - This component is a button with a loading spinner that can be used to indicate that an action is being performed. |
96 |
| - |
97 |
| - @props |
98 |
| - - variant: The variant of the button. Default is `solid`. |
99 |
| - - isLoading: A boolean to indicate if the button is in a loading state. |
100 |
| - - cb: A callback function that will be called when the button is clicked. |
101 |
| - - blockingClick: A boolean to indicate if the button should block the click event while the callback function is being executed. |
102 |
| - - icon: A slot for an icon to be displayed inside the button. |
103 |
| - - ...restProps: Any other props that can be passed to a button element. |
104 |
| - |
105 |
| - @usage |
106 |
| - ```html |
107 |
| - <script lang="ts"> |
| 129 | + @component |
| 130 | + export default ButtonIcon |
| 131 | + @description |
| 132 | + ButtonIcon component is a button with an icon. |
| 133 | + |
| 134 | + @props |
| 135 | + - variant: 'white' | 'clear-on-light' | 'clear-on-dark' . |
| 136 | + - isLoading: boolean |
| 137 | + - callback: () => Promise<void> |
| 138 | + - onclick: () => void |
| 139 | + - blockingClick: boolean - Prevents multiple clicks |
| 140 | + - type: 'button' | 'submit' | 'reset' |
| 141 | + - size: 'sm' | 'md' | 'lg' |
| 142 | + - iconSize: 'sm' | 'md' | 'lg' | number |
| 143 | + - icon: IconSvgElement - Needs icon from Hugeicon library |
| 144 | + - isActive: boolean |
| 145 | +
|
| 146 | + |
| 147 | + @usage |
| 148 | + ```html |
| 149 | + <script lang="ts"> |
108 | 150 | import * as Button from '$lib/ui/Button'
|
109 |
| - </script> |
110 |
| - |
111 |
| - <Button.Action variant="solid" cb={() => console.log('clicked')}> |
112 |
| - Click me |
113 |
| - </Button.Action> |
114 |
| - ``` |
115 |
| - --> |
| 151 | + import { FlashlightIcon } from '@hugeicons/core-free-icons' |
| 152 | + |
| 153 | + let flashlightOn = $state(false) |
| 154 | + </script> |
| 155 | +
|
| 156 | + <Button.Icon |
| 157 | + variant="white" |
| 158 | + aria-label="Open pane" |
| 159 | + size="md" |
| 160 | + icon={FlashlightIcon} |
| 161 | + onclick={() => (flashlightOn = !flashlightOn)} |
| 162 | + isActive={flashlightOn} |
| 163 | + ></Button.Icon> |
| 164 | + ``` |
| 165 | + --> |
0 commit comments