Skip to content

Commit 8cc4af5

Browse files
committed
feat: twmerge, clsx, button in ui added
1 parent 1888acf commit 8cc4af5

13 files changed

Lines changed: 583 additions & 1 deletion

infrastructure/control-panel/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"vite": "^7.0.4"
4343
},
4444
"dependencies": {
45-
"@inlang/paraglide-js": "^2.0.0"
45+
"@inlang/paraglide-js": "^2.0.0",
46+
"clsx": "^2.1.1",
47+
"tailwind-merge": "^3.0.2"
4648
}
4749
}

infrastructure/control-panel/src/app.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
@import 'tailwindcss';
22

3+
@font-face {
4+
font-family: "Archivo";
5+
src: url("/fonts/Archivo-VariableFont_wdth,wght.ttf") format("truetype");
6+
font-weight: 100 900;
7+
font-style: normal;
8+
}
9+
10+
@layer base {
11+
/* Typography */
12+
h1 {
13+
@apply text-[90px]/[1.5] text-black font-semibold;
14+
}
15+
16+
h2 {
17+
@apply text-6xl/[1.5] text-black font-semibold;
18+
}
19+
20+
h3 {
21+
@apply text-3xl/[1.5] text-black font-semibold;
22+
}
23+
24+
h4 {
25+
@apply text-xl/[1.5] text-black font-semibold;
26+
}
27+
28+
p {
29+
@apply text-base/[1.5] text-black font-normal;
30+
}
31+
32+
.small {
33+
@apply text-xs/[1.5] text-black font-normal;
34+
}
35+
}
36+
337
@theme {
438
/* Custom theme */
539
--color-primary: #8e52ff;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script context="module">
2+
import { ArrowRight01Icon, SettingsIcon } from "@hugeicons/core-free-icons";
3+
import { HugeiconsIcon } from "@hugeicons/svelte";
4+
5+
export { ButtonText, ButtonNavText, ButtonNavSettings };
6+
</script>
7+
8+
{#snippet ButtonText()}
9+
Button
10+
{/snippet}
11+
12+
{#snippet ButtonNavText()}
13+
Nav Button
14+
{/snippet}
15+
16+
{#snippet ButtonNavSettings()}
17+
<div class="flex items-center gap-2">
18+
<div class="p-3 bg-gray max-w-max rounded-full object-cover flex items-center" >
19+
<HugeiconsIcon icon={SettingsIcon} size={30} color="var(--color-black-500)" />
20+
</div>
21+
<h1>Settings</h1>
22+
</div>
23+
<HugeiconsIcon size={30} color="var(--color-black-500)" icon={ArrowRight01Icon} />
24+
{/snippet}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { ComponentProps } from "svelte";
2+
import { ButtonText } from "./Button.stories.snippet.svelte";
3+
import ButtonAction from "./ButtonAction.svelte";
4+
5+
export default {
6+
title: "UI/ButtonAction",
7+
component: ButtonAction,
8+
tags: ["autodocs"],
9+
render: (args: {
10+
Component: ButtonAction;
11+
props: ComponentProps<typeof ButtonAction>;
12+
}) => ({
13+
Component: ButtonAction,
14+
props: args,
15+
}),
16+
};
17+
18+
export const Solid = {
19+
args: { variant: "solid", children: ButtonText },
20+
};
21+
22+
export const Soft = {
23+
args: { variant: "soft", children: ButtonText },
24+
};
25+
26+
export const Danger = {
27+
args: { variant: "danger", children: ButtonText },
28+
};
29+
30+
export const DangerSoft = {
31+
args: { variant: "danger-soft", children: ButtonText },
32+
};
33+
34+
export const Loading = {
35+
args: { isLoading: true, children: ButtonText },
36+
};
37+
38+
export const BlockingClick = {
39+
args: {
40+
blockingClick: true,
41+
children: ButtonText,
42+
callback: async () => {
43+
await new Promise((resolve) => setTimeout(resolve, 2000));
44+
},
45+
},
46+
};
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<script lang="ts">
2+
import { cn } from "$lib/utils";
3+
import type { HTMLButtonAttributes } from "svelte/elements";
4+
5+
interface IButtonProps extends HTMLButtonAttributes {
6+
variant?: "solid" | "soft" | "danger" | "danger-soft" | "white";
7+
isLoading?: boolean;
8+
callback?: () => Promise<void> | void;
9+
blockingClick?: boolean;
10+
type?: "button" | "submit" | "reset";
11+
size?: "sm" | "md";
12+
}
13+
14+
const {
15+
variant = "solid",
16+
isLoading,
17+
callback,
18+
onclick,
19+
blockingClick,
20+
type = "button",
21+
size = "md",
22+
children = undefined,
23+
...restProps
24+
}: IButtonProps = $props();
25+
26+
let isSubmitting = $state(false);
27+
const disabled = $derived(restProps.disabled || isLoading || isSubmitting);
28+
29+
const handleClick = async () => {
30+
if (typeof callback !== "function") return;
31+
32+
if (blockingClick) isSubmitting = true;
33+
try {
34+
await callback();
35+
} catch (error) {
36+
console.error("Error in button callback:", error);
37+
} finally {
38+
isSubmitting = false;
39+
}
40+
};
41+
42+
const variantClasses = {
43+
solid: { background: "bg-primary-500", text: "text-white" },
44+
soft: { background: "bg-primary-100", text: "text-primary-500" },
45+
danger: { background: "bg-danger-500", text: "text-white" },
46+
"danger-soft": { background: "bg-danger-100", text: "text-danger-500" },
47+
white: { background: "bg-white", text: "text-black" },
48+
};
49+
50+
const disabledVariantClasses = {
51+
solid: { background: "bg-primary-300", text: "text-white" },
52+
soft: { background: "bg-primary-100", text: "text-primary-300" },
53+
danger: { background: "bg-danger-400", text: "text-white" },
54+
"danger-soft": { background: "bg-danger-100", text: "text-danger-400" },
55+
white: { background: "bg-black-100", text: "text-black-700" },
56+
};
57+
58+
const sizeVariant = {
59+
sm: "px-4 py-1.5 text-base h-11",
60+
md: "px-8 py-2.5 text-xl h-14",
61+
};
62+
63+
const classes = $derived({
64+
common: cn(
65+
"cursor-pointer w-min flex items-center justify-center rounded-full font-semibold duration-100",
66+
sizeVariant[size],
67+
),
68+
background: disabled
69+
? disabledVariantClasses[variant].background ||
70+
variantClasses[variant].background
71+
: variantClasses[variant].background,
72+
text: disabled
73+
? disabledVariantClasses[variant].text || variantClasses[variant].text
74+
: variantClasses[variant].text,
75+
disabled: "cursor-not-allowed",
76+
});
77+
</script>
78+
79+
<button
80+
{...restProps}
81+
class={cn(
82+
[
83+
classes.common,
84+
classes.background,
85+
classes.text,
86+
disabled && classes.disabled,
87+
restProps.class,
88+
].join(" ")
89+
)}
90+
{disabled}
91+
onclick={handleClick}
92+
{type}
93+
>
94+
<div class="relative flex items-center justify-center">
95+
<div
96+
class="flex items-center justify-center duration-100"
97+
class:blur-xs={isLoading || isSubmitting}
98+
>
99+
{@render children?.()}
100+
</div>
101+
{#if isLoading || isSubmitting}
102+
<div
103+
class="loading loading-spinner absolute loading-xl text-white"
104+
></div>
105+
{/if}
106+
</div>
107+
</button>
108+
109+
<!--
110+
@component
111+
export default ButtonAction
112+
@description
113+
This component is a button with a loading spinner that can be used to indicate that an action is being performed.
114+
115+
@props
116+
- variant: The variant of the button. Default is `solid`.
117+
- size: The size of the button. Default is `md`.
118+
- isLoading: A boolean to indicate if the button is in a loading state.
119+
- callback: A callback function that will be called when the button is clicked.
120+
- blockingClick: A boolean to indicate if the button should block the click event while the callback function is being executed.
121+
- icon: A slot for an icon to be displayed inside the button.
122+
- ...restProps: Any other props that can be passed to a button element.
123+
124+
@usage
125+
```html
126+
<script lang="ts">
127+
import * as Button from '$lib/ui/Button'
128+
</script>
129+
130+
<Button.Action variant="solid" callback={() => console.log('clicked')}>
131+
Click me
132+
</Button.Action>
133+
```
134+
-->
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { FlashlightIcon, ViewIcon } 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: ComponentProps<typeof ButtonIcon>) => ({
10+
Component: ButtonIcon,
11+
props: args,
12+
}),
13+
};
14+
15+
export const Default = {
16+
render: () => ({
17+
Component: ButtonIcon,
18+
props: {
19+
ariaLabel: "Default button",
20+
bgSize: "md", // Predefined size
21+
iconSize: "md",
22+
icon: ViewIcon,
23+
bgColor: "black",
24+
iconColor: "white",
25+
},
26+
}),
27+
};
28+
29+
export const CustomSize = {
30+
render: () => ({
31+
Component: ButtonIcon,
32+
props: {
33+
ariaLabel: "Custom sized button",
34+
bgSize: "w-[120px] h-[120px]", // Custom Tailwind size
35+
iconSize: 56, // Custom pixel size
36+
icon: FlashlightIcon,
37+
bgColor: "bg-danger",
38+
iconColor: "white",
39+
},
40+
}),
41+
};
42+
43+
export const Loading = {
44+
render: () => ({
45+
Component: ButtonIcon,
46+
props: {
47+
ariaLabel: "Loading button",
48+
bgSize: "md",
49+
iconSize: "md",
50+
icon: FlashlightIcon,
51+
isLoading: true,
52+
bgColor: "black",
53+
iconColor: "white",
54+
},
55+
}),
56+
};
57+
58+
export const WithCallback = {
59+
render: () => ({
60+
Component: ButtonIcon,
61+
props: {
62+
ariaLabel: "Button with async callback",
63+
bgSize: "md",
64+
iconSize: "md",
65+
icon: FlashlightIcon,
66+
callback: async () => {
67+
await new Promise((resolve) => setTimeout(resolve, 2000));
68+
console.log("Action completed!");
69+
},
70+
blockingClick: true,
71+
bgColor: "primary",
72+
iconColor: "white",
73+
},
74+
}),
75+
};

0 commit comments

Comments
 (0)