oneslash-design-system 1.2.3 → 1.2.4
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/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +4 -1
- package/.claude/settings.local.json +0 -9
- package/.eslintrc.json +0 -3
- package/components/alert.tsx +0 -132
- package/components/button.tsx +0 -120
- package/components/checkBox.tsx +0 -60
- package/components/emptyBox.tsx +0 -33
- package/components/iconButton.tsx +0 -103
- package/components/loadingScreen.tsx +0 -30
- package/components/menu.tsx +0 -35
- package/components/menuItem.tsx +0 -80
- package/components/modal.tsx +0 -85
- package/components/navigation.tsx +0 -27
- package/components/popover.tsx +0 -69
- package/components/radioGroup.tsx +0 -50
- package/components/select.tsx +0 -253
- package/components/tab.tsx +0 -85
- package/components/tableCell.tsx +0 -15
- package/components/tableContainer.tsx +0 -15
- package/components/tableHeader.tsx +0 -15
- package/components/tableHeaderCell.tsx +0 -15
- package/components/tableRow.tsx +0 -15
- package/components/tabsContainer.tsx +0 -23
- package/components/tag.tsx +0 -81
- package/components/textField.tsx +0 -116
- package/components/textarea.tsx +0 -120
- package/components/timeStamp.tsx +0 -65
- package/components/tooltip.tsx +0 -66
- package/components/userImage.tsx +0 -64
- package/designTokens.js +0 -234
- package/index.css +0 -8
- package/index.ts +0 -21
- package/next.config.mjs +0 -4
- package/oneslash-design-system-1.1.1.tgz +0 -0
- package/oneslash-design-system-1.1.30.tgz +0 -0
- package/oneslash-design-system-1.1.31.tgz +0 -0
- package/oneslash-design-system-1.1.33.tgz +0 -0
- package/postcss.config.mjs +0 -8
- package/tailwind.config.ts +0 -232
- package/tsconfig.json +0 -37
package/components/tag.tsx
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
-
import * as HeroIcons from '@heroicons/react/24/outline';
|
|
4
|
-
|
|
5
|
-
interface TagProps {
|
|
6
|
-
variant: 'contained' | 'textOnly';
|
|
7
|
-
size: 'medium' | 'small';
|
|
8
|
-
state?: 'enabled' | 'selected';
|
|
9
|
-
label: React.ReactNode;
|
|
10
|
-
iconName?: keyof typeof HeroIcons;
|
|
11
|
-
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
12
|
-
color?: 'default' | 'info';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
16
|
-
|
|
17
|
-
export default function Tag({
|
|
18
|
-
variant,
|
|
19
|
-
size,
|
|
20
|
-
state = 'enabled',
|
|
21
|
-
label,
|
|
22
|
-
iconName,
|
|
23
|
-
onClick,
|
|
24
|
-
color = 'default',
|
|
25
|
-
}: TagProps): JSX.Element {
|
|
26
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
27
|
-
const [Icon, setIcon] = useState<IconType | null>(null);
|
|
28
|
-
|
|
29
|
-
// Load icon directly from HeroIcons
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (iconName) {
|
|
32
|
-
setIcon(HeroIcons[iconName] as IconType);
|
|
33
|
-
}
|
|
34
|
-
}, [iconName]);
|
|
35
|
-
|
|
36
|
-
// Size and padding
|
|
37
|
-
const sizeClasses = size === 'medium' ? 'text-body2 px-2 py-1' : 'text-caption px-2 py-[3px]';
|
|
38
|
-
|
|
39
|
-
// Background color
|
|
40
|
-
const bgClasses = variant === 'contained'
|
|
41
|
-
? (color === 'info'
|
|
42
|
-
? 'bg-light-info-main dark:bg-dark-info-main'
|
|
43
|
-
: 'bg-light-background-accent300 dark:bg-dark-background-accent300')
|
|
44
|
-
: '';
|
|
45
|
-
|
|
46
|
-
// Font color
|
|
47
|
-
const fontClasses = variant === 'textOnly'
|
|
48
|
-
? (color === 'info'
|
|
49
|
-
? 'text-light-info-main dark:text-dark-info-main'
|
|
50
|
-
: 'text-light-text-primary dark:text-dark-text-primary')
|
|
51
|
-
: 'text-light-text-primary dark:text-dark-text-primary';
|
|
52
|
-
|
|
53
|
-
// Border for contained variant
|
|
54
|
-
const borderClasses = variant === 'contained'
|
|
55
|
-
? 'border border-light-misc-divider dark:border-dark-misc-divider'
|
|
56
|
-
: '';
|
|
57
|
-
|
|
58
|
-
// State and hover
|
|
59
|
-
const stateClasses = state === 'selected'
|
|
60
|
-
? 'bg-light-accent-main dark:bg-dark-accent-main text-white'
|
|
61
|
-
: 'cursor-pointer';
|
|
62
|
-
const hoverClasses = variant === 'contained' && isHovered
|
|
63
|
-
? 'bg-light-background-accent200 dark:bg-dark-background-accent200'
|
|
64
|
-
: '';
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<div
|
|
68
|
-
className={`
|
|
69
|
-
flex items-center space-x-1 rounded-full
|
|
70
|
-
${sizeClasses} ${bgClasses} ${fontClasses} ${borderClasses} ${stateClasses} ${hoverClasses}
|
|
71
|
-
transition-colors duration-200 ease-in-out
|
|
72
|
-
`}
|
|
73
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
74
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
75
|
-
onClick={onClick}
|
|
76
|
-
>
|
|
77
|
-
{Icon && <Icon className="w-4 h-4" />}
|
|
78
|
-
<span>{label}</span>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
package/components/textField.tsx
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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;
|
|
12
|
-
autoFocus?: boolean;
|
|
13
|
-
multiline?: boolean;
|
|
14
|
-
maxRows?: number;
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
error?: boolean;
|
|
17
|
-
size?: 'large' | 'medium' | 'small';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export default function TextField({
|
|
21
|
-
id,
|
|
22
|
-
label,
|
|
23
|
-
value,
|
|
24
|
-
onChange,
|
|
25
|
-
onBlur,
|
|
26
|
-
onFocus,
|
|
27
|
-
onKeyDown,
|
|
28
|
-
autoFocus = false, // Accept the autoFocus prop with default value
|
|
29
|
-
multiline = false,
|
|
30
|
-
maxRows = 6,
|
|
31
|
-
disabled = false,
|
|
32
|
-
error = false,
|
|
33
|
-
size = 'medium',
|
|
34
|
-
}: TextFieldProps) {
|
|
35
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
36
|
-
|
|
37
|
-
// Define classes for size: text size and padding
|
|
38
|
-
const sizeClasses = {
|
|
39
|
-
large: 'text-body1 p-[7px] leading-[22px]', // body1 (16px), padding 8px(7 + border 1) height 40
|
|
40
|
-
medium: 'text-body1 p-[3px] leading-[22px]', // body1 (16px), padding 4px(3 + border 1), height 32
|
|
41
|
-
small: 'text-body2 p-[3px] leading-[18px]', // body2 (14px), padding 4px(3 + border 1), height 28
|
|
42
|
-
}[size];
|
|
43
|
-
|
|
44
|
-
const baseClasses = 'w-full border rounded-[8px]';
|
|
45
|
-
const bgColor = 'bg-light-background-default dark:bg-dark-background-default transition-colors duration-200 ease-in-out';
|
|
46
|
-
const borderColor = 'border-light-outlinedBorder-active dark:border-dark-outlinedBorder-active';
|
|
47
|
-
const containerClasses = `
|
|
48
|
-
${bgColor}
|
|
49
|
-
${borderColor}
|
|
50
|
-
${baseClasses}
|
|
51
|
-
${sizeClasses}
|
|
52
|
-
${disabled ? 'bg-gray-200 cursor-not-allowed' : ''}
|
|
53
|
-
${error ? 'border-red-500 focus:ring-red-500' : ''}
|
|
54
|
-
${isFocused ? 'focus:border-light-accent-main focus:dark:border-dark-accent-main outline-none' : ''}
|
|
55
|
-
${!disabled && !error ? 'hover:border-light-outlinedBorder-hover' : ''}
|
|
56
|
-
border-gray-300
|
|
57
|
-
`;
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div className="flex flex-col w-full">
|
|
61
|
-
{label && (
|
|
62
|
-
<label htmlFor={id} className="mb-1 text-body2 text-light-text-secondary dark:text-dark-text-secondary">
|
|
63
|
-
{label}
|
|
64
|
-
</label>
|
|
65
|
-
)}
|
|
66
|
-
<div className="relative">
|
|
67
|
-
{multiline ? (
|
|
68
|
-
<textarea
|
|
69
|
-
id={id}
|
|
70
|
-
rows={maxRows}
|
|
71
|
-
className={containerClasses}
|
|
72
|
-
value={value}
|
|
73
|
-
onChange={onChange}
|
|
74
|
-
onFocus={(e) => {
|
|
75
|
-
setIsFocused(true);
|
|
76
|
-
if (onFocus) onFocus(e);
|
|
77
|
-
}}
|
|
78
|
-
onBlur={(e) => {
|
|
79
|
-
setIsFocused(false);
|
|
80
|
-
if (onBlur) onBlur(e);
|
|
81
|
-
}}
|
|
82
|
-
onKeyDown={onKeyDown}
|
|
83
|
-
autoFocus={autoFocus} // Pass autoFocus to textarea
|
|
84
|
-
disabled={disabled}
|
|
85
|
-
autoComplete="off" // Disable browser autocomplete/autofill
|
|
86
|
-
/>
|
|
87
|
-
) : (
|
|
88
|
-
<input
|
|
89
|
-
id={id}
|
|
90
|
-
type="text"
|
|
91
|
-
className={containerClasses}
|
|
92
|
-
value={value}
|
|
93
|
-
onChange={onChange}
|
|
94
|
-
onFocus={(e) => {
|
|
95
|
-
setIsFocused(true);
|
|
96
|
-
if (onFocus) onFocus(e);
|
|
97
|
-
}}
|
|
98
|
-
onBlur={(e) => {
|
|
99
|
-
setIsFocused(false);
|
|
100
|
-
if (onBlur) onBlur(e);
|
|
101
|
-
}}
|
|
102
|
-
onKeyDown={onKeyDown}
|
|
103
|
-
autoFocus={autoFocus} // Pass autoFocus to input
|
|
104
|
-
disabled={disabled}
|
|
105
|
-
autoComplete="off" // Disable browser autocomplete/autofill
|
|
106
|
-
/>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
{error && (
|
|
110
|
-
<p className="mt-1 text-light-error-main text-body2">
|
|
111
|
-
This field is required
|
|
112
|
-
</p>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|
package/components/textarea.tsx
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
interface TextareaProps {
|
|
5
|
-
id: string;
|
|
6
|
-
label?: string;
|
|
7
|
-
value: string;
|
|
8
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
9
|
-
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
10
|
-
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
|
11
|
-
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
12
|
-
autoFocus?: boolean;
|
|
13
|
-
maxRows?: number;
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
error?: boolean;
|
|
16
|
-
size?: 'large' | 'medium' | 'small';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default function Textarea({
|
|
20
|
-
id,
|
|
21
|
-
label,
|
|
22
|
-
value,
|
|
23
|
-
onChange,
|
|
24
|
-
onBlur,
|
|
25
|
-
onFocus,
|
|
26
|
-
onKeyDown,
|
|
27
|
-
autoFocus = false,
|
|
28
|
-
maxRows = 6,
|
|
29
|
-
disabled = false,
|
|
30
|
-
error = false,
|
|
31
|
-
size = 'medium',
|
|
32
|
-
}: TextareaProps) {
|
|
33
|
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
34
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const textarea = textareaRef.current;
|
|
38
|
-
if (!textarea) return;
|
|
39
|
-
|
|
40
|
-
const adjustHeight = () => {
|
|
41
|
-
textarea.style.height = 'auto'; // Reset height to calculate scrollHeight
|
|
42
|
-
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);
|
|
43
|
-
const maxHeight = lineHeight * maxRows;
|
|
44
|
-
const scrollHeight = textarea.scrollHeight;
|
|
45
|
-
|
|
46
|
-
// Set height to scrollHeight, capped at maxRows, but at least 1 line
|
|
47
|
-
textarea.style.height = `${Math.max(Math.min(scrollHeight, maxHeight), lineHeight)}px`;
|
|
48
|
-
|
|
49
|
-
// Enable vertical scroll if content exceeds maxRows
|
|
50
|
-
textarea.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Set initial rows to 1 for single-line height
|
|
54
|
-
textarea.rows = 1;
|
|
55
|
-
|
|
56
|
-
adjustHeight();
|
|
57
|
-
textarea.addEventListener('input', adjustHeight);
|
|
58
|
-
|
|
59
|
-
return () => textarea.removeEventListener('input', adjustHeight);
|
|
60
|
-
}, [maxRows]);
|
|
61
|
-
|
|
62
|
-
// Define classes for size: text size and padding
|
|
63
|
-
const sizeClasses = {
|
|
64
|
-
large: 'text-body1 p-[7px] leading-[22px]', // body1 (16px), padding 8px(7 + border 1) height 40
|
|
65
|
-
medium: 'text-body1 p-[3px] leading-[22px]', // body1 (16px), padding 4px(3 + border 1), height 32
|
|
66
|
-
small: 'text-body2 p-[3px] leading-[18px]', // body2 (14px), padding 4px(3 + border 1), height 28
|
|
67
|
-
}[size];
|
|
68
|
-
|
|
69
|
-
const baseClasses = 'w-full border rounded-[8px]';
|
|
70
|
-
const bgColor = 'bg-light-background-default dark:bg-dark-background-default transition-colors duration-200 ease-in-out';
|
|
71
|
-
const borderColor = 'border-light-outlinedBorder-active dark:border-dark-outlinedBorder-active';
|
|
72
|
-
const containerClasses = `
|
|
73
|
-
${bgColor}
|
|
74
|
-
${borderColor}
|
|
75
|
-
${baseClasses}
|
|
76
|
-
${sizeClasses}
|
|
77
|
-
${disabled ? 'bg-gray-200 cursor-not-allowed' : ''}
|
|
78
|
-
${error ? 'border-red-500 focus:ring-red-500' : ''}
|
|
79
|
-
${isFocused ? 'focus:border-light-accent-main focus:dark:border-dark-accent-main outline-none' : ''}
|
|
80
|
-
${!disabled && !error ? 'hover:border-light-outlinedBorder-hover' : ''}
|
|
81
|
-
border-gray-300
|
|
82
|
-
`;
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div className="flex flex-col w-full">
|
|
86
|
-
{label && (
|
|
87
|
-
<label htmlFor={id} className="mb-1 text-body2 text-light-text-secondary dark:text-dark-text-secondary">
|
|
88
|
-
{label}
|
|
89
|
-
</label>
|
|
90
|
-
)}
|
|
91
|
-
<div className="relative">
|
|
92
|
-
<textarea
|
|
93
|
-
ref={textareaRef}
|
|
94
|
-
id={id}
|
|
95
|
-
rows={1}
|
|
96
|
-
className={containerClasses}
|
|
97
|
-
value={value}
|
|
98
|
-
onChange={onChange}
|
|
99
|
-
onFocus={(e) => {
|
|
100
|
-
setIsFocused(true);
|
|
101
|
-
if (onFocus) onFocus(e);
|
|
102
|
-
}}
|
|
103
|
-
onBlur={(e) => {
|
|
104
|
-
setIsFocused(false);
|
|
105
|
-
if (onBlur) onBlur(e);
|
|
106
|
-
}}
|
|
107
|
-
onKeyDown={onKeyDown}
|
|
108
|
-
autoFocus={autoFocus}
|
|
109
|
-
disabled={disabled}
|
|
110
|
-
autoComplete="off"
|
|
111
|
-
/>
|
|
112
|
-
</div>
|
|
113
|
-
{error && (
|
|
114
|
-
<p className="mt-1 text-light-error-main text-body2">
|
|
115
|
-
This field is required
|
|
116
|
-
</p>
|
|
117
|
-
)}
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
}
|
package/components/timeStamp.tsx
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
};
|
package/components/tooltip.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
-
|
|
4
|
-
interface TooltipProps {
|
|
5
|
-
title: string;
|
|
6
|
-
children: React.ReactElement;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function Tooltip({ title, children }: TooltipProps) {
|
|
10
|
-
const [visible, setVisible] = useState(false);
|
|
11
|
-
const [position, setPosition] = useState<'top' | 'bottom'>('bottom');
|
|
12
|
-
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
13
|
-
const buttonRef = useRef<HTMLDivElement>(null);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const handlePosition = () => {
|
|
17
|
-
if (tooltipRef.current && buttonRef.current) {
|
|
18
|
-
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
19
|
-
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
20
|
-
// Check if there's enough space below; if not, place tooltip above
|
|
21
|
-
if (window.innerHeight - buttonRect.bottom < tooltipRect.height + 8) {
|
|
22
|
-
setPosition('top');
|
|
23
|
-
} else {
|
|
24
|
-
setPosition('bottom');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
if (visible) {
|
|
29
|
-
handlePosition();
|
|
30
|
-
}
|
|
31
|
-
}, [visible]);
|
|
32
|
-
|
|
33
|
-
const handleClick = () => {
|
|
34
|
-
setVisible(false); // Hide tooltip on click
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div
|
|
39
|
-
ref={buttonRef}
|
|
40
|
-
onMouseEnter={() => setVisible(true)}
|
|
41
|
-
onMouseLeave={() => setVisible(false)}
|
|
42
|
-
onClick={handleClick}
|
|
43
|
-
className="relative inline-block"
|
|
44
|
-
>
|
|
45
|
-
{children}
|
|
46
|
-
{visible && (
|
|
47
|
-
<div
|
|
48
|
-
ref={tooltipRef}
|
|
49
|
-
className={`absolute whitespace-nowrap text-caption rounded-[8px] py-1 px-2 z-50
|
|
50
|
-
dark:bg-light-background-accent300 bg-dark-background-accent300
|
|
51
|
-
dark:text-light-text-primary text-dark-text-primary
|
|
52
|
-
${position === 'bottom' ? 'mt-1' : 'mb-1'}`}
|
|
53
|
-
style={{
|
|
54
|
-
bottom: position === 'top' ? '100%' : undefined,
|
|
55
|
-
top: position === 'bottom' ? '100%' : undefined,
|
|
56
|
-
left: '50%',
|
|
57
|
-
transform: 'translateX(-50%)',
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
{title}
|
|
61
|
-
</div>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
package/components/userImage.tsx
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
interface UserImageProps {
|
|
5
|
-
userHandle: string;
|
|
6
|
-
userImgUrl?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function getColorSeed(userHandle: string): string {
|
|
10
|
-
const words = userHandle.trim().split(/\s+/);
|
|
11
|
-
let letters = words.map(word => word.charAt(0).toLowerCase());
|
|
12
|
-
if (letters.length === 1 && words[0].length >= 2) {
|
|
13
|
-
letters.push(words[0].charAt(1).toLowerCase());
|
|
14
|
-
} else if (letters.length > 2) {
|
|
15
|
-
letters = letters.slice(0, 2);
|
|
16
|
-
} else if (letters.length === 0) {
|
|
17
|
-
letters = ['x', 'x'];
|
|
18
|
-
}
|
|
19
|
-
return letters.join('');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getHash(str: string): number {
|
|
23
|
-
let hash = 0;
|
|
24
|
-
for (let i = 0; i < str.length; i++) {
|
|
25
|
-
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
26
|
-
}
|
|
27
|
-
return hash;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function UserImage({
|
|
31
|
-
userHandle,
|
|
32
|
-
userImgUrl,
|
|
33
|
-
}: UserImageProps) {
|
|
34
|
-
const displayInitial = userHandle.charAt(0).toUpperCase() || 'A';
|
|
35
|
-
const seed = getColorSeed(userHandle);
|
|
36
|
-
const hue = Math.abs(getHash(seed)) % 360;
|
|
37
|
-
|
|
38
|
-
// Light mode: vibrant pastel
|
|
39
|
-
const lightBg = `hsl(${hue}, 80%, 80%)`;
|
|
40
|
-
// Dark mode: darker, vibrant variant
|
|
41
|
-
const darkBg = `hsl(${hue}, 80%, 30%)`;
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div
|
|
45
|
-
style={{
|
|
46
|
-
'--light-bg': lightBg,
|
|
47
|
-
'--dark-bg': darkBg,
|
|
48
|
-
} as React.CSSProperties}
|
|
49
|
-
className="flex items-center justify-center w-6 h-6 rounded-full overflow-hidden bg-[var(--light-bg)] dark:bg-[var(--dark-bg)] text-light-text-secondary dark:text-dark-text-secondary"
|
|
50
|
-
>
|
|
51
|
-
{userImgUrl ? (
|
|
52
|
-
<img
|
|
53
|
-
src={userImgUrl}
|
|
54
|
-
alt={userHandle}
|
|
55
|
-
className="w-full h-full object-cover rounded-full"
|
|
56
|
-
/>
|
|
57
|
-
) : (
|
|
58
|
-
<span className="text-body1 text-light-text-secondary dark:text-dark-text-secondary">
|
|
59
|
-
{displayInitial}
|
|
60
|
-
</span>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|