oneslash-design-system 1.0.3 → 1.0.6
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/components/{alert/alert.tsx → alert.tsx} +14 -1
- package/components/button.tsx +121 -0
- package/components/{checkBox/checkBox.tsx → checkBox.tsx} +6 -2
- package/components/{iconButton/iconButton.tsx → iconButton.tsx} +9 -0
- package/components/{menuItem/menuItem.tsx → menuItem.tsx} +8 -0
- package/components/modal.tsx +66 -0
- package/components/popover.tsx +98 -0
- package/components/tab.tsx +85 -0
- package/components/{tag/tag.tsx → tag.tsx} +14 -0
- package/components/textField.tsx +128 -0
- package/components/timeStamp.tsx +65 -0
- package/components/{tooltip/tooltip.tsx → tooltip.tsx} +5 -0
- package/components/{userImage/userImage.tsx → userImage.tsx} +6 -0
- package/global.d.ts +1 -1
- package/index.css +5 -1
- package/index.ts +15 -16
- package/package.json +2 -1
- package/tsconfig.json +3 -2
- package/components/button/button.tsx +0 -107
- package/components/modal/modal.tsx +0 -41
- package/components/popover/popover.tsx +0 -74
- package/components/tab/tab.tsx +0 -62
- package/components/textField/textField.tsx +0 -107
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface AlertProps {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
type: 'success' | 'warning' | 'error' | 'info';
|
|
7
|
+
message: string;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function Alert({
|
|
12
|
+
open,
|
|
13
|
+
type,
|
|
14
|
+
message,
|
|
15
|
+
onClose
|
|
16
|
+
}: AlertProps) {
|
|
17
|
+
|
|
5
18
|
useEffect(() => {
|
|
6
19
|
if (open) {
|
|
7
20
|
const timer = setTimeout(() => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ButtonProps{
|
|
4
|
+
size: 'small' | 'medium' | 'large';
|
|
5
|
+
type: 'primary' | 'secondary' | 'tertiary' | 'textOnly';
|
|
6
|
+
state: 'enabled' | 'hovered' | 'focused' | 'disabled';
|
|
7
|
+
label: string;
|
|
8
|
+
decoIcon?: string;
|
|
9
|
+
actionIcon?: string;
|
|
10
|
+
onClickButton?: any;
|
|
11
|
+
onClickActionIcon?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
15
|
+
|
|
16
|
+
export default function Button({
|
|
17
|
+
size,
|
|
18
|
+
type,
|
|
19
|
+
state,
|
|
20
|
+
label,
|
|
21
|
+
decoIcon,
|
|
22
|
+
actionIcon,
|
|
23
|
+
onClickButton,
|
|
24
|
+
onClickActionIcon,
|
|
25
|
+
}: ButtonProps) {
|
|
26
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
27
|
+
const [IconLeft, setIconLeft] = useState<IconType | null>(null);
|
|
28
|
+
const [IconRight, setIconRight] = useState<IconType | null>(null);
|
|
29
|
+
|
|
30
|
+
// Import icon dynamically
|
|
31
|
+
const loadIcon = useCallback(async (iconName?: string) => {
|
|
32
|
+
if (!iconName) return null;
|
|
33
|
+
try {
|
|
34
|
+
const module = await import('@heroicons/react/24/outline');
|
|
35
|
+
const Icon = module[iconName as keyof typeof module] as IconType;
|
|
36
|
+
return Icon || null;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Failed to load icon ${iconName}:`, error);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const fetchIcons = async () => {
|
|
45
|
+
if (decoIcon) {
|
|
46
|
+
setIconLeft(await loadIcon(decoIcon));
|
|
47
|
+
}
|
|
48
|
+
if (actionIcon) {
|
|
49
|
+
setIconRight(await loadIcon(actionIcon));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
fetchIcons();
|
|
53
|
+
}, [decoIcon, actionIcon, loadIcon]);
|
|
54
|
+
|
|
55
|
+
// Define classes for size
|
|
56
|
+
const sizeClasses = {
|
|
57
|
+
large: 'text-body1 p-2',
|
|
58
|
+
medium: 'text-body1 p-1',
|
|
59
|
+
small: 'text-body2 p-1',
|
|
60
|
+
}[size];
|
|
61
|
+
|
|
62
|
+
// Define icon size classes
|
|
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
|
+
// Define classes for button types
|
|
70
|
+
const baseTypeClasses = {
|
|
71
|
+
primary: 'bg-light-accent-main dark:bg-dark-accent-main text-light-text-primary dark:text-dark-text-contrast',
|
|
72
|
+
secondary: 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast',
|
|
73
|
+
tertiary: 'bg-light-background-accent200 dark:bg-dark-background-accent200 text-light-text-primary dark:text-dark-text-primary',
|
|
74
|
+
textOnly: 'text-light-text-primary dark:text-dark-text-primary',
|
|
75
|
+
}[type];
|
|
76
|
+
|
|
77
|
+
const hoverTypeClasses = {
|
|
78
|
+
primary: 'hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark',
|
|
79
|
+
secondary: 'hover:bg-light-primary-dark dark:hover:bg-dark-primary-dark',
|
|
80
|
+
tertiary: 'hover:bg-light-background-accent300 hover:dark:bg-dark-background-accent300',
|
|
81
|
+
textOnly: 'hover:bg-light-background-accent100 hover:dark:bg-dark-background-accent100',
|
|
82
|
+
}[type];
|
|
83
|
+
|
|
84
|
+
// State classes
|
|
85
|
+
const stateClasses = {
|
|
86
|
+
enabled: 'cursor-pointer',
|
|
87
|
+
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',
|
|
88
|
+
disabled: type === 'textOnly'
|
|
89
|
+
? 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-transparent'
|
|
90
|
+
: 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Build the button classes dynamically
|
|
94
|
+
const buttonClasses = `
|
|
95
|
+
flex flex-row space-x-2 items-center rounded-[8px]
|
|
96
|
+
${sizeClasses}
|
|
97
|
+
${state === 'enabled' ? baseTypeClasses : ''}
|
|
98
|
+
${state === 'focused' ? stateClasses.focused : ''}
|
|
99
|
+
${state === 'disabled' ? stateClasses.disabled : baseTypeClasses}
|
|
100
|
+
${state !== 'disabled' && isHovered ? hoverTypeClasses : ''}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
className={buttonClasses}
|
|
106
|
+
onMouseEnter={() => { if (state !== 'disabled') setIsHovered(true); }}
|
|
107
|
+
onMouseLeave={() => { if (state !== 'disabled') setIsHovered(false); }}
|
|
108
|
+
onClick={onClickButton} // Button click action
|
|
109
|
+
>
|
|
110
|
+
{IconLeft && (
|
|
111
|
+
<IconLeft className={sizeIcon} />
|
|
112
|
+
)}
|
|
113
|
+
<div className="whitespace-nowrap px-2">{label}</div>
|
|
114
|
+
{IconRight && (
|
|
115
|
+
<div onClick={onClickActionIcon} className="cursor-pointer">
|
|
116
|
+
<IconRight className={sizeIcon} />
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</button>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
2
|
|
|
3
|
+
import { useState } from 'react';
|
|
4
4
|
|
|
5
|
+
interface CheckboxProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
onChange?: (checked: boolean) => void;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
export default function Checkbox({
|
|
7
12
|
label,
|
|
8
13
|
checked = false,
|
|
9
14
|
onChange
|
|
10
15
|
}: CheckboxProps) {
|
|
11
|
-
|
|
12
16
|
const [isChecked, setIsChecked] = useState(checked);
|
|
13
17
|
|
|
14
18
|
const handleToggle = () => {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import * as HeroIcons from '@heroicons/react/24/outline';
|
|
4
|
+
|
|
5
|
+
interface IconButtonProps{
|
|
6
|
+
variant: "contained" | "iconOnly";
|
|
7
|
+
color: "primary" | "secondary";
|
|
8
|
+
state: "enabled" | "selected" | "disabled";
|
|
9
|
+
iconName: keyof typeof HeroIcons;
|
|
10
|
+
onClick?: any;
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
5
14
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import NextLink from 'next/link';
|
|
4
4
|
|
|
5
|
+
interface MenuItemProps {
|
|
6
|
+
href?: string;
|
|
7
|
+
iconName?: string;
|
|
8
|
+
label: string;
|
|
9
|
+
isSelected?: boolean;
|
|
10
|
+
onClick: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export default function MenuItem({
|
|
6
14
|
href = '#',
|
|
7
15
|
iconName,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
title: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
actions: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function Modal({
|
|
12
|
+
isOpen,
|
|
13
|
+
title,
|
|
14
|
+
children,
|
|
15
|
+
onClose,
|
|
16
|
+
actions,
|
|
17
|
+
}: ModalProps) {
|
|
18
|
+
if (!isOpen) return null;
|
|
19
|
+
|
|
20
|
+
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
21
|
+
if (e.target === e.currentTarget) {
|
|
22
|
+
onClose();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === 'Escape') {
|
|
29
|
+
onClose();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
36
|
+
};
|
|
37
|
+
}, [onClose]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className="fixed inset-[-32px] bg-black bg-opacity-50 flex items-center justify-center z-50"
|
|
42
|
+
onClick={handleOverlayClick}
|
|
43
|
+
role="dialog"
|
|
44
|
+
aria-labelledby="modal-title"
|
|
45
|
+
aria-modal="true"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
className="bg-light-background-default dark:bg-dark-background-default p-6 rounded-[8px] space-y-4 w-[600px]"
|
|
49
|
+
style={{
|
|
50
|
+
maxHeight: 'calc(100vh - 64px)',
|
|
51
|
+
overflowY: 'auto',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<h2 id="modal-title" className="text-h6">
|
|
55
|
+
{title}
|
|
56
|
+
</h2>
|
|
57
|
+
<div className="text-body1 space-y-4">
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex justify-between">
|
|
61
|
+
{actions}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useRef, useLayoutEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
interface PopoverProps {
|
|
5
|
+
id?: string;
|
|
6
|
+
anchorEl?: HTMLElement | null;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
onClose?: () => void;
|
|
9
|
+
children: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Popover({
|
|
13
|
+
anchorEl,
|
|
14
|
+
open = false,
|
|
15
|
+
onClose,
|
|
16
|
+
children
|
|
17
|
+
}: PopoverProps) {
|
|
18
|
+
|
|
19
|
+
const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
|
|
20
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
// Close popover when clicking outside
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
25
|
+
if (popoverRef.current && !popoverRef.current.contains(event.target as Node) && anchorEl) {
|
|
26
|
+
onClose?.(); // Safe call for optional onClose
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
if (open) {
|
|
30
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
31
|
+
}
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
34
|
+
};
|
|
35
|
+
}, [open, anchorEl, onClose]);
|
|
36
|
+
|
|
37
|
+
// Calculate position and handle scroll/resize
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
if (anchorEl && open) {
|
|
40
|
+
const handlePositioning = () => {
|
|
41
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
42
|
+
const popoverRect = popoverRef.current?.getBoundingClientRect();
|
|
43
|
+
|
|
44
|
+
if (popoverRect) {
|
|
45
|
+
const spaceBelow = window.innerHeight - anchorRect.bottom;
|
|
46
|
+
const spaceAbove = anchorRect.top;
|
|
47
|
+
|
|
48
|
+
// Decide whether to place the popover above or below the anchor
|
|
49
|
+
const shouldPlaceAbove = spaceBelow < popoverRect.height + 8 && spaceAbove > popoverRect.height + 8;
|
|
50
|
+
|
|
51
|
+
// Calculate top and left positions
|
|
52
|
+
const topPosition = shouldPlaceAbove
|
|
53
|
+
? anchorRect.top + window.scrollY - popoverRect.height - 8
|
|
54
|
+
: anchorRect.bottom + window.scrollY + 8;
|
|
55
|
+
|
|
56
|
+
let leftPosition = anchorRect.left + window.scrollX + (anchorRect.width / 2) - (popoverRect.width / 2);
|
|
57
|
+
|
|
58
|
+
// Ensure the popover doesn't overflow off the left or right of the screen
|
|
59
|
+
if (leftPosition < 8) {
|
|
60
|
+
leftPosition = 8; // Prevent overflow on the left
|
|
61
|
+
} else if (leftPosition + popoverRect.width > window.innerWidth - 8) {
|
|
62
|
+
leftPosition = window.innerWidth - popoverRect.width - 8; // Prevent overflow on the right
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setPopoverStyle({
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
top: `${topPosition}px`,
|
|
68
|
+
left: `${leftPosition}px`,
|
|
69
|
+
zIndex: 10000,
|
|
70
|
+
visibility: 'visible',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
handlePositioning(); // Initial positioning calculation
|
|
76
|
+
window.addEventListener('resize', handlePositioning);
|
|
77
|
+
window.addEventListener('scroll', handlePositioning, true);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener('resize', handlePositioning);
|
|
81
|
+
window.removeEventListener('scroll', handlePositioning, true);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}, [anchorEl, open]);
|
|
85
|
+
|
|
86
|
+
if (!open || !anchorEl) return null;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={popoverRef}
|
|
91
|
+
style={popoverStyle}
|
|
92
|
+
className="bg-light-background-accent100 dark:bg-dark-background-accent100 rounded-[8px] shadow-lg p-2"
|
|
93
|
+
role="dialog"
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -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-2 p-2 rounded-[8px] cursor-pointer justify-start
|
|
68
|
+
${isSelected
|
|
69
|
+
? 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast'
|
|
70
|
+
: 'bg-light-action-selected dark:bg-dark-action-selected hover:bg-light-background-default dark:hover:bg-dark-action-hover'}
|
|
71
|
+
`}
|
|
72
|
+
onClick={handleClick}
|
|
73
|
+
>
|
|
74
|
+
{IconLeft && <IconLeft className="w-5 h-5" />}
|
|
75
|
+
<span className="whitespace-nowrap text-body1 px-2">
|
|
76
|
+
{label}
|
|
77
|
+
</span>
|
|
78
|
+
{IconRight && (
|
|
79
|
+
<div onClick={onClickActionIcon} className="cursor-pointer">
|
|
80
|
+
<IconRight className="w-5 h-5" />
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import * as HeroIcons from '@heroicons/react/24/outline';
|
|
4
|
+
|
|
5
|
+
interface TagProps{
|
|
6
|
+
key?: any;
|
|
7
|
+
variant: "contained" | "textOnly";
|
|
8
|
+
size: "medium" | "small";
|
|
9
|
+
state?: "enabled" | "selected" ;
|
|
10
|
+
label: any;
|
|
11
|
+
iconName?: keyof typeof HeroIcons;
|
|
12
|
+
isDeletable?: keyof typeof HeroIcons;
|
|
13
|
+
onClick?: any;
|
|
14
|
+
color?: 'default' | 'info';
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
5
18
|
|
|
@@ -89,6 +102,7 @@ export default function Tag({
|
|
|
89
102
|
>
|
|
90
103
|
{DeleteIcon && <DeleteIcon className="w-4 h-4" />}
|
|
91
104
|
</button>
|
|
105
|
+
|
|
92
106
|
)}
|
|
93
107
|
</div>
|
|
94
108
|
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
interface TextFieldProps {
|
|
5
|
+
id: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;// Specify the event type for both input and textarea
|
|
9
|
+
onBlur?: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;// Handle blur event for input/textarea
|
|
10
|
+
onFocus?: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;// Handle focus event for input/textarea
|
|
11
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void; // Add onKeyDown
|
|
12
|
+
autoFocus?: boolean;
|
|
13
|
+
iconLeft?: React.ReactNode;
|
|
14
|
+
iconRight?: React.ReactNode | React.ComponentType<any>;
|
|
15
|
+
multiline?: boolean;
|
|
16
|
+
maxRows?: number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
error?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function TextField({
|
|
22
|
+
id,
|
|
23
|
+
label,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
onBlur,
|
|
27
|
+
onFocus,
|
|
28
|
+
onKeyDown,
|
|
29
|
+
autoFocus = false, // Accept the autoFocus prop with default value
|
|
30
|
+
iconLeft,
|
|
31
|
+
iconRight,
|
|
32
|
+
multiline = false,
|
|
33
|
+
maxRows = 6,
|
|
34
|
+
disabled = false,
|
|
35
|
+
error = false,
|
|
36
|
+
}: TextFieldProps) {
|
|
37
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
38
|
+
|
|
39
|
+
const baseClasses = 'w-full border rounded-[8px] p-2';
|
|
40
|
+
const bgColor = 'bg-light-background-default dark:bg-dark-background-default transition-colors duration-300 ease-in-out';
|
|
41
|
+
const borderColor = 'border-light-outlinedBorder-active dark:border-dark-outlinedBorder-active';
|
|
42
|
+
const containerClasses = `
|
|
43
|
+
${bgColor}
|
|
44
|
+
${borderColor}
|
|
45
|
+
${baseClasses}
|
|
46
|
+
${disabled ? 'bg-gray-200 cursor-not-allowed' : ''}
|
|
47
|
+
${error ? 'border-red-500 focus:ring-red-500' : ''}
|
|
48
|
+
${isFocused ? 'focus:border-light-accent-main focus:dark:border-dark-accent-main outline-none' : ''}
|
|
49
|
+
${!disabled && !error ? 'hover:border-light-outlinedBorder-hover' : ''}
|
|
50
|
+
border-gray-300
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const renderIconRight = () => {
|
|
54
|
+
if (React.isValidElement(iconRight)) {
|
|
55
|
+
return iconRight;
|
|
56
|
+
}
|
|
57
|
+
if (typeof iconRight === 'function') {
|
|
58
|
+
return React.createElement(iconRight);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-col w-full">
|
|
65
|
+
{label && (
|
|
66
|
+
<label htmlFor={id} className="mb-1 text-body2 text-light-text-secondary dark:text-dark-text-secondary">
|
|
67
|
+
{label}
|
|
68
|
+
</label>
|
|
69
|
+
)}
|
|
70
|
+
<div className="relative">
|
|
71
|
+
{iconLeft && (
|
|
72
|
+
<span className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
|
73
|
+
{iconLeft}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
{iconRight && (
|
|
77
|
+
<span className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
78
|
+
{renderIconRight()}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
{multiline ? (
|
|
82
|
+
<textarea
|
|
83
|
+
id={id}
|
|
84
|
+
rows={maxRows}
|
|
85
|
+
className={containerClasses}
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={onChange}
|
|
88
|
+
onFocus={(e) => {
|
|
89
|
+
setIsFocused(true);
|
|
90
|
+
if (onFocus) onFocus(e);
|
|
91
|
+
}}
|
|
92
|
+
onBlur={(e) => {
|
|
93
|
+
setIsFocused(false);
|
|
94
|
+
if (onBlur) onBlur(e);
|
|
95
|
+
}}
|
|
96
|
+
onKeyDown={onKeyDown}
|
|
97
|
+
autoFocus={autoFocus} // Pass autoFocus to textarea
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
/>
|
|
100
|
+
) : (
|
|
101
|
+
<input
|
|
102
|
+
id={id}
|
|
103
|
+
type="text"
|
|
104
|
+
className={containerClasses}
|
|
105
|
+
value={value}
|
|
106
|
+
onChange={onChange}
|
|
107
|
+
onFocus={(e) => {
|
|
108
|
+
setIsFocused(true);
|
|
109
|
+
if (onFocus) onFocus(e);
|
|
110
|
+
}}
|
|
111
|
+
onBlur={(e) => {
|
|
112
|
+
setIsFocused(false);
|
|
113
|
+
if (onBlur) onBlur(e);
|
|
114
|
+
}}
|
|
115
|
+
onKeyDown={onKeyDown}
|
|
116
|
+
autoFocus={autoFocus} // Pass autoFocus to input
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
{error && (
|
|
122
|
+
<p className="mt-1 text-light-error-main text-body2">
|
|
123
|
+
This field is required
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TimeStampProps{
|
|
5
|
+
createdAt: string | number | Date;
|
|
6
|
+
dateFormat: 'absolute' | 'relative';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function TimeStamp({
|
|
10
|
+
createdAt,
|
|
11
|
+
dateFormat,
|
|
12
|
+
}: TimeStampProps) {
|
|
13
|
+
|
|
14
|
+
// absolute timestamp
|
|
15
|
+
const absoluteTimeStamp = (createdAt: string | number | Date): string => {
|
|
16
|
+
const date = new Date(createdAt);
|
|
17
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
18
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
19
|
+
const year = date.getFullYear();
|
|
20
|
+
const hours = date.getHours();
|
|
21
|
+
const minutes = date.getMinutes();
|
|
22
|
+
const formattedHours = hours.toString().padStart(2, '0');
|
|
23
|
+
const formattedMinutes = minutes.toString().padStart(2, '0');
|
|
24
|
+
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
25
|
+
const dayOfWeek = daysOfWeek[date.getDay()];
|
|
26
|
+
return `${month}/${day}/${year} ${dayOfWeek} ${formattedHours}:${formattedMinutes}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// relative timestamp
|
|
30
|
+
const relativeTimeStamp = (createdAt: string | number | Date): string => {
|
|
31
|
+
const date = new Date(createdAt);
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const diff = now.getTime() - date.getTime();
|
|
34
|
+
const seconds = Math.floor(diff / 1000);
|
|
35
|
+
const minutes = Math.floor(seconds / 60);
|
|
36
|
+
const hours = Math.floor(minutes / 60);
|
|
37
|
+
const days = Math.floor(hours / 24);
|
|
38
|
+
const weeks = Math.floor(days / 7);
|
|
39
|
+
const months = Math.floor(days / 30);
|
|
40
|
+
const years = Math.floor(days / 365);
|
|
41
|
+
if (years > 0) {
|
|
42
|
+
return `${years} year${years > 1 ? 's' : ''} ago`;
|
|
43
|
+
} else if (months > 0) {
|
|
44
|
+
return `${months} month${months > 1 ? 's' : ''} ago`;
|
|
45
|
+
} else if (weeks > 0) {
|
|
46
|
+
return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
|
|
47
|
+
} else if (days > 0) {
|
|
48
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
49
|
+
} else if (hours > 0) {
|
|
50
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
51
|
+
} else if (minutes > 0) {
|
|
52
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
53
|
+
} else {
|
|
54
|
+
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const timeStamp = dateFormat === 'absolute' ? absoluteTimeStamp(createdAt) : relativeTimeStamp(createdAt);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<p className="text-caption text-light-text-secondary dark:text-dark-text-secondary">
|
|
62
|
+
{timeStamp}
|
|
63
|
+
</p>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { useState, useRef, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
+
interface TooltipProps {
|
|
5
|
+
title: string;
|
|
6
|
+
children: React.ReactElement;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
export default function Tooltip({ title, children }: TooltipProps) {
|
|
5
10
|
const [visible, setVisible] = useState(false);
|
|
6
11
|
const [position, setPosition] = useState<'top' | 'bottom'>('bottom');
|
package/global.d.ts
CHANGED
package/index.css
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
@tailwind base;
|
|
2
2
|
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
4
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap');
|
|
4
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap');
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
font-family: 'Inter', sans-serif;
|
|
8
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import './index.css';
|
|
2
|
-
import
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
export * from './components/
|
|
6
|
-
export * from './components/
|
|
7
|
-
export * from './components/
|
|
8
|
-
export * from './components/
|
|
9
|
-
export * from './components/
|
|
10
|
-
export * from './components/
|
|
11
|
-
export * from './components/
|
|
12
|
-
export * from './components/
|
|
13
|
-
export * from './components/
|
|
14
|
-
export * from './components/
|
|
15
|
-
export * from './components/
|
|
16
|
-
export * from './components/userImage/userImage';
|
|
1
|
+
import './index.css';
|
|
2
|
+
import './designTokens';
|
|
3
|
+
export * from './components/button';
|
|
4
|
+
export * from './components/alert';
|
|
5
|
+
export * from './components/button';
|
|
6
|
+
export * from './components/checkBox';
|
|
7
|
+
export * from './components/iconButton';
|
|
8
|
+
export * from './components/menuItem';
|
|
9
|
+
export * from './components/modal';
|
|
10
|
+
export * from './components/popover';
|
|
11
|
+
export * from './components/tab';
|
|
12
|
+
export * from './components/tag';
|
|
13
|
+
export * from './components/textField';
|
|
14
|
+
export * from './components/tooltip';
|
|
15
|
+
export * from './components/userImage';
|
package/package.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
package/components/tab/tab.tsx
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
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;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
|
|
4
|
-
export default function TextField({
|
|
5
|
-
id,
|
|
6
|
-
label,
|
|
7
|
-
value,
|
|
8
|
-
onChange,
|
|
9
|
-
iconLeft,
|
|
10
|
-
iconRight,
|
|
11
|
-
multiline = false,
|
|
12
|
-
maxRows = 6,
|
|
13
|
-
disabled = false,
|
|
14
|
-
error = false,
|
|
15
|
-
}: TextFieldProps) {
|
|
16
|
-
|
|
17
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
18
|
-
|
|
19
|
-
// Base styles
|
|
20
|
-
const baseClasses = 'w-full border rounded-[8px] p-2';
|
|
21
|
-
|
|
22
|
-
// Background color
|
|
23
|
-
const bgColor = 'bg-light-background-default dark:bg-dark-background-default transition-colors duration-300 ease-in-out';
|
|
24
|
-
|
|
25
|
-
// Border color
|
|
26
|
-
const borderColor = 'border-light-outlinedBorder-active dark:border-dark-outlinedBorder-active';
|
|
27
|
-
|
|
28
|
-
// State styles
|
|
29
|
-
const disabledClasses = 'bg-gray-200 cursor-not-allowed';
|
|
30
|
-
const errorClasses = 'border-red-500 focus:ring-red-500';
|
|
31
|
-
const focusClasses = 'focus:border-light-accent-main focus:dark:border-dark-accent-main outline-none';
|
|
32
|
-
const hoverClasses = 'hover:border-light-outlinedBorder-hover';
|
|
33
|
-
const defaultClasses = 'border-gray-300';
|
|
34
|
-
|
|
35
|
-
// Container styles
|
|
36
|
-
const containerClasses = `
|
|
37
|
-
${bgColor}
|
|
38
|
-
${borderColor}
|
|
39
|
-
${baseClasses}
|
|
40
|
-
${disabled ? disabledClasses : ''}
|
|
41
|
-
${error ? errorClasses : ''}
|
|
42
|
-
${isFocused ? focusClasses : ''}
|
|
43
|
-
${!disabled && !error ? hoverClasses : ''}
|
|
44
|
-
${defaultClasses}
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
// Render iconRight in TextField
|
|
48
|
-
const renderIconRight = () => {
|
|
49
|
-
if (React.isValidElement(iconRight)) {
|
|
50
|
-
return iconRight;
|
|
51
|
-
}
|
|
52
|
-
if (typeof iconRight === 'function') {
|
|
53
|
-
return React.createElement(iconRight);
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="flex flex-col w-full">
|
|
60
|
-
{label && (
|
|
61
|
-
<label htmlFor={id} className="mb-1 text-body2 text-light-text-secondary dark:text-dark-text-secondary">
|
|
62
|
-
{label}
|
|
63
|
-
</label>
|
|
64
|
-
)}
|
|
65
|
-
<div className="relative">
|
|
66
|
-
{iconLeft && (
|
|
67
|
-
<span className="absolute inset-y-0 left-0 pl-3 flex items-center">
|
|
68
|
-
{iconLeft}
|
|
69
|
-
</span>
|
|
70
|
-
)}
|
|
71
|
-
{iconRight && (
|
|
72
|
-
<span className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
73
|
-
{renderIconRight()}
|
|
74
|
-
</span>
|
|
75
|
-
)}
|
|
76
|
-
{multiline ? (
|
|
77
|
-
<textarea
|
|
78
|
-
id={id}
|
|
79
|
-
rows={maxRows}
|
|
80
|
-
className={containerClasses}
|
|
81
|
-
value={value}
|
|
82
|
-
onChange={onChange}
|
|
83
|
-
onFocus={() => setIsFocused(true)}
|
|
84
|
-
onBlur={() => setIsFocused(false)}
|
|
85
|
-
disabled={disabled}
|
|
86
|
-
/>
|
|
87
|
-
) : (
|
|
88
|
-
<input
|
|
89
|
-
id={id}
|
|
90
|
-
type="text"
|
|
91
|
-
className={containerClasses}
|
|
92
|
-
value={value}
|
|
93
|
-
onChange={onChange}
|
|
94
|
-
onFocus={() => setIsFocused(true)}
|
|
95
|
-
onBlur={() => setIsFocused(false)}
|
|
96
|
-
disabled={disabled}
|
|
97
|
-
/>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
{error && (
|
|
101
|
-
<p className="mt-1 text-light-error-main text-body2">
|
|
102
|
-
This field is required
|
|
103
|
-
</p>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
}
|