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.
@@ -1,7 +1,20 @@
1
1
  'use client';
2
2
  import React, { useEffect } from 'react';
3
3
 
4
- export default function Alert({ open, type, message, onClose }: AlertProps) {
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');
@@ -1,5 +1,11 @@
1
+ 'use client';
1
2
  import React from 'react';
2
3
 
4
+ interface UserImageProps {
5
+ userHandle: string;
6
+ userImgUrl?: string;
7
+ }
8
+
3
9
  export default function UserImage({
4
10
  userHandle,
5
11
  userImgUrl,
package/global.d.ts CHANGED
@@ -122,7 +122,7 @@ declare global {
122
122
 
123
123
  interface AlertProps {
124
124
  open: boolean;
125
- type: string;
125
+ type: 'success' | 'warning' | 'error' | 'info';
126
126
  message: string;
127
127
  onClose: () => void;
128
128
  }
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 Button from './components/button/button';
3
- export { Button };
4
-
5
- export * from './components/alert/alert';
6
- export * from './components/button/button';
7
- export * from './components/checkBox/checkBox';
8
- export * from './components/iconButton/iconButton';
9
- export * from './components/menuItem/menuItem';
10
- export * from './components/modal/modal';
11
- export * from './components/popover/popover';
12
- export * from './components/tab/tab';
13
- export * from './components/tag/tag';
14
- export * from './components/textField/textField';
15
- export * from './components/tooltip/tooltip';
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
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "oneslash-design-system",
3
- "version": "1.0.3",
3
+ "description": "A design system for the Oneslash projects",
4
+ "version": "1.0.6",
4
5
  "private": false,
5
6
  "scripts": {
6
7
  "dev": "next dev",
package/tsconfig.json CHANGED
@@ -32,9 +32,10 @@
32
32
  "next-env.d.ts",
33
33
  "**/*.ts",
34
34
  "**/*.tsx",
35
- ".next/types/**/*.ts"
35
+ ".next/types/**/*.ts",
36
+ "types/**/*.d.ts"
36
37
  ],
37
38
  "exclude": [
38
39
  "node_modules"
39
40
  ]
40
- }
41
+ }
@@ -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
- }
@@ -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
- }