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.
- package/.claude/settings.local.json +9 -0
- package/.eslintrc.json +3 -0
- package/components/alert.tsx +132 -0
- package/components/button.tsx +120 -0
- package/components/checkBox.tsx +60 -0
- package/components/emptyBox.tsx +33 -0
- package/components/iconButton.tsx +103 -0
- package/components/loadingScreen.tsx +30 -0
- package/components/menu.tsx +35 -0
- package/components/menuItem.tsx +117 -0
- package/components/modal.tsx +85 -0
- package/components/navigation.tsx +27 -0
- package/components/popover.tsx +69 -0
- package/components/radioGroup.tsx +50 -0
- package/components/select.tsx +253 -0
- package/components/tab.tsx +85 -0
- package/components/tableCell.tsx +15 -0
- package/components/tableContainer.tsx +15 -0
- package/components/tableHeader.tsx +15 -0
- package/components/tableHeaderCell.tsx +15 -0
- package/components/tableRow.tsx +15 -0
- package/components/tabsContainer.tsx +23 -0
- package/components/tag.tsx +81 -0
- package/components/textField.tsx +116 -0
- package/components/textarea.tsx +120 -0
- package/components/timeStamp.tsx +65 -0
- package/components/tooltip.tsx +66 -0
- package/components/userImage.tsx +64 -0
- package/designTokens.js +234 -0
- package/dist/components/loadingScreen.jsx +2 -2
- package/dist/components/menuItem.d.ts +2 -8
- package/dist/components/menuItem.jsx +14 -34
- package/dist/components/tag.d.ts +2 -1
- package/dist/components/tag.jsx +22 -22
- package/index.css +8 -0
- package/index.ts +21 -0
- package/next.config.mjs +4 -0
- package/package.json +4 -28
- package/postcss.config.mjs +8 -0
- package/tailwind.config.ts +232 -0
- package/tsconfig.json +37 -0
- package/dist/tsconfig.tsbuildinfo +0 -1
package/.eslintrc.json
ADDED
|
@@ -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
|
+
}
|