oneslash-design-system 1.2.11 → 1.2.13

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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.eslintrc.json +3 -0
  3. package/components/alert.tsx +132 -0
  4. package/components/button.tsx +120 -0
  5. package/components/checkBox.tsx +60 -0
  6. package/components/emptyBox.tsx +33 -0
  7. package/components/iconButton.tsx +103 -0
  8. package/components/loadingScreen.tsx +30 -0
  9. package/components/menu.tsx +35 -0
  10. package/components/menuItem.tsx +117 -0
  11. package/components/modal.tsx +85 -0
  12. package/components/navigation.tsx +27 -0
  13. package/components/popover.tsx +69 -0
  14. package/components/radioGroup.tsx +50 -0
  15. package/components/select.tsx +253 -0
  16. package/components/tab.tsx +85 -0
  17. package/components/tableCell.tsx +15 -0
  18. package/components/tableContainer.tsx +15 -0
  19. package/components/tableHeader.tsx +15 -0
  20. package/components/tableHeaderCell.tsx +15 -0
  21. package/components/tableRow.tsx +15 -0
  22. package/components/tabsContainer.tsx +23 -0
  23. package/components/tag.tsx +81 -0
  24. package/components/textField.tsx +116 -0
  25. package/components/textarea.tsx +120 -0
  26. package/components/timeStamp.tsx +65 -0
  27. package/components/tooltip.tsx +66 -0
  28. package/components/userImage.tsx +64 -0
  29. package/designTokens.js +234 -0
  30. package/dist/components/loadingScreen.jsx +2 -2
  31. package/dist/components/menuItem.d.ts +2 -8
  32. package/dist/components/menuItem.jsx +14 -34
  33. package/dist/components/tag.d.ts +2 -1
  34. package/dist/components/tag.jsx +22 -22
  35. package/index.css +8 -0
  36. package/index.ts +21 -0
  37. package/next.config.mjs +4 -0
  38. package/package.json +4 -28
  39. package/postcss.config.mjs +8 -0
  40. package/tailwind.config.ts +232 -0
  41. package/tsconfig.json +37 -0
  42. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
