oneslash-design-system 1.0.4 → 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.
@@ -2,7 +2,7 @@
2
2
  import React, { useEffect } from 'react';
3
3
 
4
4
  interface AlertProps {
5
- open: boolean;
5
+ open?: boolean;
6
6
  type: 'success' | 'warning' | 'error' | 'info';
7
7
  message: string;
8
8
  onClose: () => void;
@@ -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,5 +1,6 @@
1
1
  'use client';
2
- import React, { useState } from 'react';
2
+
3
+ import { useState } from 'react';
3
4
 
4
5
  interface CheckboxProps {
5
6
  label?: string;
@@ -12,7 +13,6 @@ export default function Checkbox({
12
13
  checked = false,
13
14
  onChange
14
15
  }: CheckboxProps) {
15
-
16
16
  const [isChecked, setIsChecked] = useState(checked);
17
17
 
18
18
  const handleToggle = () => {
@@ -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
+ }
@@ -6,19 +6,36 @@ type TabProps = {
6
6
  label: string;
7
7
  href?: string;
8
8
  isSelected: boolean;
9
- onClick: () => void;
10
- iconName?: string;
9
+ onClickTab: () => void;
10
+ onClickActionIcon?: any;
11
+ decoIcon?: string;
12
+ actionIcon?: string;
11
13
  };
12
14
 
13
- const TabIcon: React.FC<{ iconName?: string }> = ({ iconName }) => {
14
- const [Icon, setIcon] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
15
+ type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
15
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
16
33
  const loadIcon = useCallback(async (iconName?: string) => {
17
34
  if (!iconName) return null;
18
35
  try {
19
36
  const module = await import('@heroicons/react/24/outline');
20
- const IconComponent = module[iconName as keyof typeof module] as React.ComponentType<React.SVGProps<SVGSVGElement>>;
21
- return IconComponent || null;
37
+ const Icon = module[iconName as keyof typeof module] as IconType;
38
+ return Icon || null;
22
39
  } catch (error) {
23
40
  console.error(`Failed to load icon ${iconName}:`, error);
24
41
  return null;
@@ -26,23 +43,19 @@ const TabIcon: React.FC<{ iconName?: string }> = ({ iconName }) => {
26
43
  }, []);
27
44
 
28
45
  useEffect(() => {
29
- const fetchIcon = async () => {
30
- if (iconName) {
31
- setIcon(await loadIcon(iconName));
46
+ const fetchIcons = async () => {
47
+ if (decoIcon) {
48
+ setIconLeft(await loadIcon(decoIcon));
49
+ }
50
+ if (actionIcon) {
51
+ setIconRight(await loadIcon(actionIcon));
32
52
  }
33
53
  };
34
- fetchIcon();
35
- }, [iconName, loadIcon]);
36
-
37
- return Icon ? <Icon className="w-5 h-5" /> : null;
38
- };
39
-
40
- const Tab: React.FC<TabProps> = ({ label, href, isSelected, onClick, iconName }) => {
41
- const router = useRouter();
42
- const pathname = usePathname();
54
+ fetchIcons();
55
+ }, [decoIcon, actionIcon, loadIcon]);
43
56
 
44
57
  const handleClick = () => {
45
- onClick();
58
+ onClickTab();
46
59
  if (href) {
47
60
  router.push(href);
48
61
  }
@@ -58,12 +71,15 @@ const Tab: React.FC<TabProps> = ({ label, href, isSelected, onClick, iconName })
58
71
  `}
59
72
  onClick={handleClick}
60
73
  >
61
- {iconName && <TabIcon iconName={iconName} />}
62
- <span className="whitespace-nowrap text-body1">
74
+ {IconLeft && <IconLeft className="w-5 h-5" />}
75
+ <span className="whitespace-nowrap text-body1 px-2">
63
76
  {label}
64
77
  </span>
78
+ {IconRight && (
79
+ <div onClick={onClickActionIcon} className="cursor-pointer">
80
+ <IconRight className="w-5 h-5" />
81
+ </div>
82
+ )}
65
83
  </div>
66
84
  );
67
- };
68
-
69
- export default Tab;
85
+ }
@@ -27,7 +27,6 @@ export default function Tag({
27
27
  onClick,
28
28
  color = 'default',
29
29
  }: TagProps) {
30
-
31
30
  const [isHovered, setIsHovered] = useState(false);
32
31
  const [Icon, setIcon] = useState<IconType | null>(null);
33
32
  const [DeleteIcon, setDeleteIcon] = useState<IconType | null>(null);
@@ -103,6 +102,7 @@ export default function Tag({
103
102
  >
104
103
  {DeleteIcon && <DeleteIcon className="w-4 h-4" />}
105
104
  </button>
105
+
106
106
  )}
107
107
  </div>
108
108
  );
@@ -5,9 +5,13 @@ interface TextFieldProps {
5
5
  id: string;
6
6
  label?: string;
7
7
  value: string;
8
- onChange: any;
9
- iconLeft?: any;
10
- iconRight?: React.ReactNode | React.ComponentType<any>;
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>;
11
15
  multiline?: boolean;
12
16
  maxRows?: number;
13
17
  disabled?: boolean;
@@ -19,6 +23,10 @@ export default function TextField({
19
23
  label,
20
24
  value,
21
25
  onChange,
26
+ onBlur,
27
+ onFocus,
28
+ onKeyDown,
29
+ autoFocus = false, // Accept the autoFocus prop with default value
22
30
  iconLeft,
23
31
  iconRight,
24
32
  multiline = false,
@@ -26,47 +34,31 @@ export default function TextField({
26
34
  disabled = false,
27
35
  error = false,
28
36
  }: TextFieldProps) {
29
-
30
37
  const [isFocused, setIsFocused] = useState(false);
31
38
 
32
- // Base styles
33
39
  const baseClasses = 'w-full border rounded-[8px] p-2';
34
-
35
- // Background color
36
40
  const bgColor = 'bg-light-background-default dark:bg-dark-background-default transition-colors duration-300 ease-in-out';
37
-
38
- // Border color
39
41
  const borderColor = 'border-light-outlinedBorder-active dark:border-dark-outlinedBorder-active';
40
-
41
- // State styles
42
- const disabledClasses = 'bg-gray-200 cursor-not-allowed';
43
- const errorClasses = 'border-red-500 focus:ring-red-500';
44
- const focusClasses = 'focus:border-light-accent-main focus:dark:border-dark-accent-main outline-none';
45
- const hoverClasses = 'hover:border-light-outlinedBorder-hover';
46
- const defaultClasses = 'border-gray-300';
47
-
48
- // Container styles
49
42
  const containerClasses = `
50
43
  ${bgColor}
51
44
  ${borderColor}
52
45
  ${baseClasses}
53
- ${disabled ? disabledClasses : ''}
54
- ${error ? errorClasses : ''}
55
- ${isFocused ? focusClasses : ''}
56
- ${!disabled && !error ? hoverClasses : ''}
57
- ${defaultClasses}
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
58
51
  `;
59
52
 
60
- // Render iconRight in TextField
61
- const renderIconRight = () => {
62
- if (React.isValidElement(iconRight)) {
63
- return iconRight;
64
- }
65
- if (typeof iconRight === 'function') {
66
- return React.createElement(iconRight);
67
- }
68
- return null;
69
- };
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
+ };
70
62
 
71
63
  return (
72
64
  <div className="flex flex-col w-full">
@@ -93,8 +85,16 @@ export default function TextField({
93
85
  className={containerClasses}
94
86
  value={value}
95
87
  onChange={onChange}
96
- onFocus={() => setIsFocused(true)}
97
- onBlur={() => setIsFocused(false)}
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
98
  disabled={disabled}
99
99
  />
100
100
  ) : (
@@ -104,8 +104,16 @@ export default function TextField({
104
104
  className={containerClasses}
105
105
  value={value}
106
106
  onChange={onChange}
107
- onFocus={() => setIsFocused(true)}
108
- onBlur={() => setIsFocused(false)}
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
109
117
  disabled={disabled}
110
118
  />
111
119
  )}
@@ -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
+ };
@@ -6,11 +6,7 @@ interface TooltipProps {
6
6
  children: React.ReactElement;
7
7
  }
8
8
 
9
- export default function Tooltip({
10
- title,
11
- children
12
- }: TooltipProps) {
13
-
9
+ export default function Tooltip({ title, children }: TooltipProps) {
14
10
  const [visible, setVisible] = useState(false);
15
11
  const [position, setPosition] = useState<'top' | 'bottom'>('bottom');
16
12
  const tooltipRef = useRef<HTMLDivElement>(null);
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,14 +1,15 @@
1
1
  import './index.css';
2
- export * from './components/button/button';
3
- export * from './components/alert/alert';
4
- export * from './components/button/button';
5
- export * from './components/checkBox/checkBox';
6
- export * from './components/iconButton/iconButton';
7
- export * from './components/menuItem/menuItem';
8
- export * from './components/modal/modal';
9
- export * from './components/popover/popover';
10
- export * from './components/tab/tab';
11
- export * from './components/tag/tag';
12
- export * from './components/textField/textField';
13
- export * from './components/tooltip/tooltip';
14
- export * from './components/userImage/userImage';
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oneslash-design-system",
3
3
  "description": "A design system for the Oneslash projects",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "private": false,
6
6
  "scripts": {
7
7
  "dev": "next dev",
@@ -1,118 +0,0 @@
1
- 'use client';
2
- import React, { useState, useEffect, useCallback } from 'react';
3
- import * as HeroIcons from '@heroicons/react/24/outline';
4
-
5
- interface ButtonProps {
6
- size: 'large' | 'medium' | 'small';
7
- type: 'primary' | 'secondary' | 'tertiary' | 'textOnly';
8
- state: 'enabled' | 'hovered' | 'selected' | 'focused' | 'disabled';
9
- label: string;
10
- iconLeftName?: keyof typeof HeroIcons; // Reference to HeroIcons
11
- iconRightName?: keyof typeof HeroIcons; // Reference to HeroIcons
12
- onClick?: React.MouseEventHandler<HTMLButtonElement>;
13
- }
14
-
15
- type IconType = React.ComponentType<React.SVGProps<SVGSVGElement>>;
16
-
17
- export default function Button({
18
- size,
19
- type,
20
- state,
21
- label,
22
- iconLeftName,
23
- iconRightName,
24
- onClick,
25
- }: ButtonProps) {
26
-
27
- const [isHovered, setIsHovered] = useState(false);
28
- const [IconLeft, setIconLeft] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
29
- const [IconRight, setIconRight] = useState<React.ComponentType<React.SVGProps<SVGSVGElement>> | null>(null);
30
-
31
- // import icon
32
- const loadIcon = useCallback(async (iconName?: string) => {
33
- if (!iconName) return null;
34
- try {
35
- const module = await import('@heroicons/react/24/outline');
36
- const Icon = module[iconName as keyof typeof module] as IconType;
37
- return Icon || null;
38
- } catch (error) {
39
- console.error(`Failed to load icon ${iconName}:`, error);
40
- return null;
41
- }
42
- }, []);
43
-
44
- // Load icons on mount and when props change
45
- useEffect(() => {
46
- const fetchIcons = async () => {
47
- if (typeof iconLeftName === 'string') {
48
- setIconLeft(await loadIcon(iconLeftName));
49
- }
50
- if (typeof iconRightName === 'string') {
51
- setIconRight(await loadIcon(iconRightName));
52
- }
53
- };
54
- fetchIcons();
55
- }, [iconLeftName, iconRightName, loadIcon]);
56
-
57
- // size
58
- const sizeClasses =
59
- size === 'large' ? 'text-body1 p-2'
60
- : size === 'small' ? 'text-body2 p-1'
61
- : 'text-body1 p-1'; // medium size
62
-
63
- // icon size
64
- const sizeIcon =
65
- size === 'large' ? 'w-6 h-6'
66
- : size === 'small' ? 'w-4 h-4'
67
- : 'w-5 h-5'; // medium size
68
-
69
- // type
70
- const typeClasses = {
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
85
- const stateClasses = {
86
- enabled: 'cursor-pointer',
87
- hovered: 'cursor-pointer',
88
- selected: 'bg-light-primary-main dark:bg-dark-primary-main text-light-text-contrast dark:text-dark-text-contrast',
89
- focused: 'ring-2 ring-light-accent-main dark:ring-dark-accent-main',
90
- disabled: 'text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
91
- }[state];
92
-
93
-
94
-
95
- return (
96
- <button
97
- className={`flex flex-row space-x-2 items-center rounded-[8px]
98
- ${sizeClasses}
99
- ${typeClasses}
100
- ${state !== 'disabled' && isHovered ? hoverTypeClasses : ''}
101
- ${stateClasses}`}
102
- disabled={state === 'disabled'}
103
- onMouseEnter={() => {
104
- if (state !== 'disabled') setIsHovered(true);
105
- }}
106
- onMouseLeave={() => {
107
- if (state !== 'disabled') setIsHovered(false);
108
- }}
109
- onClick={onClick}
110
- >
111
- {IconLeft && <IconLeft className={sizeIcon} />}
112
- <div className="whitespace-nowrap px-2">
113
- {label}
114
- </div>
115
- {IconRight && <IconRight className={sizeIcon} />}
116
- </button>
117
- );
118
- }
@@ -1,49 +0,0 @@
1
- 'use client';
2
- import React from 'react';
3
-
4
- interface ModalProps {
5
- isOpen: boolean;
6
- title: string;
7
- children: React.ReactNode;
8
- onClose: () => void;
9
- actions?: React.ReactNode;
10
- }
11
-
12
- export default function Modal({
13
- isOpen,
14
- title,
15
- children,
16
- onClose,
17
- actions,
18
- }: ModalProps) {
19
-
20
- if (!isOpen) return null;
21
-
22
- const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
23
- if (e.target === e.currentTarget) {
24
- onClose();
25
- }
26
- };
27
-
28
- return (
29
- <div className="fixed inset-[-32px] bg-black bg-opacity-50 flex items-center justify-center z-50 "
30
- onClick={handleOverlayClick}
31
- >
32
- <div className="bg-light-background-default dark:bg-dark-background-default p-6 rounded-[8px] space-y-4 w-[600px]"
33
- style={{
34
- position: 'relative',
35
- margin: '0 auto'
36
- }}>
37
- <h2 className="text-h6">
38
- {title}
39
- </h2>
40
- <div className="text-body1 space-y-4">
41
- {children}
42
- </div>
43
- <div className="flex justify-between">
44
- {actions}
45
- </div>
46
- </div>
47
- </div>
48
- );
49
- }
@@ -1,88 +0,0 @@
1
- 'use client';
2
- import React, { useState, useRef, useEffect } 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,
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
- useEffect(() => {
24
- const handleClickOutside = (event: MouseEvent) => {
25
- if (popoverRef.current && !popoverRef.current.contains(event.target as Node) && anchorEl) {
26
- 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
38
- useEffect(() => {
39
- if (anchorEl && open) {
40
- const anchorRect = anchorEl.getBoundingClientRect();
41
- const popoverRect = popoverRef.current?.getBoundingClientRect();
42
- if (popoverRect) {
43
- const spaceBelow = window.innerHeight - anchorRect.bottom;
44
- const spaceAbove = anchorRect.top;
45
-
46
- // Determine if we should place the popover above
47
- const shouldPlaceAbove = spaceBelow < popoverRect.height + 8 && spaceAbove > popoverRect.height + 8;
48
-
49
- // Calculate top position, correcting for popover height when placing above
50
- const topPosition = shouldPlaceAbove
51
- ? anchorRect.top + window.scrollY - popoverRect.height - 8 // Adjust by 8px to maintain spacing
52
- : anchorRect.bottom + window.scrollY + 8; // Add 8px for spacing below
53
-
54
- // Calculate left position with viewport boundary checks
55
- let leftPosition = anchorRect.left + (anchorRect.width / 2) - (popoverRect.width / 2);
56
-
57
- if (leftPosition < 8) {
58
- // Adjust to align with the left boundary of the viewport
59
- leftPosition = 8;
60
- } else if (leftPosition + popoverRect.width > window.innerWidth - 8) {
61
- // Adjust to align with the right boundary of the viewport
62
- leftPosition = window.innerWidth - popoverRect.width - 8;
63
- }
64
-
65
- setPopoverStyle({
66
- position: 'absolute',
67
- top: topPosition,
68
- left: leftPosition,
69
- zIndex: 10000,
70
- pointerEvents: 'auto',
71
- });
72
- }
73
- }
74
- }, [anchorEl, open]);
75
-
76
- if (!open || !anchorEl) return null;
77
-
78
- return (
79
- <div
80
- ref={popoverRef}
81
- style={popoverStyle}
82
- className="bg-light-background-default dark:bg-dark-background-default border rounded-[8px] shadow-lg"
83
- role="dialog"
84
- >
85
- {children}
86
- </div>
87
- );
88
- }