Skip to content

Commit 05293f3

Browse files
authored
feat: add icon button component (#68)
* feat: add icon button component * feat: finish up buttonIcon + stories * fix: update with new color naming * feat: polish button icon (and button action too) * chore: format lint * chore: sort imports * chore: format, not sure why
1 parent ded7f9c commit 05293f3

File tree

5 files changed

+223
-4
lines changed

5 files changed

+223
-4
lines changed
File renamed without changes.

infrastructure/eid-wallet/src/lib/ui/Button/ButtonAction.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ComponentProps } from "svelte";
2+
import { ButtonText } from "./Button.stories.snippet.svelte";
23
import ButtonAction from "./ButtonAction.svelte";
3-
import { ButtonText } from "./ButtonSnippets.svelte";
44

55
export default {
66
title: "UI/ButtonAction",

infrastructure/eid-wallet/src/lib/ui/Button/ButtonAction.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface IButtonProps extends HTMLButtonAttributes {
1111
size?: "sm" | "md";
1212
}
1313
14-
let {
14+
const {
1515
variant = "solid",
1616
isLoading,
1717
callback,
@@ -23,7 +23,7 @@ let {
2323
}: IButtonProps = $props();
2424
2525
let isSubmitting = $state(false);
26-
let disabled = $derived(restProps.disabled || isLoading || isSubmitting);
26+
const disabled = $derived(restProps.disabled || isLoading || isSubmitting);
2727
2828
const handleClick = async () => {
2929
if (typeof callback !== "function") return;
@@ -59,7 +59,7 @@ const sizeVariant = {
5959
md: "px-8 py-2.5 text-xl h-14",
6060
};
6161
62-
let classes = $derived({
62+
const classes = $derived({
6363
common: cn(
6464
"cursor-pointer w-min flex items-center justify-center rounded-full font-semibold duration-100",
6565
sizeVariant[size],
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { FlashlightIcon } from "@hugeicons/core-free-icons";
2+
import type { ComponentProps } from "svelte";
3+
import ButtonIcon from "./ButtonIcon.svelte";
4+
5+
export default {
6+
title: "UI/ButtonIcon",
7+
component: ButtonIcon,
8+
tags: ["autodocs"],
9+
render: (args: {
10+
Component: ButtonIcon<{ icon: typeof FlashlightIcon }>;
11+
props: ComponentProps<typeof ButtonIcon>;
12+
}) => ({
13+
Component: ButtonIcon,
14+
props: args,
15+
}),
16+
};
17+
18+
export const Default = {
19+
render: () => ({
20+
Component: ButtonIcon,
21+
props: {
22+
variant: "white",
23+
ariaLabel: "Default button",
24+
size: "md",
25+
icon: FlashlightIcon,
26+
},
27+
}),
28+
};
29+
30+
export const Loading = {
31+
render: () => ({
32+
Component: ButtonIcon,
33+
props: {
34+
variant: "white",
35+
ariaLabel: "Loading button",
36+
size: "md",
37+
icon: FlashlightIcon,
38+
isLoading: true,
39+
},
40+
}),
41+
};
42+
43+
export const Active = {
44+
render: () => ({
45+
Component: ButtonIcon,
46+
props: {
47+
variant: "white",
48+
ariaLabel: "Active button",
49+
size: "md",
50+
icon: FlashlightIcon,
51+
isActive: true,
52+
},
53+
}),
54+
};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<script lang="ts" generics="T">
2+
import { cn } from "$lib/utils";
3+
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/svelte";
4+
import type { HTMLButtonAttributes } from "svelte/elements";
5+
6+
interface IButtonProps extends HTMLButtonAttributes {
7+
variant?: "white" | "clear-on-light" | "clear-on-dark";
8+
isLoading?: boolean;
9+
callback?: () => Promise<void>;
10+
onclick?: () => void;
11+
blockingClick?: boolean;
12+
type?: "button" | "submit" | "reset";
13+
size?: "sm" | "md" | "lg";
14+
iconSize?: "sm" | "md" | "lg" | number;
15+
icon: IconSvgElement;
16+
isActive?: boolean;
17+
}
18+
19+
const {
20+
variant = "white",
21+
isLoading,
22+
callback,
23+
onclick,
24+
blockingClick,
25+
type = "button",
26+
size = "md",
27+
icon,
28+
iconSize = undefined,
29+
isActive = false,
30+
children = undefined,
31+
...restProps
32+
}: IButtonProps = $props();
33+
34+
let isSubmitting = $state(false);
35+
const disabled = $derived(restProps.disabled || isLoading || isSubmitting);
36+
37+
const handleClick = async () => {
38+
if (typeof callback !== "function") return;
39+
40+
if (blockingClick) isSubmitting = true;
41+
try {
42+
await callback();
43+
} catch (error) {
44+
console.error("Error in button callback:", error);
45+
} finally {
46+
isSubmitting = false;
47+
}
48+
};
49+
50+
const variantClasses = {
51+
white: { background: "bg-white", text: "text-black" },
52+
"clear-on-light": { background: "transparent", text: "text-black" },
53+
"clear-on-dark": { background: "transparent", text: "text-white" },
54+
};
55+
56+
const disabledClasses = {
57+
white: { background: "bg-white", 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" },
60+
};
61+
62+
const isActiveClasses = {
63+
white: { background: "bg-secondary-500", text: "text-black" },
64+
"clear-on-light": { background: "bg-secondary-500", text: "text-black" },
65+
"clear-on-dark": { background: "bg-secondary-500", 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+
const resolvedIconSize =
81+
typeof iconSize === "number" ? iconSize : iconSizeVariant[iconSize ?? size];
82+
83+
const classes = $derived({
84+
common: cn(
85+
"cursor-pointer w-min flex items-center justify-center rounded-full font-semibold duration-100",
86+
sizeVariant[size],
87+
),
88+
background: disabled
89+
? disabledClasses[variant].background
90+
: isActive
91+
? isActiveClasses[variant].background
92+
: variantClasses[variant].background,
93+
text: disabled
94+
? disabledClasses[variant].text
95+
: isActive
96+
? isActiveClasses[variant].text
97+
: variantClasses[variant].text,
98+
disabled: "cursor-not-allowed",
99+
});
100+
</script>
101+
102+
<button
103+
{...restProps}
104+
class={cn(
105+
[
106+
classes.common,
107+
classes.background,
108+
classes.text,
109+
disabled && classes.disabled,
110+
restProps.class,
111+
].join(' ')
112+
)}
113+
{disabled}
114+
onclick={callback ? handleClick : onclick}
115+
{type}
116+
>
117+
{#if isLoading || isSubmitting}
118+
<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}
126+
</button>
127+
128+
<!--
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">
150+
import * as Button from '$lib/ui/Button'
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)