Skip to content

Commit 0c4973a

Browse files
committed
feat: finish up buttonIcon + stories
1 parent f8bee3d commit 0c4973a

File tree

2 files changed

+163
-50
lines changed

2 files changed

+163
-50
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import ButtonIcon from './ButtonIcon.svelte'
2+
import { FlashlightIcon } from '@hugeicons/core-free-icons'
3+
import type { ComponentProps } from 'svelte'
4+
5+
export default {
6+
title: 'UI/ButtonIcon',
7+
component: ButtonIcon,
8+
tags: ['autodocs'],
9+
10+
parameters: {
11+
backgrounds: { default: 'dark' },
12+
},
13+
render: (args: {
14+
Component: ButtonIcon<{
15+
variant: 'white'
16+
ariaLabel: 'Default button'
17+
size: 'md'
18+
icon: typeof FlashlightIcon
19+
}>
20+
props: ComponentProps<typeof ButtonIcon>
21+
}) => ({
22+
Component: ButtonIcon,
23+
props: args,
24+
}),
25+
}
26+
27+
export const Default = {
28+
render: () => ({
29+
Component: ButtonIcon,
30+
props: {
31+
variant: 'white',
32+
ariaLabel: 'Default button',
33+
size: 'md',
34+
icon: FlashlightIcon,
35+
},
36+
}),
37+
}
38+
39+
export const Loading = {
40+
render: () => ({
41+
Component: ButtonIcon,
42+
props: {
43+
variant: 'white',
44+
ariaLabel: 'Loading button',
45+
size: 'md',
46+
icon: FlashlightIcon,
47+
isLoading: true,
48+
},
49+
}),
50+
}
51+
52+
export const Active = {
53+
render: () => ({
54+
Component: ButtonIcon,
55+
props: {
56+
variant: 'white',
57+
ariaLabel: 'Active button',
58+
size: 'md',
59+
icon: FlashlightIcon,
60+
isActive: true,
61+
},
62+
}),
63+
}
Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
<script lang="ts" generics="T">
22
import { cn } from '$lib/utils'
33
import type { HTMLButtonAttributes } from 'svelte/elements'
4+
import { HugeiconsIcon, type IconSvgElement } from '@hugeicons/svelte'
45
56
interface IButtonProps extends HTMLButtonAttributes {
6-
variant?: 'white' | 'clear'
7+
variant?: 'white' | 'clear-on-light' | 'clear-on-dark'
78
isLoading?: boolean
8-
cb?: () => Promise<void>
9+
callback?: () => Promise<void>
10+
onclick?: () => void
911
blockingClick?: boolean
1012
type?: 'button' | 'submit' | 'reset'
13+
size?: 'sm' | 'md' | 'lg'
14+
iconSize?: 'sm' | 'md' | 'lg' | number
15+
icon: IconSvgElement
16+
isActive?: boolean
1117
}
1218
1319
let {
1420
variant = 'white',
1521
isLoading,
16-
cb,
22+
callback,
23+
onclick,
1724
blockingClick,
1825
type = 'button',
26+
size = 'md',
27+
icon,
28+
iconSize = undefined,
29+
isActive = false,
1930
children = undefined,
2031
...restProps
2132
}: IButtonProps = $props()
@@ -24,11 +35,11 @@
2435
let disabled = $derived(restProps.disabled || isLoading || isSubmitting)
2536
2637
const handleClick = async () => {
27-
if (typeof cb !== 'function') return
38+
if (typeof callback !== 'function') return
2839
2940
if (blockingClick) isSubmitting = true
3041
try {
31-
await cb()
42+
await callback()
3243
} catch (error) {
3344
console.error('Error in button callback:', error)
3445
} finally {
@@ -38,24 +49,52 @@
3849
3950
const variantClasses = {
4051
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' },
4254
}
4355
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' },
4760
}
4861
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+
4983
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+
),
5288
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,
5693
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,
5998
disabled: 'cursor-not-allowed',
6099
})
61100
</script>
@@ -72,44 +111,55 @@
72111
].join(' ')
73112
)}
74113
{disabled}
75-
onclick={handleClick}
114+
onclick={callback ? handleClick : onclick}
76115
{type}
77116
>
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}
82118
<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}
89126
</button>
90127

91128
<!--
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">
108150
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

Comments
 (0)