oneslash-design-system 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["next/core-web-vitals", "next/typescript"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+ import React, { useEffect } from 'react';
3
+
4
+ export default function Alert({ open, type, message, onClose }: AlertProps) {
5
+ useEffect(() => {
6
+ if (open) {
7
+ const timer = setTimeout(() => {
8
+ onClose();
9
+ }, 3000);
10
+ return () => clearTimeout(timer);
11
+ }
12
+ }, [open, onClose]);
13
+
14
+ if (!open) return null;
15
+
16
+ let bgColor;
17
+ switch (type) {
18
+ case 'error':
19
+ bgColor = 'bg-light-error-main dark:bg-dark-error-main';
20
+ break;
21
+ case 'warning':
22
+ bgColor = 'bg-light-warning-main dark:bg-dark-warning-main';
23
+ break;
24
+ case 'info':
25
+ bgColor = 'bg-light-info-main dark:bg-dark-info-main';
26
+ break;
27
+ case 'success':
28
+ bgColor = 'bg-light-success-main dark:bg-dark-success-main';
29
+ break;
30
+ }
31
+
32
+ return (
33
+ <div className="fixed top-4 inset-x-0 z-50 flex justify-center">
34
+ <div className={`flex items-center justify-between w-full max-w-md p-4 rounded-[8px] shadow-lg text-light-text-contrast dark:text-dark-text-contrast ${bgColor}`}>
35
+ <span>{message}</span>
36
+ <button onClick={onClose} className="ml-4 text-xl font-bold">
37
+ &times;
38
+ </button>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+
4
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
5
+
6
+ export default function Button({
7
+ size,
8
+ type,
9
+ state,
10
+ label,
11
+ iconLeftName,
12
+ iconRightName,
13
+ onClick,
14
+ }: ButtonProps) {
15
+
16
+ const [isHovered, setIsHovered] = useState(false);
17
+ const [IconLeft, setIconLeft] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
18
+ const [IconRight, setIconRight] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
19
+
20
+ // import icon
21
+ const loadIcon = useCallback(async (iconName?: string) => {
22
+ if (!iconName) return null;
23
+ try {
24
+ const module = await import('@heroicons/react/24/outline');
25
+ const Icon = module[iconName as keyof typeof module] as IconType;
26
+ return Icon || null;
27
+ } catch (error) {
28
+ console.error(`Failed to load icon ${iconName}:`, error);
29
+ return null;
30
+ }
31
+ }, []);
32
+
33
+ // Load icons on mount and when props change
34
+ useEffect(() => {
35
+ const fetchIcons = async () => {
36
+ if (typeof iconLeftName === 'string') {
37
+ setIconLeft(await loadIcon(iconLeftName));
38
+ }
39
+ if (typeof iconRightName === 'string') {
40
+ setIconRight(await loadIcon(iconRightName));
41
+ }
42
+ };
43
+ fetchIcons();
44
+ }, [iconLeftName, iconRightName, loadIcon]);
45
+
46
+ // size
47
+ const sizeClasses =
48
+ size === 'large' ? 'text-body1 p-2'
49
+ : size === 'small' ? 'text-body2 p-1'
50
+ : 'text-body1 p-1'; // medium size
51
+
52
+ // icon size
53
+ const sizeIcon =
54
+ size === 'large' ? 'w-6 h-6'
55
+ : size === 'small' ? 'w-4 h-4'
56
+ : 'w-5 h-5'; // medium size
57
+
58
+ // type
59
+ const typeClasses = {
60
+ primary: 'bg-light-accent-main dark:bg-dark-accent-main text-light-text-primary dark:text-dark-text-contrast',
61
+ secondary: 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast',
62
+ tertiary: 'bg-light-background-accent200 dark:bg-dark-background-accent200 text-light-text-primary dark:text-dark-text-primary',
63
+ textOnly: 'text-light-text-primary dark:text-dark-text-primary',
64
+ }[type];
65
+
66
+ const hoverTypeClasses = {
67
+ primary: ' hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark',
68
+ secondary: 'hover:bg-light-primary-dark dark:hover:bg-dark-primary-dark',
69
+ tertiary: 'hover:bg-light-background-accent300 hover:dark:bg-dark-background-accent300',
70
+ textOnly: 'hover:bg-light-background-accent100 hover:dark:bg-dark-background-accent100',
71
+ }[type];
72
+
73
+ // state
74
+ const stateClasses = {
75
+ enabled: 'cursor-pointer',
76
+ hovered: 'cursor-pointer',
77
+ selected: 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast',
78
+ focused: 'ring-2 ring-light-accent-main dark:ring-dark-accent-main',
79
+ disabled: 'text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
80
+ }[state];
81
+
82
+
83
+
84
+ return (
85
+ <button
86
+ className={`flex flex-row space-x-2 items-center rounded-[8px]
87
+ ${sizeClasses}
88
+ ${typeClasses}
89
+ ${state !== 'disabled' && isHovered ? hoverTypeClasses : ''}
90
+ ${stateClasses}`}
91
+ disabled={state === 'disabled'}
92
+ onMouseEnter={() => {
93
+ if (state !== 'disabled') setIsHovered(true);
94
+ }}
95
+ onMouseLeave={() => {
96
+ if (state !== 'disabled') setIsHovered(false);
97
+ }}
98
+ onClick={onClick}
99
+ >
100
+ {IconLeft && <IconLeft className={sizeIcon} />}
101
+ <div className="whitespace-nowrap px-2">
102
+ {label}
103
+ </div>
104
+ {IconRight && <IconRight className={sizeIcon} />}
105
+ </button>
106
+ );
107
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+ import React, { useState } from 'react';
3
+
4
+
5
+
6
+ export default function Checkbox({
7
+ label,
8
+ checked = false,
9
+ onChange
10
+ }: CheckboxProps) {
11
+
12
+ const [isChecked, setIsChecked] = useState(checked);
13
+
14
+ const handleToggle = () => {
15
+ const newChecked = !isChecked;
16
+ setIsChecked(newChecked);
17
+ if (onChange) {
18
+ onChange(newChecked);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <label className="flex items-center cursor-pointer">
24
+ <div
25
+ onClick={handleToggle}
26
+ className="relative flex items-center justify-center w-6 h-6 group"
27
+ >
28
+ {/* Circle behind the checkbox */}
29
+ <div
30
+ className="absolute w-6 h-6 rounded-full group-hover:bg-light-action-selected dark:group-hover:bg-dark-action-selected transition-all"
31
+ ></div>
32
+
33
+ {/* Checkbox */}
34
+ <div
35
+ className={`relative z-10 w-4 h-4 border-2 rounded ${
36
+ isChecked
37
+ ? 'bg-light-accent-main dark:bg-dark-accent-main border-none'
38
+ : 'border-light-text-secondary dark:border-dark-text-secondary'
39
+ } flex items-center justify-center transition-colors`}
40
+ >
41
+ {isChecked && (
42
+ <svg
43
+ className="w-3 h-3 text-light-text-contrast dark:text-dark-text-contrast"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ viewBox="0 0 12 12"
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ >
49
+ <path strokeWidth="2" d="M1 6l4 3 6-7" />
50
+ </svg>
51
+ )}
52
+ </div>
53
+ </div>
54
+ {label && <span className="ml-2">{label}</span>}
55
+ </label>
56
+ );
57
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+
4
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
5
+
6
+ export default function IconButton({
7
+ variant,
8
+ color,
9
+ state,
10
+ iconName,
11
+ onClick,
12
+ }: IconButtonProps) {
13
+
14
+ const [isHovered, setIsHovered] = useState(false);
15
+ const [Icon, setIcon] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
16
+
17
+ // import icon
18
+ const loadIcon = useCallback(async (iconName?: string) => {
19
+ if (!iconName) return null;
20
+ try {
21
+ const module = await import('@heroicons/react/24/outline');
22
+ const Icon = module[iconName as keyof typeof module] as IconType;
23
+ return Icon || null;
24
+ } catch (error) {
25
+ console.error(`Failed to load icon ${iconName}:`, error);
26
+ return null;
27
+ }
28
+ }, []);
29
+
30
+ // Load icons on mount and when props change
31
+ useEffect(() => {
32
+ const fetchIcons = async () => {
33
+ if (typeof iconName === 'string') {
34
+ setIcon(await loadIcon(iconName));
35
+ }
36
+ };
37
+ fetchIcons();
38
+ }, [iconName, loadIcon]);
39
+
40
+
41
+ // padding, corner
42
+ const baseClasses = variant === 'contained'
43
+ ? 'p-2 rounded-[8px] leading-none '
44
+ : 'p-2 rounded-[8px] leading-none ';
45
+
46
+ // bg color
47
+ const bgColor = variant === 'contained'
48
+ ? color === 'primary'
49
+ ? 'bg-light-primary-main dark:bg-dark-primary-main' // contained && primary
50
+ : 'bg-light-background-accent200 dark:bg-dark-background-accent200' // contained && secondary
51
+ : color === 'primary'
52
+ ? ' ' // textOnly && primary
53
+ : ' '; // textOnly && secondary
54
+
55
+ // bg color hover
56
+ const bgColorHover = variant === 'contained'
57
+ ? color === 'primary'
58
+ ? 'hover:bg-light-primary-dark hover:dark:bg-dark-primary-dark' // contained && primary
59
+ : 'hover:bg-light-background-accent300 hover:dark:bg-dark-background-accent300' // contained && secondary
60
+ : color === 'primary'
61
+ ? 'hover:bg-light-action-hover hover:dark:bg-dark-action-hover' // textOnly && primary
62
+ : 'hover:bg-light-action-hover hover:dark:bg-dark-action-hover'; // textOnly && secondary
63
+
64
+ // icon color
65
+ const iconColor = variant === 'contained'
66
+ ? color === 'primary'
67
+ ? 'text-light-primary-contrast dark:text-dark-primary-contrast' // contained && primary
68
+ : 'text-light-text-primary dark:text-dark-text-primary' // contained && secondary
69
+ : color === 'primary'
70
+ ? ' text-light-text-primary dark:text-dark-text-primary' // textOnly && primary
71
+ : ' text-light-text-primary dark:text-dark-text-primary'; // textOnly && secondary
72
+
73
+
74
+ // state
75
+ const stateClasses = state === 'disabled'
76
+ ? 'cursor-not-allowed opacity-50'
77
+ : state === 'selected'
78
+ ? 'cursor-pointer ring-2 ring-offset-2 ring-blue-500'
79
+ : isHovered
80
+ ? 'cursor-pointer hover:bg-opacity-75'
81
+ : 'cursor-pointer';
82
+
83
+
84
+
85
+ return (
86
+ <button
87
+ className={`${baseClasses} ${bgColor} ${iconColor} ${bgColorHover} ${stateClasses} transition-colors duration-300 ease-in-out`}
88
+ disabled={state === 'disabled'}
89
+ onMouseEnter={() => setIsHovered(true)}
90
+ onMouseLeave={() => setIsHovered(false)}
91
+ onClick={onClick}
92
+ >
93
+ {Icon && <Icon className="size-6" />}
94
+ </button>
95
+ );
96
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+ import NextLink from 'next/link';
4
+
5
+ export default function MenuItem({
6
+ href = '#',
7
+ iconName,
8
+ label,
9
+ isSelected,
10
+ onClick, }
11
+ : MenuItemProps) {
12
+
13
+ const [Icon, setIcon] = useState<IconType | null>(null);
14
+
15
+ // Import icon dynamically
16
+ const loadIcon = useCallback(async (iconName?: string) => {
17
+ if (!iconName) return null;
18
+ try {
19
+ const module = await import('@heroicons/react/24/outline');
20
+ const IconComponent = module[iconName as keyof typeof module] as IconType;
21
+ return IconComponent || null;
22
+ } catch (error) {
23
+ console.error(`Failed to load icon ${iconName}:`, error);
24
+ return null;
25
+ }
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ const fetchIcon = async () => {
30
+ if (iconName) {
31
+ setIcon(await loadIcon(iconName));
32
+ }
33
+ };
34
+ fetchIcon();
35
+ }, [iconName, loadIcon]);
36
+
37
+
38
+ return (
39
+ <NextLink href={href}>
40
+ <div
41
+ className={`
42
+ flex items-center space-x-2 p-2 rounded-[8px] cursor-pointer justify-start
43
+ ${isSelected
44
+ ? 'bg-light-action-selected dark:bg-dark-action-selected'
45
+ : 'hover:bg-light-action-hover dark:hover:bg-dark-action-hover'}
46
+ `}
47
+ style={{ width: '100%' }}
48
+ onClick={onClick}
49
+ >
50
+ {Icon && <Icon className="w-5 h-5" />}
51
+ <span className="whitespace-nowrap text-body1">
52
+ {label}
53
+ </span>
54
+ </div>
55
+ </NextLink>
56
+ );
57
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import React from 'react';
3
+
4
+ export default function Modal({
5
+ isOpen,
6
+ title,
7
+ children,
8
+ onClose,
9
+ actions,
10
+ }: ModalProps) {
11
+
12
+ if (!isOpen) return null;
13
+
14
+ const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
15
+ if (e.target === e.currentTarget) {
16
+ onClose();
17
+ }
18
+ };
19
+
20
+ return (
21
+ <div className="fixed inset-[-32px] bg-black bg-opacity-50 flex items-center justify-center z-50 "
22
+ onClick={handleOverlayClick}
23
+ >
24
+ <div className="bg-light-background-default dark:bg-dark-background-default p-6 rounded-[8px] space-y-4 w-[600px]"
25
+ style={{
26
+ position: 'relative',
27
+ margin: '0 auto'
28
+ }}>
29
+ <h2 className="text-h6">
30
+ {title}
31
+ </h2>
32
+ <div className="text-body1 space-y-4">
33
+ {children}
34
+ </div>
35
+ <div className="flex justify-between">
36
+ {actions}
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+ import React, { useState, useRef, useEffect } from 'react';
3
+
4
+ export default function Popover({ anchorEl, open, onClose, children }: PopoverProps) {
5
+ const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
6
+ const popoverRef = useRef<HTMLDivElement>(null);
7
+
8
+ // Close popover when clicking outside
9
+ useEffect(() => {
10
+ const handleClickOutside = (event: MouseEvent) => {
11
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node) && anchorEl) {
12
+ onClose();
13
+ }
14
+ };
15
+ if (open) {
16
+ document.addEventListener('mousedown', handleClickOutside);
17
+ }
18
+ return () => {
19
+ document.removeEventListener('mousedown', handleClickOutside);
20
+ };
21
+ }, [open, anchorEl, onClose]);
22
+
23
+ // Calculate position
24
+ useEffect(() => {
25
+ if (anchorEl && open) {
26
+ const anchorRect = anchorEl.getBoundingClientRect();
27
+ const popoverRect = popoverRef.current?.getBoundingClientRect();
28
+ if (popoverRect) {
29
+ const spaceBelow = window.innerHeight - anchorRect.bottom;
30
+ const spaceAbove = anchorRect.top;
31
+
32
+ // Determine if we should place the popover above
33
+ const shouldPlaceAbove = spaceBelow < popoverRect.height + 8 && spaceAbove > popoverRect.height + 8;
34
+
35
+ // Calculate top position, correcting for popover height when placing above
36
+ const topPosition = shouldPlaceAbove
37
+ ? anchorRect.top + window.scrollY - popoverRect.height - 8 // Adjust by 8px to maintain spacing
38
+ : anchorRect.bottom + window.scrollY + 8; // Add 8px for spacing below
39
+
40
+ // Calculate left position with viewport boundary checks
41
+ let leftPosition = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
42
+
43
+ if (leftPosition < 8) {
44
+ // Adjust to align with the left boundary of the viewport
45
+ leftPosition = 8;
46
+ } else if (leftPosition + popoverRect.width > window.innerWidth - 8) {
47
+ // Adjust to align with the right boundary of the viewport
48
+ leftPosition = window.innerWidth - popoverRect.width - 8;
49
+ }
50
+
51
+ setPopoverStyle({
52
+ position: 'absolute',
53
+ top: topPosition,
54
+ left: leftPosition,
55
+ zIndex: 10000,
56
+ pointerEvents: 'auto',
57
+ });
58
+ }
59
+ }
60
+ }, [anchorEl, open]);
61
+
62
+ if (!open || !anchorEl) return null;
63
+
64
+ return (
65
+ <div
66
+ ref={popoverRef}
67
+ style={popoverStyle}
68
+ className="bg-light-background-default dark:bg-dark-background-default border rounded-[8px] shadow-lg"
69
+ role="dialog"
70
+ >
71
+ {children}
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+ import { useRouter, usePathname } from 'next/navigation';
4
+
5
+
6
+ const TabIcon: React.FC<{ iconName?: string }> = ({ iconName }) => {
7
+ const [Icon, setIcon] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
8
+
9
+ const loadIcon = useCallback(async (iconName?: string) => {
10
+ if (!iconName) return null;
11
+ try {
12
+ const module = await import('@heroicons/react/24/outline');
13
+ const IconComponent = module[iconName as keyof typeof module] as React.ComponentType<React.SVGProps<SVGSVGElement>>;
14
+ return IconComponent || null;
15
+ } catch (error) {
16
+ console.error(`Failed to load icon ${iconName}:`, error);
17
+ return null;
18
+ }
19
+ }, []);
20
+
21
+ useEffect(() => {
22
+ const fetchIcon = async () => {
23
+ if (iconName) {
24
+ setIcon(await loadIcon(iconName));
25
+ }
26
+ };
27
+ fetchIcon();
28
+ }, [iconName, loadIcon]);
29
+
30
+ return Icon ? <Icon className="w-5 h-5" /> : null;
31
+ };
32
+
33
+ const Tab: React.FC<TabProps> = ({ label, href, isSelected, onClick, iconName }) => {
34
+ const router = useRouter();
35
+ const pathname = usePathname();
36
+
37
+ const handleClick = () => {
38
+ onClick();
39
+ if (href) {
40
+ router.push(href);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div
46
+ className={`
47
+ flex items-center space-x-2 p-2 rounded-[8px] cursor-pointer justify-start
48
+ ${isSelected
49
+ ? 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast'
50
+ : 'bg-light-action-selected dark:bg-dark-action-selected hover:bg-light-background-default dark:hover:bg-dark-action-hover'}
51
+ `}
52
+ onClick={handleClick}
53
+ >
54
+ {iconName && <TabIcon iconName={iconName} />}
55
+ <span className="whitespace-nowrap text-body1">
56
+ {label}
57
+ </span>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default Tab;
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+
4
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
5
+
6
+ export default function Tag({
7
+ key,
8
+ variant,
9
+ size,
10
+ state,
11
+ label,
12
+ iconName,
13
+ isDeletable,
14
+ onClick,
15
+ color = 'default',
16
+ }: TagProps) {
17
+ const [isHovered, setIsHovered] = useState(false);
18
+ const [Icon, setIcon] = useState<IconType | null>(null);
19
+ const [DeleteIcon, setDeleteIcon] = useState<IconType | null>(null);
20
+
21
+ const loadIcon = useCallback(async (iconName?: string) => {
22
+ if (!iconName) return null;
23
+ try {
24
+ const module = await import('@heroicons/react/24/outline');
25
+ const Icon = module[iconName as keyof typeof module] as IconType;
26
+ return Icon || null;
27
+ } catch (error) {
28
+ console.error(`Failed to load icon ${iconName}:`, error);
29
+ return null;
30
+ }
31
+ }, []);
32
+
33
+ useEffect(() => {
34
+ const fetchIcons = async () => {
35
+ if (typeof iconName === 'string') {
36
+ setIcon(await loadIcon(iconName));
37
+ }
38
+ if (typeof isDeletable === 'string') {
39
+ setDeleteIcon(await loadIcon(isDeletable));
40
+ }
41
+ };
42
+ fetchIcons();
43
+ }, [iconName, isDeletable, loadIcon]);
44
+
45
+ // size and padding
46
+ const sizeClasses = size === 'medium' ? 'text-body2 px-2 py-1' : 'text-caption px-2 py-[3px]';
47
+
48
+
49
+ // bg color
50
+ const bgClasses = variant === 'contained'
51
+ ? (color === 'info'
52
+ ? 'bg-light-info-main dark:bg-dark-info-main' // info
53
+ : 'bg-light-background-accent200 dark:bg-dark-background-accent200') // default
54
+ : ''; // textOnly
55
+
56
+ // font color
57
+ const fontClasses = variant === 'textOnly'
58
+ ? (color === 'info'
59
+ ? 'text-light-info-main dark:text-dark-info-main' // info
60
+ : 'text-light-text-primary dark:text-dark-text-primary') // default
61
+ : 'text-light-text-primary dark:text-dark-text-primary'; // contained
62
+
63
+ // state
64
+ const stateClasses = state === 'selected'
65
+ ? 'bg-light-accent-main dark:bg-dark-accent-main text-white'
66
+ : 'cursor-pointer';
67
+
68
+ // hover
69
+ const hoverClasses = isHovered ? (variant === 'contained' ? 'hover:bg-dark-background-accent300' : '') : '';
70
+
71
+ return (
72
+ <div
73
+ className={`flex items-center space-x-1 rounded-full
74
+ ${sizeClasses} ${bgClasses} ${fontClasses} ${stateClasses} ${hoverClasses}
75
+ transition-colors duration-300 ease-in-out`}
76
+ onMouseEnter={() => setIsHovered(true)}
77
+ onMouseLeave={() => setIsHovered(false)}
78
+ onClick={onClick}
79
+ >
80
+ {Icon && <Icon className="w-4 h-4" />}
81
+ <span>{label}</span>
82
+ {isDeletable && (
83
+ <button
84
+ className="ml-2 text-red-500"
85
+ onClick={(e) => {
86
+ e.stopPropagation();
87
+ // Handle delete action
88
+ }}
89
+ >
90
+ {DeleteIcon && <DeleteIcon className="w-4 h-4" />}
91
+ </button>
92
+ )}
93
+ </div>
94
+ );
95
+ }