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