package/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["next/core-web-vitals", "next/typescript"]
3
+ }
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+ import React, { useEffect, useState } from 'react';
3
+ import {
4
+ InformationCircleIcon,
5
+ ExclamationCircleIcon,
6
+ ExclamationTriangleIcon,
7
+ CheckCircleIcon
8
+ } from '@heroicons/react/24/outline';
9
+ import IconButton from './iconButton';
10
+
11
+ interface AlertProps {
12
+ open?: boolean;
13
+ type: 'success' | 'warning' | 'error' | 'info' | 'default';
14
+ message: string;
15
+ onClose: () => void;
16
+ showCloseButton?: boolean;
17
+ }
18
+
19
+ export default function Alert({
20
+ open,
21
+ type,
22
+ message,
23
+ onClose,
24
+ showCloseButton = false
25
+ }: AlertProps) {
26
+ const [isVisible, setIsVisible] = useState(false);
27
+ const [shouldRender, setShouldRender] = useState(false);
28
+
29
+ useEffect(() => {
30
+ if (open) {
31
+ setShouldRender(true);
32
+ // Small delay to trigger the animation
33
+ const showTimer = setTimeout(() => setIsVisible(true), 10);
34
+
35
+ // Only auto-dismiss if there's no close button
36
+ if (!showCloseButton) {
37
+ const dismissTimer = setTimeout(() => {
38
+ handleClose();
39
+ }, 5000);
40
+ return () => {
41
+ clearTimeout(showTimer);
42
+ clearTimeout(dismissTimer);
43
+ };
44
+ }
45
+
46
+ return () => clearTimeout(showTimer);
47
+ } else if (shouldRender) {
48
+ // When open becomes false, trigger fade out
49
+ setIsVisible(false);
50
+ const hideTimer = setTimeout(() => {
51
+ setShouldRender(false);
52
+ }, 300);
53
+ return () => clearTimeout(hideTimer);
54
+ }
55
+ }, [open, showCloseButton, shouldRender]);
56
+
57
+ const handleClose = () => {
58
+ setIsVisible(false);
59
+ // Wait for animation to complete before unmounting
60
+ setTimeout(() => {
61
+ setShouldRender(false);
62
+ onClose();
63
+ }, 300);
64
+ };
65
+
66
+ if (!shouldRender) return null;
67
+
68
+ // Get the appropriate icon based on type
69
+ const getIcon = () => {
70
+ switch (type) {
71
+ case 'error':
72
+ return <ExclamationCircleIcon className="w-6 h-6" />;
73
+ case 'warning':
74
+ return <ExclamationTriangleIcon className="w-6 h-6" />;
75
+ case 'info':
76
+ return <InformationCircleIcon className="w-6 h-6" />;
77
+ case 'success':
78
+ return <CheckCircleIcon className="w-6 h-6" />;
79
+ case 'default':
80
+ default:
81
+ return <InformationCircleIcon className="w-6 h-6" />;
82
+ }
83
+ };
84
+
85
+ let bgColor;
86
+ switch (type) {
87
+ case 'error':
88
+ bgColor = 'bg-light-error-main dark:bg-dark-error-main';
89
+ break;
90
+ case 'warning':
91
+ bgColor = 'bg-light-warning-main dark:bg-dark-warning-main';
92
+ break;
93
+ case 'info':
94
+ bgColor = 'bg-light-info-main dark:bg-dark-info-main';
95
+ break;
96
+ case 'success':
97
+ bgColor = 'bg-light-success-main dark:bg-dark-success-main';
98
+ break;
99
+ case 'default':
100
+ default:
101
+ bgColor = 'bg-light-secondary-light dark:bg-dark-secondary-light';
102
+ break;
103
+ }
104
+
105
+ return (
106
+ <div className="fixed top-0 inset-x-0 z-50 flex justify-center pointer-events-none">
107
+ <div
108
+ className={`flex items-start justify-between w-full max-w-md p-2 rounded-[8px] pointer-events-auto transition-opacity duration-200 ease-out ${bgColor} ${
109
+ isVisible
110
+ ? 'opacity-100'
111
+ : 'opacity-0'
112
+ }`}
113
+ >
114
+ <div className="flex items-start gap-2 text-light-text-primary dark:text-dark-text-primary">
115
+ {getIcon()}
116
+ <span className="body1">{message}</span>
117
+ </div>
118
+ {showCloseButton && (
119
+ <div className="ml-4">
120
+ <IconButton
121
+ color="iconOnly"
122
+ state="enabled"
123
+ size="small"
124
+ iconName="XMarkIcon"
125
+ onClick={handleClose}
126
+ />
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,120 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback } from 'react';
3
+
4
+ interface ButtonProps {
5
+ size: 'small' | 'medium' | 'large';
6
+ type: 'primary' | 'secondary' | 'tertiary' | 'textOnly';
7
+ state: 'enabled' | 'hovered' | 'focused' | 'disabled';
8
+ label: string;
9
+ decoIcon?: string;
10
+ actionIcon?: string;
11
+ onClickButton?: any;
12
+ onClickActionIcon?: () => void;
13
+ className?: string;
14
+ }
15
+
16
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
17
+
18
+ export default function Button({
19
+ size,
20
+ type,
21
+ state,
22
+ label,
23
+ decoIcon,
24
+ actionIcon,
25
+ onClickButton,
26
+ onClickActionIcon,
27
+ className = '',
28
+ }: ButtonProps) {
29
+ const [isHovered, setIsHovered] = useState(false);
30
+ const [IconLeft, setIconLeft] = useState<IconType | null>(null);
31
+ const [IconRight, setIconRight] = useState<IconType | null>(null);
32
+
33
+ const loadIcon = useCallback(async (iconName?: string) => {
34
+ if (!iconName) return null;
35
+ try {
36
+ const module = await import('@heroicons/react/24/outline');
37
+ const Icon = module[iconName as keyof typeof module] as IconType;
38
+ return Icon || null;
39
+ } catch (error) {
40
+ console.error(`Failed to load icon ${iconName}:`, error);
41
+ return null;
42
+ }
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ const fetchIcons = async () => {
47
+ if (decoIcon) {
48
+ setIconLeft(await loadIcon(decoIcon));
49
+ }
50
+ if (actionIcon) {
51
+ setIconRight(await loadIcon(actionIcon));
52
+ }
53
+ };
54
+ fetchIcons();
55
+ }, [decoIcon, actionIcon, loadIcon]);
56
+
57
+ const sizeClasses = {
58
+ large: 'text-body1 p-2',
59
+ medium: 'text-body1 p-1',
60
+ small: 'text-body2 p-1',
61
+ }[size];
62
+
63
+ const sizeIcon = {
64
+ large: 'w-6 h-6',
65
+ medium: 'w-5 h-5',
66
+ small: 'w-4 h-4',
67
+ }[size];
68
+
69
+ const baseTypeClasses = {
70
+ primary: 'bg-light-accent-main dark:bg-dark-accent-main text-light-text-primary dark:text-dark-text-contrast',
71
+ secondary: 'bg-light-secondary-main dark:bg-dark-secondary-main text-light-text-primary dark:text-dark-text-primary',
72
+ tertiary: 'bg-light-background-accent100 dark:bg-dark-background-accent100 text-light-text-primary dark:text-dark-text-primary',
73
+ textOnly: 'text-light-text-primary dark:text-dark-text-primary',
74
+ }[type];
75
+
76
+ const hoverTypeClasses = {
77
+ primary: 'hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark',
78
+ secondary: 'hover:bg-light-secondary-dark dark:hover:bg-dark-secondary-dark',
79
+ tertiary: 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200',
80
+ textOnly: 'hover:bg-light-background-accent100 hover:dark:bg-dark-background-accent100',
81
+ }[type];
82
+
83
+ const stateClasses = {
84
+ enabled: 'cursor-pointer',
85
+ focused: 'ring-2 ring-offset-4 ring-offset-light-background-default dark:ring-offset-dark-background-default ring-light-accent-main dark:ring-dark-accent-main',
86
+ disabled: type === 'textOnly'
87
+ ? 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-transparent'
88
+ : 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
89
+ };
90
+
91
+ const buttonClasses = `
92
+ flex flex-row items-center rounded-[8px] transition-colors duration-200 ease-in-out justify-between
93
+ ${sizeClasses}
94
+ ${state === 'enabled' ? baseTypeClasses : ''}
95
+ ${state === 'focused' ? stateClasses.focused : ''}
96
+ ${state === 'disabled' ? stateClasses.disabled : baseTypeClasses}
97
+ ${state !== 'disabled' && isHovered ? hoverTypeClasses : ''}
98
+ ${className}
99
+ `;
100
+
101
+ return (
102
+ <button
103
+ className={buttonClasses}
104
+ onMouseEnter={() => { if (state !== 'disabled') setIsHovered(true); }}
105
+ onMouseLeave={() => { if (state !== 'disabled') setIsHovered(false); }}
106
+ onClick={onClickButton}
107
+ >
108
+ {/* Group IconLeft and label in a flex container for left alignment */}
109
+ <div className="flex items-center">
110
+ {IconLeft && <IconLeft className={sizeIcon} />}
111
+ <div className="whitespace-nowrap overflow-hidden truncate px-2">{label}</div>
112
+ </div>
113
+ {IconRight && (
114
+ <div onClick={onClickActionIcon} className="cursor-pointer">
115
+ <IconRight className={sizeIcon} />
116
+ </div>
117
+ )}
118
+ </button>
119
+ );
120
+ }
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+
4
+ interface CheckboxProps {
5
+ label?: string;
6
+ checked?: boolean;
7
+ onChange?: (checked: boolean) => void;
8
+ }
9
+
10
+ export default function Checkbox({
11
+ label,
12
+ checked = false,
13
+ onChange
14
+ }: CheckboxProps) {
15
+ const [isChecked, setIsChecked] = useState(checked);
16
+
17
+ const handleToggle = () => {
18
+ const newChecked = !isChecked;
19
+ setIsChecked(newChecked);
20
+ if (onChange) {
21
+ onChange(newChecked);
22
+ }
23
+ };
24
+
25
+ return (
26
+ <label className="flex items-center cursor-pointer">
27
+ <div
28
+ onClick={handleToggle}
29
+ className="relative flex items-center justify-center w-6 h-6 group transition-colors duration-200 ease-in-out"
30
+ >
31
+ {/* Circle behind the checkbox */}
32
+ <div
33
+ className="absolute w-6 h-6 rounded-full group-hover:bg-light-action-selected dark:group-hover:bg-dark-action-selected"
34
+ ></div>
35
+
36
+ {/* Checkbox */}
37
+ <div
38
+ className={`relative z-10 w-4 h-4 border-2 rounded ${
39
+ isChecked
40
+ ? 'bg-light-text-primary dark:bg-dark-text-primary border-none'
41
+ : 'border-light-text-secondary dark:border-dark-text-secondary'
42
+ } flex items-center justify-center`}
43
+ >
44
+ {isChecked && (
45
+ <svg
46
+ className="w-3 h-3 text-light-text-contrast dark:text-dark-text-contrast"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ viewBox="0 0 12 12"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ >
52
+ <path strokeWidth="2" d="M1 6l4 3 6-7" />
53
+ </svg>
54
+ )}
55
+ </div>
56
+ </div>
57
+ {label && <span className="ml-2 text-body1 text-light-text-primary dark:text-dark-text-primary">{label}</span>}
58
+ </label>
59
+ );
60
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+ import React from 'react';
3
+ import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
4
+
5
+ interface EmptyBoxProps{
6
+ text: string;
7
+ size: "small" | "large";
8
+ }
9
+
10
+ export default function EmptyBox ({
11
+ text,
12
+ size,
13
+ }: EmptyBoxProps) {
14
+
15
+ const color = 'text-light-text-disabled dark:text-dark-text-disabled';
16
+
17
+ const height = size === 'small'
18
+ ? 'py-6'
19
+ : 'h-full';
20
+
21
+ const iconSize = 'size-6' ;
22
+
23
+ return (
24
+ <div className={`flex flex-col space-y-2 justify-center items-center w-full ${height}`}>
25
+ <ExclamationCircleIcon
26
+ className={`${iconSize} ${color}`}
27
+ />
28
+ <p className={`text-body1 ${color}`} text-center>
29
+ {text}
30
+ </p>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+ import React, { useState } from 'react';
3
+ import * as HeroIcons24 from '@heroicons/react/24/outline';
4
+ import * as HeroIcons20 from '@heroicons/react/20/solid';
5
+ import { LoadingSmall } from './loadingScreen';
6
+
7
+ interface IconButtonProps {
8
+ color: "primary" | "secondary" | "tertiary" | "iconOnly";
9
+ state: "enabled" | "selected" | "disabled";
10
+ size: "large" | "medium" | "small";
11
+ iconName: keyof typeof HeroIcons24 & keyof typeof HeroIcons20;
12
+ onClick?: any;
13
+ loading?: boolean;
14
+ }
15
+
16
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
17
+
18
+ export default function IconButton({
19
+ color,
20
+ state,
21
+ size,
22
+ iconName,
23
+ onClick,
24
+ loading = false, // Default to false
25
+ }: IconButtonProps) {
26
+ const [isHovered, setIsHovered] = useState(false);
27
+
28
+ // Select icon based on size
29
+ const Icon: IconType = size === 'small'
30
+ ? HeroIcons20[iconName]
31
+ : HeroIcons24[iconName];
32
+
33
+ // Size-based classes
34
+ const sizeClasses = {
35
+ large: 'p-2', // 8px padding
36
+ medium: 'p-1', // 4px padding
37
+ small: 'p-1', // 4px padding
38
+ };
39
+
40
+ const iconSizeClasses = {
41
+ large: 'size-6', // 24px
42
+ medium: 'size-6', // 24px
43
+ small: 'size-5', // 20px
44
+ };
45
+
46
+ // Base classes (padding and corner radius)
47
+ const baseClasses = `${sizeClasses[size]} rounded-[8px] leading-none relative`;
48
+
49
+ // Background color
50
+ const bgColor = color === 'primary'
51
+ ? 'bg-light-accent-main dark:bg-dark-accent-main' // Primary
52
+ : color === 'secondary'
53
+ ? 'bg-light-secondary-main dark:bg-dark-secondary-main' // Secondary
54
+ : color === 'tertiary'
55
+ ? 'bg-light-background-accent100 dark:bg-dark-background-accent100' // Tertiary
56
+ : ' '; // iconOnly: none
57
+
58
+ // Background hover color
59
+ const bgColorHover = color === 'primary'
60
+ ? 'hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark' // Primary
61
+ : color === 'secondary'
62
+ ? 'hover:bg-light-secondary-dark hover:dark:bg-dark-secondary-dark' // Secondary
63
+ : color === 'tertiary'
64
+ ? 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200' // Tertiary
65
+ : 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200'; // iconOnly
66
+
67
+ // Icon color
68
+ const iconColor = color === 'primary'
69
+ ? 'text-light-text-primary dark:text-dark-text-contrast' // Primary
70
+ : color === 'secondary'
71
+ ? 'text-light-text-primary dark:text-dark-text-primary' // Secondary
72
+ : color === 'tertiary'
73
+ ? 'text-light-text-primary dark:text-dark-text-primary' // Tertiary
74
+ : 'text-light-text-primary dark:text-dark-text-primary'; // iconOnly
75
+
76
+ // State classes, including loading
77
+ const stateClasses = loading
78
+ ? 'cursor-wait' // Show a waiting cursor during loading
79
+ : state === 'disabled'
80
+ ? 'cursor-not-allowed opacity-50'
81
+ : state === 'selected'
82
+ ? 'cursor-pointer ring-2 ring-offset-2 ring-blue-500'
83
+ : isHovered
84
+ ? 'cursor-pointer hover:bg-opacity-75'
85
+ : 'cursor-pointer';
86
+
87
+ return (
88
+ <button
89
+ className={`${baseClasses} ${bgColor} ${iconColor} ${bgColorHover} ${stateClasses} transition-colors duration-200 ease-in-out flex items-center justify-center`}
90
+ disabled={state === 'disabled' || loading} // Disable button during loading
91
+ onMouseEnter={() => setIsHovered(true)}
92
+ onMouseLeave={() => setIsHovered(false)}
93
+ onClick={onClick}
94
+ aria-label={loading ? 'Loading' : 'Reload'}
95
+ >
96
+ {loading ? (
97
+ <LoadingSmall size={size} /> // Pass the size prop to match the icon
98
+ ) : (
99
+ Icon && <Icon className={iconSizeClasses[size]} /> // Show icon when not loading
100
+ )}
101
+ </button>
102
+ );
103
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import React from 'react';
3
+
4
+ interface LoadingSmallProps {
5
+ size?: 'large' | 'medium' | 'small'; // Match IconButton sizes
6
+ }
7
+
8
+ export default function LoadingScreen() {
9
+ return (
10
+ <div className="flex justify-center items-center h-full w-full min-h-[200px]">
11
+ <div className="w-12 h-12 border-4 border-t-transparent border-light-text-primary dark:border-dark-text-primary dark:border-t-transparent rounded-full animate-spin"></div>
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export function LoadingSmall({ size = 'medium' }: LoadingSmallProps) {
17
+ const spinnerSizeClasses = {
18
+ large: 'w-6 h-6 border-2', // 24px, border-2 for proportional thickness
19
+ medium: 'w-6 h-6 border-2', // 24px, border-2
20
+ small: 'w-5 h-5 border-2', // 20px, slightly thinner border
21
+ };
22
+
23
+ return (
24
+ <div className="flex justify-center items-center">
25
+ <div
26
+ className={`border-t-transparent border-light-text-primary dark:border-dark-text-primary dark:border-t-transparent rounded-full animate-spin ${spinnerSizeClasses[size]}`}
27
+ />
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+ import React, { forwardRef } from 'react';
3
+
4
+ interface MenuProps {
5
+ children: React.ReactNode;
6
+ width?: number | string;
7
+ className?: string;
8
+ }
9
+
10
+ const Menu = forwardRef<HTMLDivElement, MenuProps>(
11
+ ({ children, width, className = '' }, ref) => {
12
+ return (
13
+ <div
14
+ ref={ref}
15
+ className={`
16
+ bg-light-background-accent300 dark:bg-dark-background-accent300
17
+ rounded-[8px]
18
+ shadow-lg
19
+ overflow-hidden
20
+ ${className}
21
+ `}
22
+ style={{ width: width || 'auto' }}
23
+ >
24
+ <div className="p-[10px]">
25
+ {children}
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+ );
31
+
32
+ Menu.displayName = 'Menu';
33
+
34
+ export default Menu;
35
+ export { Menu };
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useCallback, SVGProps, JSX } from 'react';
3
+ import NextLink from 'next/link';
4
+ import UserImage from './userImage';
5
+ import Tag from './tag';
6
+
7
+ type IconType = (props: SVGProps<SVGSVGElement>) => JSX.Element;
8
+
9
+ interface MenuItemProps {
10
+ href?: string;
11
+ iconName?: string;
12
+ userHandle?: string;
13
+ userImgUrl?: string;
14
+ label: string;
15
+ isSelected?: boolean;
16
+ onClick?: any;
17
+ className?: string;
18
+ size?: 'medium' | 'large';
19
+ tag?: {
20
+ label: React.ReactNode;
21
+ iconName?: string;
22
+ };
23
+ iconRight?: string;
24
+ }
25
+
26
+ export default function MenuItem({
27
+ href,
28
+ iconName,
29
+ userHandle,
30
+ userImgUrl,
31
+ label,
32
+ isSelected,
33
+ onClick,
34
+ className = '',
35
+ size = 'medium',
36
+ tag,
37
+ iconRight,
38
+ }: MenuItemProps) {
39
+ const [IconLeft, setIconLeft] = useState<IconType | null>(null);
40
+ const [IconRight, setIconRight] = useState<IconType | null>(null);
41
+
42
+ const loadIcon = useCallback(async (iconName?: string) => {
43
+ if (!iconName) return null;
44
+ try {
45
+ const module = await import('@heroicons/react/24/outline');
46
+ const IconComponent = module[iconName as keyof typeof module] as IconType;
47
+ return IconComponent || null;
48
+ } catch (error) {
49
+ console.error(`Failed to load icon ${iconName}:`, error);
50
+ return null;
51
+ }
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ const fetchIcon = async () => {
56
+ if (iconName) {
57
+ setIconLeft(await loadIcon(iconName));
58
+ }
59
+ if (iconRight) {
60
+ setIconRight(await loadIcon(iconRight));
61
+ }
62
+ };
63
+ fetchIcon();
64
+ }, [iconName, iconRight, loadIcon]);
65
+
66
+ // Size-based icon and text classes
67
+ const iconSize = size === 'large' ? 'w-6 h-6' : 'w-5 h-5';
68
+ const labelClass = size === 'large' ? 'text-body1' : 'text-body2';
69
+ const tagSize = size === 'large' ? 'medium' : 'small';
70
+
71
+ const content = (
72
+ <div
73
+ className={`
74
+ flex items-center p-2 rounded-[8px] cursor-pointer justify-between transition-colors duration-200 ease-in-out
75
+ ${isSelected
76
+ ? 'bg-light-background-accent300 dark:bg-dark-background-accent300 hover:bg-light-background-accent200 dark:hover:bg-dark-background-accent200'
77
+ : 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200'}
78
+ ${className}
79
+ `}
80
+ style={{ width: '100%' }}
81
+ onClick={onClick}
82
+ >
83
+ {/* Left group: icon/userImg + label + tag with 8px gap */}
84
+ <div className="flex items-center gap-1">
85
+ {userImgUrl ? (
86
+ <UserImage userHandle={userHandle || ''} userImgUrl={userImgUrl} />
87
+ ) : (
88
+ IconLeft && (
89
+ <IconLeft
90
+ className={`${iconSize} text-light-text-secondary dark:text-dark-text-secondary`}
91
+ />
92
+ )
93
+ )}
94
+ <span className={`whitespace-nowrap ${labelClass} text-light-text-primary dark:text-dark-text-primary`}>
95
+ {label}
96
+ </span>
97
+ {tag && (
98
+ <Tag
99
+ variant="contained"
100
+ size={tagSize}
101
+ label={tag.label}
102
+ iconName={tag.iconName as any}
103
+ />
104
+ )}
105
+ </div>
106
+
107
+ {/* Right icon aligned to the right */}
108
+ {IconRight && (
109
+ <IconRight
110
+ className={`${iconSize} text-light-text-secondary dark:text-dark-text-secondary flex-shrink-0`}
111
+ />
112
+ )}
113
+ </div>
114
+ );
115
+
116
+ return href ? <NextLink href={href}>{content}</NextLink> : content;
117
+ }