oneslash-design-system 1.2.12 → 1.2.14
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/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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
actions?: React.ReactNode;
|
|
10
|
+
size?: 'medium' | 'large';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function Modal({
|
|
14
|
+
isOpen,
|
|
15
|
+
title,
|
|
16
|
+
children,
|
|
17
|
+
onClose,
|
|
18
|
+
actions,
|
|
19
|
+
size = 'medium', // Default size is medium
|
|
20
|
+
}: ModalProps) {
|
|
21
|
+
|
|
22
|
+
if (!isOpen) return null;
|
|
23
|
+
|
|
24
|
+
// close modal by clicking elsewhere
|
|
25
|
+
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
26
|
+
if (e.target === e.currentTarget) {
|
|
27
|
+
onClose();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// close modal by esc keypress
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
34
|
+
if (e.key === 'Escape') {
|
|
35
|
+
onClose();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
41
|
+
};
|
|
42
|
+
}, [onClose]);
|
|
43
|
+
|
|
44
|
+
// Determine width based on size prop
|
|
45
|
+
const modalWidth = size === 'large' ? 'w-[1200px]' : 'w-[600px]';
|
|
46
|
+
const maxWidth = size === 'large' ? '1200px' : '600px';
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className="fixed inset-[-32px] bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
53
|
+
onClick={handleOverlayClick}
|
|
54
|
+
role="dialog"
|
|
55
|
+
aria-labelledby="modal-title"
|
|
56
|
+
aria-modal="true"
|
|
57
|
+
tabIndex={-1}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
className={`bg-light-background-default dark:bg-dark-background-default p-6 rounded-[8px] space-y-4 ${modalWidth}`}
|
|
61
|
+
style={{
|
|
62
|
+
maxWidth,
|
|
63
|
+
width: 'calc(100vw - 64px)',
|
|
64
|
+
maxHeight: '800px',
|
|
65
|
+
height: 'auto',
|
|
66
|
+
overflowY: 'auto',
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{title && (
|
|
70
|
+
<h2 id="modal-title" className="text-h6">
|
|
71
|
+
{title}
|
|
72
|
+
</h2>
|
|
73
|
+
)}
|
|
74
|
+
<div className="text-body1 space-y-4">
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
{actions && (
|
|
78
|
+
<div className="flex justify-between">
|
|
79
|
+
{actions}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface NavigationProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Navigation({
|
|
10
|
+
children,
|
|
11
|
+
className = '',
|
|
12
|
+
}: NavigationProps) {
|
|
13
|
+
return (
|
|
14
|
+
<nav
|
|
15
|
+
className={`
|
|
16
|
+
bg-light-background-default dark:bg-dark-background-default
|
|
17
|
+
border-r border-light-misc-divider dark:border-dark-misc-divider
|
|
18
|
+
p-[10px]
|
|
19
|
+
${className}
|
|
20
|
+
`}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</nav>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Navigation };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { usePopper } from 'react-popper';
|
|
5
|
+
|
|
6
|
+
interface PopoverProps {
|
|
7
|
+
id?: string;
|
|
8
|
+
anchorEl?: HTMLElement | null;
|
|
9
|
+
open: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Popover({
|
|
15
|
+
id,
|
|
16
|
+
anchorEl,
|
|
17
|
+
open,
|
|
18
|
+
onClose,
|
|
19
|
+
children,
|
|
20
|
+
}: PopoverProps) {
|
|
21
|
+
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null);
|
|
22
|
+
const [hasMounted, setHasMounted] = useState(false);
|
|
23
|
+
|
|
24
|
+
// Initialize Popper.js
|
|
25
|
+
const { styles, attributes } = usePopper(anchorEl, popoverElement, {
|
|
26
|
+
placement: 'bottom-start', // Default placement, can be customized
|
|
27
|
+
modifiers: [
|
|
28
|
+
{ name: 'offset', options: { offset: [0, 8] } }, // Offset for spacing between anchor and popover
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Handle outside clicks to close the popover
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
35
|
+
if (popoverElement && !popoverElement.contains(event.target as Node) && anchorEl) {
|
|
36
|
+
onClose();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (open) {
|
|
41
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
45
|
+
};
|
|
46
|
+
}, [open, anchorEl, popoverElement, onClose]);
|
|
47
|
+
|
|
48
|
+
// Ensure popover is only rendered after the component mounts
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setHasMounted(true);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
if (!open || !hasMounted || !anchorEl) return null;
|
|
54
|
+
|
|
55
|
+
// Render popover in a portal to prevent layout shifts and positioning issues
|
|
56
|
+
return createPortal(
|
|
57
|
+
<div
|
|
58
|
+
id={id}
|
|
59
|
+
ref={setPopoverElement}
|
|
60
|
+
style={{ ...styles.popper, display: open ? 'block' : 'none' }}
|
|
61
|
+
{...attributes.popper}
|
|
62
|
+
className="bg-light-background-accent300 dark:bg-dark-background-accent300 rounded-[8px] shadow-lg p-2"
|
|
63
|
+
role="dialog"
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>,
|
|
67
|
+
document.body // Mounting the popover in the document body for isolation
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface RadioOption {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RadioGroupProps {
|
|
10
|
+
options: RadioOption[];
|
|
11
|
+
selectedValue: string;
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
direction?: 'horizontal' | 'vertical';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function RadioGroup({
|
|
17
|
+
options,
|
|
18
|
+
selectedValue,
|
|
19
|
+
onChange,
|
|
20
|
+
direction = 'vertical',
|
|
21
|
+
}: RadioGroupProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`flex ${ direction === 'horizontal' ? 'space-x-4' : 'flex-col space-y-2' }`}
|
|
25
|
+
>
|
|
26
|
+
{options.map((option) => (
|
|
27
|
+
<label
|
|
28
|
+
key={option.value}
|
|
29
|
+
className="flex items-center cursor-pointer"
|
|
30
|
+
onClick={() => onChange(option.value)}
|
|
31
|
+
>
|
|
32
|
+
{/* outer circle */}
|
|
33
|
+
<div
|
|
34
|
+
className={`relative flex justify-center items-center w-4 h-4 rounded-full border-2
|
|
35
|
+
${selectedValue === option.value
|
|
36
|
+
? 'border-light-text-primary dark:border-dark-text-primary'
|
|
37
|
+
: 'border-light-text-secondary dark:border-dark-text-secondary'}
|
|
38
|
+
`}
|
|
39
|
+
>
|
|
40
|
+
{/* Inner circle */}
|
|
41
|
+
{selectedValue === option.value && (
|
|
42
|
+
<div className='absolute w-2 h-2 rounded-full bg-light-text-primary dark:bg-dark-text-primary'/>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
<span className="ml-2 text-body1 text-light-text-primary dark:text-dark-text-primary">{option.label}</span>
|
|
46
|
+
</label>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect, useRef, useCallback, SVGProps, JSX } from 'react';
|
|
3
|
+
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
|
4
|
+
import Menu from './menu';
|
|
5
|
+
import MenuItem from './menuItem';
|
|
6
|
+
|
|
7
|
+
type IconType = (props: SVGProps<SVGSVGElement>) => JSX.Element;
|
|
8
|
+
|
|
9
|
+
export interface SelectOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
iconName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SelectProps {
|
|
16
|
+
value?: string | string[];
|
|
17
|
+
options: SelectOption[];
|
|
18
|
+
onChange?: (value: string | string[]) => void;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
decoIconName?: string;
|
|
22
|
+
width?: number | string;
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function Select({
|
|
28
|
+
value,
|
|
29
|
+
options,
|
|
30
|
+
onChange,
|
|
31
|
+
disabled = false,
|
|
32
|
+
placeholder = 'Label',
|
|
33
|
+
decoIconName,
|
|
34
|
+
width,
|
|
35
|
+
multiple = false,
|
|
36
|
+
className = '',
|
|
37
|
+
}: SelectProps) {
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
40
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
41
|
+
const [DecoIcon, setDecoIcon] = useState<IconType | null>(null);
|
|
42
|
+
const [mounted, setMounted] = useState(false);
|
|
43
|
+
|
|
44
|
+
const selectRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
// Handle SSR
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setMounted(true);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
// Load decoration icon
|
|
53
|
+
const loadIcon = useCallback(async (iconName?: string) => {
|
|
54
|
+
if (!iconName) return null;
|
|
55
|
+
try {
|
|
56
|
+
const module = await import('@heroicons/react/24/outline');
|
|
57
|
+
const IconComponent = module[iconName as keyof typeof module] as IconType;
|
|
58
|
+
return IconComponent || null;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`Failed to load icon ${iconName}:`, error);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const fetchIcon = async () => {
|
|
67
|
+
if (decoIconName) {
|
|
68
|
+
setDecoIcon(await loadIcon(decoIconName));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
fetchIcon();
|
|
72
|
+
}, [decoIconName, loadIcon]);
|
|
73
|
+
|
|
74
|
+
// Close menu when clicking outside
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!mounted) return;
|
|
77
|
+
|
|
78
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
79
|
+
if (
|
|
80
|
+
selectRef.current &&
|
|
81
|
+
menuRef.current &&
|
|
82
|
+
!selectRef.current.contains(event.target as Node) &&
|
|
83
|
+
!menuRef.current.contains(event.target as Node)
|
|
84
|
+
) {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
setIsFocused(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (isOpen) {
|
|
91
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
96
|
+
};
|
|
97
|
+
}, [isOpen, mounted]);
|
|
98
|
+
|
|
99
|
+
const handleToggle = () => {
|
|
100
|
+
if (disabled) return;
|
|
101
|
+
setIsOpen(!isOpen);
|
|
102
|
+
setIsFocused(!isOpen);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleSelect = (optionValue: string) => {
|
|
106
|
+
if (multiple) {
|
|
107
|
+
const currentValues = Array.isArray(value) ? value : [];
|
|
108
|
+
const newValues = currentValues.includes(optionValue)
|
|
109
|
+
? currentValues.filter(v => v !== optionValue)
|
|
110
|
+
: [...currentValues, optionValue];
|
|
111
|
+
onChange?.(newValues);
|
|
112
|
+
} else {
|
|
113
|
+
onChange?.(optionValue);
|
|
114
|
+
setIsOpen(false);
|
|
115
|
+
setIsFocused(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const getDisplayLabel = () => {
|
|
120
|
+
if (!value) return placeholder;
|
|
121
|
+
|
|
122
|
+
if (multiple && Array.isArray(value)) {
|
|
123
|
+
if (value.length === 0) return placeholder;
|
|
124
|
+
const labels = value
|
|
125
|
+
.map(v => options.find(opt => opt.value === v)?.label)
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
return labels.join(', ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return options.find(opt => opt.value === value)?.label || placeholder;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const isSelected = (optionValue: string) => {
|
|
134
|
+
if (multiple && Array.isArray(value)) {
|
|
135
|
+
return value.includes(optionValue);
|
|
136
|
+
}
|
|
137
|
+
return value === optionValue;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Determine border styles based on state
|
|
141
|
+
const getBorderStyles = () => {
|
|
142
|
+
if (disabled) {
|
|
143
|
+
return 'border border-light-actionOutlinedBorder-disabled dark:border-dark-actionOutlinedBorder-disabled';
|
|
144
|
+
}
|
|
145
|
+
if (isFocused || isOpen) {
|
|
146
|
+
return 'border border-light-accent-main dark:border-dark-accent-main outline outline-1 outline-light-accent-main dark:outline-dark-accent-main outline-offset-0';
|
|
147
|
+
}
|
|
148
|
+
if (isHovered) {
|
|
149
|
+
return 'border border-light-actionOutlinedBorder-enabled dark:border-dark-actionOutlinedBorder-enabled';
|
|
150
|
+
}
|
|
151
|
+
return 'border border-light-actionOutlinedBorder-enabled dark:border-dark-actionOutlinedBorder-enabled';
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Determine background color
|
|
155
|
+
const getBackgroundColor = () => {
|
|
156
|
+
if (isHovered && !isOpen && !disabled) {
|
|
157
|
+
return 'bg-light-action-hover dark:bg-dark-action-hover';
|
|
158
|
+
}
|
|
159
|
+
return 'bg-light-background-default dark:bg-dark-background-default';
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Determine text color
|
|
163
|
+
const getTextColor = () => {
|
|
164
|
+
if (disabled) {
|
|
165
|
+
return 'text-light-text-disabled dark:text-dark-text-disabled';
|
|
166
|
+
}
|
|
167
|
+
return 'text-light-text-primary dark:text-dark-text-primary';
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Determine icon color
|
|
171
|
+
const getIconColor = () => {
|
|
172
|
+
if (disabled) {
|
|
173
|
+
return 'text-light-text-disabled dark:text-dark-text-disabled';
|
|
174
|
+
}
|
|
175
|
+
return 'text-light-text-secondary dark:text-dark-text-secondary';
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className={`relative ${className}`} style={{ width: width || '100%' }}>
|
|
180
|
+
{/* Select Button */}
|
|
181
|
+
<div
|
|
182
|
+
ref={selectRef}
|
|
183
|
+
className={`
|
|
184
|
+
rounded-[8px]
|
|
185
|
+
${getBorderStyles()}
|
|
186
|
+
${getBackgroundColor()}
|
|
187
|
+
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
|
188
|
+
transition-all duration-200 ease-in-out
|
|
189
|
+
outline-none
|
|
190
|
+
`}
|
|
191
|
+
onClick={handleToggle}
|
|
192
|
+
onMouseEnter={() => !disabled && setIsHovered(true)}
|
|
193
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
194
|
+
tabIndex={disabled ? -1 : 0}
|
|
195
|
+
onKeyDown={(e) => {
|
|
196
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
handleToggle();
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{/* Inner Content Container with Padding */}
|
|
203
|
+
<div className={`flex items-center justify-between p-2 ${getTextColor()}`}>
|
|
204
|
+
{/* Content Container */}
|
|
205
|
+
<div className="flex items-center space-x-1 flex-1 overflow-hidden">
|
|
206
|
+
{/* Optional Decoration Icon */}
|
|
207
|
+
{DecoIcon && (
|
|
208
|
+
<DecoIcon className={`w-6 h-6 flex-shrink-0 ${getIconColor()}`} />
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{/* Display Text */}
|
|
212
|
+
<span className="text-body1 truncate">
|
|
213
|
+
{getDisplayLabel()}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Chevron Icon */}
|
|
218
|
+
<ChevronDownIcon
|
|
219
|
+
className={`
|
|
220
|
+
w-6 h-6 flex-shrink-0 ml-1
|
|
221
|
+
${getIconColor()}
|
|
222
|
+
transition-transform duration-200
|
|
223
|
+
${isOpen ? 'transform rotate-180' : ''}
|
|
224
|
+
`}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Dropdown Menu */}
|
|
230
|
+
{mounted && isOpen && (
|
|
231
|
+
<div
|
|
232
|
+
className="absolute z-50 mt-1 left-0"
|
|
233
|
+
style={{ width: width || '100%' }}
|
|
234
|
+
>
|
|
235
|
+
<Menu ref={menuRef} width={width || '100%'}>
|
|
236
|
+
{options.map((option) => (
|
|
237
|
+
<MenuItem
|
|
238
|
+
key={option.value}
|
|
239
|
+
label={option.label}
|
|
240
|
+
iconName={option.iconName}
|
|
241
|
+
isSelected={isSelected(option.value)}
|
|
242
|
+
onClick={() => handleSelect(option.value)}
|
|
243
|
+
className="w-full"
|
|
244
|
+
/>
|
|
245
|
+
))}
|
|
246
|
+
</Menu>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { Select };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
4
|
+
|
|
5
|
+
type TabProps = {
|
|
6
|
+
label: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
isSelected: boolean;
|
|
9
|
+
onClickTab: () => void;
|
|
10
|
+
onClickActionIcon?: any;
|
|
11
|
+
decoIcon?: string;
|
|
12
|
+
actionIcon?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
16
|
+
|
|
17
|
+
export default function Tab({
|
|
18
|
+
label,
|
|
19
|
+
href,
|
|
20
|
+
isSelected,
|
|
21
|
+
onClickTab,
|
|
22
|
+
onClickActionIcon,
|
|
23
|
+
decoIcon,
|
|
24
|
+
actionIcon
|
|
25
|
+
}: TabProps) {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
const pathname = usePathname();
|
|
28
|
+
|
|
29
|
+
const [IconLeft, setIconLeft] = useState<IconType | null>(null);
|
|
30
|
+
const [IconRight, setIconRight] = useState<IconType | null>(null);
|
|
31
|
+
|
|
32
|
+
// Load icon dynamically
|
|
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 handleClick = () => {
|
|
58
|
+
onClickTab();
|
|
59
|
+
if (href) {
|
|
60
|
+
router.push(href);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className={`
|
|
67
|
+
flex items-center space-x-1 py-1 px-[6px] rounded-[8px] cursor-pointer justify-start transition-colors duration-200 ease-in-out
|
|
68
|
+
${isSelected
|
|
69
|
+
? 'bg-light-primary-dark dark:bg-dark-primary-dark text-light-text-contrast dark:text-dark-text-contrast'
|
|
70
|
+
: 'hover:bg-light-background-accent200 dark:hover:bg-dark-background-accent200'}
|
|
71
|
+
`}
|
|
72
|
+
onClick={handleClick}
|
|
73
|
+
>
|
|
74
|
+
{IconLeft && <IconLeft className="w-6 h-6" />}
|
|
75
|
+
<span className="whitespace-nowrap text-body1 px-[6px]">
|
|
76
|
+
{label}
|
|
77
|
+
</span>
|
|
78
|
+
{IconRight && (
|
|
79
|
+
<div onClick={onClickActionIcon} className="cursor-pointer">
|
|
80
|
+
<IconRight className="w-6 h-6" />
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TableCellProps{
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableCell({children}: TableCellProps) {
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex-1 p-2 text-body2 text-light-text-primary dark:text-dark-text-primary border-t border-light-misc-divider dark:border-dark-misc-divider">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TableContainerProps{
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableContainer({children}: TableContainerProps) {
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="w-full overflow-x-auto bg-light-background-default dark:bg-dark-background-default">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TableHeaderProps{
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableHeader({children}: TableHeaderProps) {
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="py-2 bg-light-background-default dark:bg-dark-background-default">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TableHeaderCellProps{
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableHeaderCell({children}: TableHeaderCellProps) {
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex-1 p-2 text-body2 text-light-text-primary dark:text-dark-text-primary">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TableRowProps{
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableRow({children}: TableRowProps) {
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex w-full">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TabsContainerProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TabsContainer({
|
|
9
|
+
children,
|
|
10
|
+
}: TabsContainerProps) {
|
|
11
|
+
const tabCount = React.Children.count(children);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={`
|
|
16
|
+
flex space-x-2 p-1 rounded-[12px]
|
|
17
|
+
${tabCount > 0 ? 'bg-light-background-accent100 dark:bg-dark-background-accent100' : 'bg-transparent'}
|
|
18
|
+
`}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|