@umituz/web-design-system 1.0.1 → 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/package.json +1 -1
- package/src/domain/tokens/animation.tokens.ts +87 -0
- package/src/domain/tokens/index.ts +11 -0
- package/src/presentation/atoms/Checkbox.tsx +50 -0
- package/src/presentation/atoms/Divider.tsx +34 -0
- package/src/presentation/atoms/Link.tsx +45 -0
- package/src/presentation/atoms/Progress.tsx +48 -0
- package/src/presentation/atoms/Radio.tsx +42 -0
- package/src/presentation/atoms/Skeleton.tsx +38 -0
- package/src/presentation/atoms/Slider.tsx +49 -0
- package/src/presentation/atoms/Tooltip.tsx +76 -0
- package/src/presentation/atoms/index.ts +24 -0
- package/src/presentation/hooks/index.ts +13 -0
- package/src/presentation/hooks/useClickOutside.ts +33 -0
- package/src/presentation/hooks/useClipboard.ts +38 -0
- package/src/presentation/hooks/useDebounce.ts +22 -0
- package/src/presentation/hooks/useKeyboard.ts +89 -0
- package/src/presentation/hooks/useScrollLock.ts +26 -0
- package/src/presentation/hooks/useToggle.ts +16 -0
- package/src/presentation/molecules/CheckboxGroup.tsx +68 -0
- package/src/presentation/molecules/InputGroup.tsx +65 -0
- package/src/presentation/molecules/RadioGroup.tsx +63 -0
- package/src/presentation/molecules/Select.tsx +41 -0
- package/src/presentation/molecules/Textarea.tsx +43 -0
- package/src/presentation/molecules/index.ts +15 -0
- package/src/presentation/organisms/Accordion.tsx +117 -0
- package/src/presentation/organisms/Breadcrumb.tsx +83 -0
- package/src/presentation/organisms/Table.tsx +120 -0
- package/src/presentation/organisms/Tabs.tsx +99 -0
- package/src/presentation/organisms/index.ts +21 -0
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Tokens
|
|
3
|
+
* @description Animation durations, easings, and keyframes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type DurationToken =
|
|
7
|
+
| 'instant'
|
|
8
|
+
| 'fast'
|
|
9
|
+
| 'normal'
|
|
10
|
+
| 'slow'
|
|
11
|
+
| 'slower';
|
|
12
|
+
|
|
13
|
+
export type EasingToken =
|
|
14
|
+
| 'linear'
|
|
15
|
+
| 'ease'
|
|
16
|
+
| 'easeIn'
|
|
17
|
+
| 'easeOut'
|
|
18
|
+
| 'easeInOut'
|
|
19
|
+
| 'bounce'
|
|
20
|
+
| 'elastic';
|
|
21
|
+
|
|
22
|
+
export const durations: Record<DurationToken, string> = {
|
|
23
|
+
instant: '150ms',
|
|
24
|
+
fast: '200ms',
|
|
25
|
+
normal: '300ms',
|
|
26
|
+
slow: '500ms',
|
|
27
|
+
slower: '700ms',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const easings: Record<EasingToken, string> = {
|
|
31
|
+
linear: 'linear',
|
|
32
|
+
ease: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
33
|
+
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
34
|
+
easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
|
|
35
|
+
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
36
|
+
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
|
37
|
+
elastic: 'cubic-bezier(0.87, 0, 0.13, 1)',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const animations = {
|
|
41
|
+
fade: 'fade 300ms ease-out',
|
|
42
|
+
slide: 'slide 300ms ease-out',
|
|
43
|
+
scale: 'scale 200ms ease-out',
|
|
44
|
+
spin: 'spin 1s linear infinite',
|
|
45
|
+
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
|
|
46
|
+
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
47
|
+
bounce: 'bounce 1s infinite',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const keyframes = {
|
|
51
|
+
fade: {
|
|
52
|
+
from: { opacity: '0' },
|
|
53
|
+
to: { opacity: '1' },
|
|
54
|
+
},
|
|
55
|
+
slide: {
|
|
56
|
+
from: { transform: 'translateY(-10px)', opacity: '0' },
|
|
57
|
+
to: { transform: 'translateY(0)', opacity: '1' },
|
|
58
|
+
},
|
|
59
|
+
scale: {
|
|
60
|
+
from: { transform: 'scale(0.95)', opacity: '0' },
|
|
61
|
+
to: { transform: 'scale(1)', opacity: '1' },
|
|
62
|
+
},
|
|
63
|
+
spin: {
|
|
64
|
+
from: { transform: 'rotate(0deg)' },
|
|
65
|
+
to: { transform: 'rotate(360deg)' },
|
|
66
|
+
},
|
|
67
|
+
ping: {
|
|
68
|
+
'75%, 100%': {
|
|
69
|
+
transform: 'scale(2)',
|
|
70
|
+
opacity: '0',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
pulse: {
|
|
74
|
+
'0%, 100%': { opacity: '1' },
|
|
75
|
+
'50%': { opacity: '0.5' },
|
|
76
|
+
},
|
|
77
|
+
bounce: {
|
|
78
|
+
'0%, 100%': {
|
|
79
|
+
transform: 'translateY(-25%)',
|
|
80
|
+
animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)',
|
|
81
|
+
},
|
|
82
|
+
'50%': {
|
|
83
|
+
transform: 'translateY(0)',
|
|
84
|
+
animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -42,3 +42,14 @@ export type {
|
|
|
42
42
|
export {
|
|
43
43
|
shadows,
|
|
44
44
|
} from './shadow.tokens';
|
|
45
|
+
|
|
46
|
+
export type {
|
|
47
|
+
DurationToken,
|
|
48
|
+
EasingToken,
|
|
49
|
+
} from './animation.tokens';
|
|
50
|
+
export {
|
|
51
|
+
durations,
|
|
52
|
+
easings,
|
|
53
|
+
animations,
|
|
54
|
+
keyframes,
|
|
55
|
+
} from './animation.tokens';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkbox Component (Atom)
|
|
3
|
+
* @description Checkbox input element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
11
|
+
checked?: boolean;
|
|
12
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizeStyles = {
|
|
17
|
+
sm: 'h-4 w-4',
|
|
18
|
+
md: 'h-5 w-5',
|
|
19
|
+
lg: 'h-6 w-6',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|
23
|
+
({ className, checked, onCheckedChange, size = 'md', disabled, ...props }, ref) => {
|
|
24
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
25
|
+
onCheckedChange?.(e.target.checked);
|
|
26
|
+
props.onChange?.(e);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<input
|
|
31
|
+
ref={ref}
|
|
32
|
+
type="checkbox"
|
|
33
|
+
checked={checked}
|
|
34
|
+
onChange={handleChange}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
className={cn(
|
|
37
|
+
'shrink-0 rounded border border-primary ring-offset-background',
|
|
38
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
39
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
40
|
+
'accent-primary',
|
|
41
|
+
sizeStyles[size],
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider Component (Atom)
|
|
3
|
+
* @description Visual separator line
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface DividerProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
+
orientation?: 'horizontal' | 'vertical';
|
|
12
|
+
decorative?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const orientationStyles = {
|
|
16
|
+
horizontal: 'h-px w-full',
|
|
17
|
+
vertical: 'h-full w-px',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
|
21
|
+
({ className, orientation = 'horizontal', decorative = true, role = 'separator', ...props }, ref) => {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
role={decorative ? 'none' : role}
|
|
26
|
+
aria-orientation={orientation}
|
|
27
|
+
className={cn('shrink-0 bg-border', orientationStyles[orientation], className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
Divider.displayName = 'Divider';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Component (Atom)
|
|
3
|
+
* @description Styled anchor/link element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type AnchorHTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement>, BaseProps {
|
|
11
|
+
variant?: 'default' | 'primary' | 'muted';
|
|
12
|
+
underline?: 'none' | 'hover' | 'always';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantStyles: Record<'default' | 'primary' | 'muted', string> = {
|
|
16
|
+
default: 'text-foreground',
|
|
17
|
+
primary: 'text-primary',
|
|
18
|
+
muted: 'text-muted-foreground',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const underlineStyles: Record<'none' | 'hover' | 'always', string> = {
|
|
22
|
+
none: '',
|
|
23
|
+
hover: 'hover:underline',
|
|
24
|
+
always: 'underline',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
28
|
+
({ className, variant = 'default', underline = 'hover', ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<a
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn(
|
|
33
|
+
'inline-flex items-center gap-1 transition-colors',
|
|
34
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm',
|
|
35
|
+
variantStyles[variant],
|
|
36
|
+
underlineStyles[underline],
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
Link.displayName = 'Link';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Component (Atom)
|
|
3
|
+
* @description Progress indicator bar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface ProgressProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
+
value?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizeStyles: Record<'sm' | 'md' | 'lg', string> = {
|
|
17
|
+
sm: 'h-1',
|
|
18
|
+
md: 'h-2',
|
|
19
|
+
lg: 'h-3',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Progress = forwardRef<HTMLDivElement, ProgressProps>(
|
|
23
|
+
({ className, value = 0, max = 100, size = 'md', ...props }, ref) => {
|
|
24
|
+
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
role="progressbar"
|
|
30
|
+
aria-valuenow={value}
|
|
31
|
+
aria-valuemax={max}
|
|
32
|
+
className={cn(
|
|
33
|
+
'w-full bg-muted rounded-full overflow-hidden',
|
|
34
|
+
sizeStyles[size],
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
className="h-full bg-primary transition-all duration-300 ease-in-out"
|
|
41
|
+
style={{ width: `${percentage}%` }}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
Progress.displayName = 'Progress';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radio Component (Atom)
|
|
3
|
+
* @description Radio input element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
11
|
+
value: string;
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sizeStyles = {
|
|
16
|
+
sm: 'h-4 w-4',
|
|
17
|
+
md: 'h-5 w-5',
|
|
18
|
+
lg: 'h-6 w-6',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
|
22
|
+
({ className, size = 'md', disabled, ...props }, ref) => {
|
|
23
|
+
return (
|
|
24
|
+
<input
|
|
25
|
+
ref={ref}
|
|
26
|
+
type="radio"
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
className={cn(
|
|
29
|
+
'shrink-0 rounded-full border border-primary ring-offset-background',
|
|
30
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
31
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
32
|
+
'accent-primary',
|
|
33
|
+
sizeStyles[size],
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
Radio.displayName = 'Radio';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton Component (Atom)
|
|
3
|
+
* @description Loading placeholder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
+
variant?: 'text' | 'circular' | 'rectangular';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const variantStyles: Record<'text' | 'circular' | 'rectangular', string> = {
|
|
15
|
+
text: 'h-4 w-full rounded',
|
|
16
|
+
circular: 'h-12 w-12 rounded-full',
|
|
17
|
+
rectangular: 'h-24 w-full rounded',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
21
|
+
({ className, variant = 'rectangular', ...props }, ref) => {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
'animate-pulse bg-muted',
|
|
27
|
+
variantStyles[variant],
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
role="status"
|
|
31
|
+
aria-label="Loading"
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
Skeleton.displayName = 'Skeleton';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider Component (Atom)
|
|
3
|
+
* @description Range slider input
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
step?: number;
|
|
14
|
+
value?: number[];
|
|
15
|
+
onValueChange?: (value: number[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Slider = forwardRef<HTMLInputElement, SliderProps>(
|
|
19
|
+
({ className, min = 0, max = 100, step = 1, value = [0], onValueChange, disabled, ...props }, ref) => {
|
|
20
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
21
|
+
const newValue = [Number(e.target.value)];
|
|
22
|
+
onValueChange?.(newValue);
|
|
23
|
+
props.onChange?.(e);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<input
|
|
28
|
+
ref={ref}
|
|
29
|
+
type="range"
|
|
30
|
+
min={min}
|
|
31
|
+
max={max}
|
|
32
|
+
step={step}
|
|
33
|
+
value={value[0]}
|
|
34
|
+
onChange={handleChange}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
className={cn(
|
|
37
|
+
'w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer',
|
|
38
|
+
'accent-primary',
|
|
39
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
40
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
Slider.displayName = 'Slider';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip Component (Atom)
|
|
3
|
+
* @description Popup with additional information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes, type ReactNode, useState, useRef, useEffect } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface TooltipProps extends HTMLAttributes<HTMLDivElement>, BaseProps, ChildrenProps {
|
|
11
|
+
content: ReactNode;
|
|
12
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
13
|
+
delay?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const placementStyles: Record<'top' | 'bottom' | 'left' | 'right', string> = {
|
|
17
|
+
top: 'bottom-full mb-2',
|
|
18
|
+
bottom: 'top-full mt-2',
|
|
19
|
+
left: 'right-full mr-2',
|
|
20
|
+
right: 'left-full ml-2',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
|
24
|
+
({ className, children, content, placement = 'top', delay = 200, ...props }, ref) => {
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
27
|
+
|
|
28
|
+
const handleMouseEnter = () => {
|
|
29
|
+
timeoutRef.current = setTimeout(() => {
|
|
30
|
+
setIsOpen(true);
|
|
31
|
+
}, delay);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleMouseLeave = () => {
|
|
35
|
+
if (timeoutRef.current) {
|
|
36
|
+
clearTimeout(timeoutRef.current);
|
|
37
|
+
}
|
|
38
|
+
setIsOpen(false);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
if (timeoutRef.current) {
|
|
44
|
+
clearTimeout(timeoutRef.current);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className="relative inline-block"
|
|
53
|
+
onMouseEnter={handleMouseEnter}
|
|
54
|
+
onMouseLeave={handleMouseLeave}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
{isOpen && (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
'absolute z-50 px-2 py-1 text-xs text-white bg-foreground rounded shadow-lg whitespace-nowrap',
|
|
62
|
+
placementStyles[placement],
|
|
63
|
+
'animate-in fade-in-0 zoom-in-95',
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
role="tooltip"
|
|
67
|
+
>
|
|
68
|
+
{content}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
Tooltip.displayName = 'Tooltip';
|
|
@@ -21,3 +21,27 @@ export type { IconProps } from './Icon';
|
|
|
21
21
|
|
|
22
22
|
export { Spinner } from './Spinner';
|
|
23
23
|
export type { SpinnerProps } from './Spinner';
|
|
24
|
+
|
|
25
|
+
export { Checkbox } from './Checkbox';
|
|
26
|
+
export type { CheckboxProps } from './Checkbox';
|
|
27
|
+
|
|
28
|
+
export { Radio } from './Radio';
|
|
29
|
+
export type { RadioProps } from './Radio';
|
|
30
|
+
|
|
31
|
+
export { Slider } from './Slider';
|
|
32
|
+
export type { SliderProps } from './Slider';
|
|
33
|
+
|
|
34
|
+
export { Divider } from './Divider';
|
|
35
|
+
export type { DividerProps } from './Divider';
|
|
36
|
+
|
|
37
|
+
export { Skeleton } from './Skeleton';
|
|
38
|
+
export type { SkeletonProps } from './Skeleton';
|
|
39
|
+
|
|
40
|
+
export { Link } from './Link';
|
|
41
|
+
export type { LinkProps } from './Link';
|
|
42
|
+
|
|
43
|
+
export { Tooltip } from './Tooltip';
|
|
44
|
+
export type { TooltipProps } from './Tooltip';
|
|
45
|
+
|
|
46
|
+
export { Progress } from './Progress';
|
|
47
|
+
export type { ProgressProps } from './Progress';
|
|
@@ -11,3 +11,16 @@ export { useMediaQuery, useBreakpoint } from './useMediaQuery';
|
|
|
11
11
|
export type { Breakpoint } from './useMediaQuery';
|
|
12
12
|
|
|
13
13
|
export { useLocalStorage } from './useLocalStorage';
|
|
14
|
+
|
|
15
|
+
export { useClickOutside } from './useClickOutside';
|
|
16
|
+
|
|
17
|
+
export { useKeyboard, useEscape } from './useKeyboard';
|
|
18
|
+
export type { KeyboardKey, KeyboardModifier, KeyboardOptions, UseClipboardReturn } from './useKeyboard';
|
|
19
|
+
|
|
20
|
+
export { useDebounce } from './useDebounce';
|
|
21
|
+
|
|
22
|
+
export { useClipboard } from './useClipboard';
|
|
23
|
+
|
|
24
|
+
export { useToggle } from './useToggle';
|
|
25
|
+
|
|
26
|
+
export { useScrollLock } from './useScrollLock';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClickOutside Hook
|
|
3
|
+
* @description Detect clicks outside an element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useRef } from 'react';
|
|
7
|
+
|
|
8
|
+
export function useClickOutside<T extends HTMLElement>(
|
|
9
|
+
callback: () => void,
|
|
10
|
+
enabled: boolean = true
|
|
11
|
+
) {
|
|
12
|
+
const ref = useRef<T>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!enabled) return;
|
|
16
|
+
|
|
17
|
+
const handleClick = (event: MouseEvent) => {
|
|
18
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
19
|
+
callback();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
document.addEventListener('mousedown', handleClick);
|
|
24
|
+
document.addEventListener('touchstart', handleClick);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
document.removeEventListener('mousedown', handleClick);
|
|
28
|
+
document.removeEventListener('touchstart', handleClick);
|
|
29
|
+
};
|
|
30
|
+
}, [callback, enabled]);
|
|
31
|
+
|
|
32
|
+
return ref;
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClipboard Hook
|
|
3
|
+
* @description Clipboard operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
export interface UseClipboardReturn {
|
|
9
|
+
value: string;
|
|
10
|
+
copied: boolean;
|
|
11
|
+
copy: (text: string) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useClipboard(): UseClipboardReturn {
|
|
15
|
+
const [value, setValue] = useState<string>('');
|
|
16
|
+
const [copied, setCopied] = useState<boolean>(false);
|
|
17
|
+
|
|
18
|
+
const copy = useCallback(async (text: string) => {
|
|
19
|
+
try {
|
|
20
|
+
await navigator.clipboard.writeText(text);
|
|
21
|
+
setValue(text);
|
|
22
|
+
setCopied(true);
|
|
23
|
+
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
setCopied(false);
|
|
26
|
+
}, 2000);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to copy:', error);
|
|
29
|
+
setCopied(false);
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
value,
|
|
35
|
+
copied,
|
|
36
|
+
copy,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDebounce Hook
|
|
3
|
+
* @description Debounce a value
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
export function useDebounce<T>(value: T, delay: number = 500): T {
|
|
9
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handler = setTimeout(() => {
|
|
13
|
+
setDebouncedValue(value);
|
|
14
|
+
}, delay);
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
clearTimeout(handler);
|
|
18
|
+
};
|
|
19
|
+
}, [value, delay]);
|
|
20
|
+
|
|
21
|
+
return debouncedValue;
|
|
22
|
+
}
|