@umituz/web-design-system 1.1.0 → 1.5.1
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 +13 -3
- package/src/global.d.ts +20 -0
- package/src/infrastructure/error/ErrorDisplay.tsx +6 -12
- package/src/infrastructure/error/SuspenseWrapper.tsx +15 -10
- package/src/infrastructure/performance/useMemoryOptimization.ts +1 -1
- package/src/infrastructure/utils/cn.util.ts +7 -7
- package/src/infrastructure/utils/index.ts +1 -1
- package/src/presentation/atoms/Input.tsx +1 -1
- package/src/presentation/atoms/Slider.tsx +1 -1
- package/src/presentation/atoms/Text.tsx +1 -1
- package/src/presentation/atoms/Tooltip.tsx +2 -2
- package/src/presentation/hooks/index.ts +2 -1
- package/src/presentation/hooks/useClickOutside.ts +1 -1
- package/src/presentation/molecules/Avatar.tsx +47 -19
- package/src/presentation/molecules/CheckboxGroup.tsx +1 -1
- package/src/presentation/molecules/FormField.tsx +2 -2
- package/src/presentation/molecules/InputGroup.tsx +1 -1
- package/src/presentation/molecules/RadioGroup.tsx +1 -1
- package/src/presentation/molecules/Select.tsx +152 -33
- package/src/presentation/molecules/index.ts +3 -3
- package/src/presentation/organisms/Accordion.tsx +53 -111
- package/src/presentation/organisms/Alert.tsx +52 -85
- package/src/presentation/organisms/Card.tsx +1 -1
- package/src/presentation/organisms/Footer.tsx +99 -0
- package/src/presentation/organisms/Navbar.tsx +1 -1
- package/src/presentation/organisms/Table.tsx +1 -1
- package/src/presentation/organisms/index.ts +6 -4
- package/src/presentation/templates/Form.tsx +2 -2
- package/src/presentation/templates/List.tsx +1 -1
- package/src/presentation/templates/PageHeader.tsx +78 -0
- package/src/presentation/templates/PageLayout.tsx +38 -0
- package/src/presentation/templates/ProjectSkeleton.tsx +35 -0
- package/src/presentation/templates/Section.tsx +1 -1
- package/src/presentation/templates/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-design-system",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
|
|
5
6
|
"main": "./src/index.ts",
|
|
6
7
|
"types": "./src/index.ts",
|
|
7
8
|
"sideEffects": false,
|
|
@@ -51,14 +52,23 @@
|
|
|
51
52
|
"url": "git+https://github.com/umituz/web-design-system.git"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
55
|
+
"@radix-ui/react-accordion": ">=1.0.0",
|
|
56
|
+
"@radix-ui/react-select": ">=2.0.0",
|
|
57
|
+
"clsx": ">=2.0.0",
|
|
54
58
|
"react": ">=18.0.0",
|
|
55
|
-
"react-dom": ">=18.0.0"
|
|
59
|
+
"react-dom": ">=18.0.0",
|
|
60
|
+
"tailwind-merge": ">=2.0.0"
|
|
56
61
|
},
|
|
57
62
|
"devDependencies": {
|
|
63
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
64
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
58
65
|
"@types/react": "^18.0.0",
|
|
59
66
|
"@types/react-dom": "^18.0.0",
|
|
67
|
+
"clsx": "^2.1.1",
|
|
68
|
+
"lucide-react": "^0.577.0",
|
|
60
69
|
"react": "^18.0.0",
|
|
61
70
|
"react-dom": "^18.0.0",
|
|
71
|
+
"tailwind-merge": "^3.5.0",
|
|
62
72
|
"typescript": "~5.9.2"
|
|
63
73
|
},
|
|
64
74
|
"publishConfig": {
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Type Declarations
|
|
3
|
+
* @description Global type definitions for the design system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface ImportMetaEnv {
|
|
8
|
+
readonly DEV: boolean;
|
|
9
|
+
readonly MODE: string;
|
|
10
|
+
readonly BASE_URL: string;
|
|
11
|
+
readonly PROD: boolean;
|
|
12
|
+
readonly SSR: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImportMeta {
|
|
16
|
+
readonly env: ImportMetaEnv;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {};
|
|
@@ -55,13 +55,9 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
55
55
|
const errorMessage = error instanceof Error ? error.message : (error || 'Unknown error');
|
|
56
56
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
57
57
|
|
|
58
|
-
const containerStyle: React.CSSProperties = {
|
|
59
|
-
className
|
|
60
|
-
};
|
|
61
|
-
|
|
62
58
|
if (variant === 'minimal') {
|
|
63
59
|
return (
|
|
64
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: config.color
|
|
60
|
+
<div className={className} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: config.color }}>
|
|
65
61
|
<span>{config.icon}</span>
|
|
66
62
|
<span>{errorMessage}</span>
|
|
67
63
|
</div>
|
|
@@ -70,12 +66,11 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
70
66
|
|
|
71
67
|
if (variant === 'inline') {
|
|
72
68
|
return (
|
|
73
|
-
<div style={{
|
|
69
|
+
<div className={className} style={{
|
|
74
70
|
padding: '12px 16px',
|
|
75
71
|
background: config.bgColor,
|
|
76
72
|
border: `1px solid ${config.borderColor}`,
|
|
77
|
-
borderRadius: '6px'
|
|
78
|
-
...containerStyle
|
|
73
|
+
borderRadius: '6px'
|
|
79
74
|
}}>
|
|
80
75
|
<span style={{ marginRight: '8px' }}>{config.icon}</span>
|
|
81
76
|
<span style={{ fontWeight: 500 }}>{title}:</span> {errorMessage}
|
|
@@ -85,12 +80,11 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
85
80
|
|
|
86
81
|
if (variant === 'card') {
|
|
87
82
|
return (
|
|
88
|
-
<div style={{
|
|
83
|
+
<div className={className} style={{
|
|
89
84
|
background: 'white',
|
|
90
85
|
borderRadius: '8px',
|
|
91
86
|
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
92
|
-
border: 'none'
|
|
93
|
-
...containerStyle
|
|
87
|
+
border: 'none'
|
|
94
88
|
}}>
|
|
95
89
|
<div style={{ padding: '24px' }}>
|
|
96
90
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
|
@@ -209,7 +203,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
209
203
|
|
|
210
204
|
// Default variant
|
|
211
205
|
return (
|
|
212
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px'
|
|
206
|
+
<div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
|
|
213
207
|
<div style={{
|
|
214
208
|
width: '100%',
|
|
215
209
|
maxWidth: '448px',
|
|
@@ -8,6 +8,7 @@ export interface SuspenseWrapperProps {
|
|
|
8
8
|
loadingText?: string;
|
|
9
9
|
variant?: 'default' | 'minimal' | 'card';
|
|
10
10
|
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
11
12
|
showErrorBoundary?: boolean;
|
|
12
13
|
errorBoundaryLevel?: 'page' | 'component' | 'feature';
|
|
13
14
|
}
|
|
@@ -41,6 +42,7 @@ export const SuspenseWrapper: React.FC<SuspenseWrapperProps> = ({
|
|
|
41
42
|
loadingText = 'Loading...',
|
|
42
43
|
variant = 'default',
|
|
43
44
|
className = '',
|
|
45
|
+
style,
|
|
44
46
|
showErrorBoundary = true,
|
|
45
47
|
errorBoundaryLevel = 'component'
|
|
46
48
|
}) => {
|
|
@@ -57,18 +59,21 @@ export const SuspenseWrapper: React.FC<SuspenseWrapperProps> = ({
|
|
|
57
59
|
</Suspense>
|
|
58
60
|
);
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
const content = showErrorBoundary ? (
|
|
63
|
+
<ErrorBoundary
|
|
64
|
+
level={errorBoundaryLevel}
|
|
65
|
+
fallback={errorFallback}
|
|
66
|
+
>
|
|
67
|
+
{suspenseContent}
|
|
68
|
+
</ErrorBoundary>
|
|
69
|
+
) : suspenseContent;
|
|
70
|
+
|
|
71
|
+
// Wrap in div if className or style is provided
|
|
72
|
+
if (className || style) {
|
|
73
|
+
return <div className={className} style={style}>{content}</div>;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
return
|
|
76
|
+
return content;
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
// Specialized Suspense wrappers
|
|
@@ -249,7 +249,7 @@ export const useMemoryOptimization = (config: MemoryOptimizationConfig = {}) =>
|
|
|
249
249
|
};
|
|
250
250
|
|
|
251
251
|
export const useMemoryLeakDetector = (componentName?: string) => {
|
|
252
|
-
const mountTime = useRef<number>(
|
|
252
|
+
const mountTime = useRef<number>(Date.now());
|
|
253
253
|
const [renderCount, setRenderCount] = useState<number>(0);
|
|
254
254
|
const [lifespan, setLifespan] = useState<number>(0);
|
|
255
255
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cn Utility
|
|
3
|
-
* @description Conditional className utility
|
|
3
|
+
* @description Conditional className utility using clsx + tailwind-merge for proper Tailwind class merging
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.join(' ');
|
|
9
|
+
export type { ClassValue };
|
|
10
|
+
|
|
11
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
13
13
|
}
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, SizeVariant } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, BaseProps {
|
|
10
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
11
11
|
error?: boolean;
|
|
12
12
|
size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
|
|
13
13
|
}
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
10
|
+
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'value'>, BaseProps {
|
|
11
11
|
min?: number;
|
|
12
12
|
max?: number;
|
|
13
13
|
step?: number;
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
10
|
+
export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label';
|
|
11
11
|
export type TextVariant = 'body' | 'heading' | 'label' | 'caption';
|
|
12
12
|
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes, type ReactNode, useState, useRef, useE
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface TooltipProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
10
|
+
export interface TooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'content'>, BaseProps {
|
|
11
11
|
content: ReactNode;
|
|
12
12
|
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
13
13
|
delay?: number;
|
|
@@ -23,7 +23,7 @@ const placementStyles: Record<'top' | 'bottom' | 'left' | 'right', string> = {
|
|
|
23
23
|
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
|
24
24
|
({ className, children, content, placement = 'top', delay = 200, ...props }, ref) => {
|
|
25
25
|
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
-
const timeoutRef = useRef<
|
|
26
|
+
const timeoutRef = useRef<number>();
|
|
27
27
|
|
|
28
28
|
const handleMouseEnter = () => {
|
|
29
29
|
timeoutRef.current = setTimeout(() => {
|
|
@@ -15,11 +15,12 @@ export { useLocalStorage } from './useLocalStorage';
|
|
|
15
15
|
export { useClickOutside } from './useClickOutside';
|
|
16
16
|
|
|
17
17
|
export { useKeyboard, useEscape } from './useKeyboard';
|
|
18
|
-
export type { KeyboardKey, KeyboardModifier, KeyboardOptions
|
|
18
|
+
export type { KeyboardKey, KeyboardModifier, KeyboardOptions } from './useKeyboard';
|
|
19
19
|
|
|
20
20
|
export { useDebounce } from './useDebounce';
|
|
21
21
|
|
|
22
22
|
export { useClipboard } from './useClipboard';
|
|
23
|
+
export type { UseClipboardReturn } from './useClipboard';
|
|
23
24
|
|
|
24
25
|
export { useToggle } from './useToggle';
|
|
25
26
|
|
|
@@ -14,7 +14,7 @@ export function useClickOutside<T extends HTMLElement>(
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
if (!enabled) return;
|
|
16
16
|
|
|
17
|
-
const handleClick = (event:
|
|
17
|
+
const handleClick = (event: Event) => {
|
|
18
18
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
19
19
|
callback();
|
|
20
20
|
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Avatar Component (Molecule)
|
|
3
|
-
* @description User avatar with fallback
|
|
3
|
+
* @description User avatar with image and fallback (shadcn/ui compatible)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { forwardRef, type HTMLAttributes } from 'react';
|
|
6
|
+
import { forwardRef, type HTMLAttributes, type ElementType, type ComponentPropsWithoutRef } from 'react';
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, SizeVariant } from '../../domain/types';
|
|
9
9
|
|
|
10
10
|
export interface AvatarProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
-
src?: string;
|
|
12
|
-
alt?: string;
|
|
13
|
-
fallback?: string;
|
|
14
11
|
size?: Extract<SizeVariant, 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'>;
|
|
15
12
|
}
|
|
16
13
|
|
|
@@ -23,30 +20,61 @@ const sizeStyles: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl', string> = {
|
|
|
23
20
|
'2xl': 'h-20 w-20 text-2xl',
|
|
24
21
|
};
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
({ className,
|
|
28
|
-
const hasError = !src;
|
|
29
|
-
|
|
23
|
+
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
|
24
|
+
({ className, size = 'md', ...props }, ref) => {
|
|
30
25
|
return (
|
|
31
26
|
<div
|
|
32
27
|
ref={ref}
|
|
33
28
|
className={cn(
|
|
34
|
-
'relative
|
|
29
|
+
'relative flex shrink-0 overflow-hidden rounded-full',
|
|
35
30
|
sizeStyles[size],
|
|
36
31
|
className
|
|
37
32
|
)}
|
|
38
33
|
{...props}
|
|
39
|
-
|
|
40
|
-
{hasError ? (
|
|
41
|
-
<span className="font-medium text-muted-foreground">
|
|
42
|
-
{fallback || '?'}
|
|
43
|
-
</span>
|
|
44
|
-
) : (
|
|
45
|
-
<img src={src} alt={alt || 'Avatar'} className="h-full w-full object-cover" />
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
34
|
+
/>
|
|
48
35
|
);
|
|
49
36
|
}
|
|
50
37
|
);
|
|
51
38
|
|
|
52
39
|
Avatar.displayName = 'Avatar';
|
|
40
|
+
|
|
41
|
+
export interface AvatarImageProps extends ComponentPropsWithoutRef<'img'> {
|
|
42
|
+
asChild?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const AvatarImage = forwardRef<HTMLImageElement, AvatarImageProps>(
|
|
46
|
+
({ className, src, alt, ...props }, ref) => {
|
|
47
|
+
return (
|
|
48
|
+
<img
|
|
49
|
+
ref={ref}
|
|
50
|
+
src={src}
|
|
51
|
+
alt={alt}
|
|
52
|
+
className={cn('aspect-square h-full w-full object-cover', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
AvatarImage.displayName = 'AvatarImage';
|
|
60
|
+
|
|
61
|
+
export interface AvatarFallbackProps extends HTMLAttributes<HTMLDivElement> {}
|
|
62
|
+
|
|
63
|
+
const AvatarFallback = forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
|
64
|
+
({ className, ...props }, ref) => {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn(
|
|
69
|
+
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
AvatarFallback.displayName = 'AvatarFallback';
|
|
79
|
+
|
|
80
|
+
export { Avatar, AvatarImage, AvatarFallback };
|
|
@@ -15,7 +15,7 @@ export interface CheckboxOption {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface CheckboxGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
18
|
+
export interface CheckboxGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, BaseProps {
|
|
19
19
|
options: CheckboxOption[];
|
|
20
20
|
value?: string[];
|
|
21
21
|
onChange?: (values: string[]) => void;
|
|
@@ -27,10 +27,10 @@ export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
|
27
27
|
return (
|
|
28
28
|
<div className={cn('space-y-1.5', className)} {...props}>
|
|
29
29
|
{label && (
|
|
30
|
-
<
|
|
30
|
+
<label htmlFor={fieldId} className="text-sm font-medium">
|
|
31
31
|
{label}
|
|
32
32
|
{required && <span className="text-destructive ml-1">*</span>}
|
|
33
|
-
</
|
|
33
|
+
</label>
|
|
34
34
|
)}
|
|
35
35
|
|
|
36
36
|
<Input
|
|
@@ -8,7 +8,7 @@ import { cn } from '../../infrastructure/utils';
|
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
import { Input } from '../atoms/Input';
|
|
10
10
|
|
|
11
|
-
export interface InputGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
11
|
+
export interface InputGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
12
12
|
leftElement?: ReactNode;
|
|
13
13
|
rightElement?: ReactNode;
|
|
14
14
|
}
|
|
@@ -15,7 +15,7 @@ export interface RadioOption {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface RadioGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
18
|
+
export interface RadioGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, BaseProps {
|
|
19
19
|
name: string;
|
|
20
20
|
options: RadioOption[];
|
|
21
21
|
value?: string;
|
|
@@ -1,41 +1,160 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Select Component (Molecule)
|
|
3
|
-
* @description Dropdown select
|
|
3
|
+
* @description Dropdown select (Shadcn/Radix UI compatible)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
8
|
+
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
|
7
9
|
import { cn } from '../../infrastructure/utils';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
|
|
11
|
+
const Select = SelectPrimitive.Root;
|
|
12
|
+
|
|
13
|
+
const SelectGroup = SelectPrimitive.Group;
|
|
14
|
+
|
|
15
|
+
const SelectValue = SelectPrimitive.Value;
|
|
16
|
+
|
|
17
|
+
const SelectTrigger = React.forwardRef<
|
|
18
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
19
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
20
|
+
>(({ className, children, ...props }, ref) => (
|
|
21
|
+
<SelectPrimitive.Trigger
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn(
|
|
24
|
+
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
<SelectPrimitive.Icon asChild>
|
|
31
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
32
|
+
</SelectPrimitive.Icon>
|
|
33
|
+
</SelectPrimitive.Trigger>
|
|
34
|
+
));
|
|
35
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
36
|
+
|
|
37
|
+
const SelectScrollUpButton = React.forwardRef<
|
|
38
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
39
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
40
|
+
>(({ className, ...props }, ref) => (
|
|
41
|
+
<SelectPrimitive.ScrollUpButton
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex cursor-default items-center justify-center py-1',
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<ChevronUp className="h-4 w-4" />
|
|
50
|
+
</SelectPrimitive.ScrollUpButton>
|
|
51
|
+
));
|
|
52
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
53
|
+
|
|
54
|
+
const SelectScrollDownButton = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
56
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
57
|
+
>(({ className, ...props }, ref) => (
|
|
58
|
+
<SelectPrimitive.ScrollDownButton
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(
|
|
61
|
+
'flex cursor-default items-center justify-center py-1',
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<ChevronDown className="h-4 w-4" />
|
|
67
|
+
</SelectPrimitive.ScrollDownButton>
|
|
68
|
+
));
|
|
69
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
70
|
+
|
|
71
|
+
const SelectContent = React.forwardRef<
|
|
72
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
73
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
74
|
+
>(({ className, children, position = 'popper', ...props }, ref) => (
|
|
75
|
+
<SelectPrimitive.Portal>
|
|
76
|
+
<SelectPrimitive.Content
|
|
77
|
+
ref={ref}
|
|
78
|
+
className={cn(
|
|
79
|
+
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
80
|
+
position === 'popper' &&
|
|
81
|
+
'data-[side=bottom]:translate-y-1 data-[side=left]:translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:translate-y-1',
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
position={position}
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
<SelectScrollUpButton />
|
|
88
|
+
<SelectPrimitive.Viewport
|
|
21
89
|
className={cn(
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
26
|
-
error && 'border-destructive',
|
|
27
|
-
className
|
|
90
|
+
'p-1',
|
|
91
|
+
position === 'popper' &&
|
|
92
|
+
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
|
28
93
|
)}
|
|
29
|
-
{...props}
|
|
30
94
|
>
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
{children}
|
|
96
|
+
</SelectPrimitive.Viewport>
|
|
97
|
+
<SelectScrollDownButton />
|
|
98
|
+
</SelectPrimitive.Content>
|
|
99
|
+
</SelectPrimitive.Portal>
|
|
100
|
+
));
|
|
101
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
102
|
+
|
|
103
|
+
const SelectLabel = React.forwardRef<
|
|
104
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
105
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
106
|
+
>(({ className, ...props }, ref) => (
|
|
107
|
+
<SelectPrimitive.Label
|
|
108
|
+
ref={ref}
|
|
109
|
+
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
));
|
|
113
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
114
|
+
|
|
115
|
+
const SelectItem = React.forwardRef<
|
|
116
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
117
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
118
|
+
>(({ className, children, ...props }, ref) => (
|
|
119
|
+
<SelectPrimitive.Item
|
|
120
|
+
ref={ref}
|
|
121
|
+
className={cn(
|
|
122
|
+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
128
|
+
<SelectPrimitive.ItemIndicator>
|
|
129
|
+
<Check className="h-4 w-4" />
|
|
130
|
+
</SelectPrimitive.ItemIndicator>
|
|
131
|
+
</span>
|
|
132
|
+
{children}
|
|
133
|
+
</SelectPrimitive.Item>
|
|
134
|
+
));
|
|
135
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
136
|
+
|
|
137
|
+
const SelectSeparator = React.forwardRef<
|
|
138
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
139
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
140
|
+
>(({ className, ...props }, ref) => (
|
|
141
|
+
<SelectPrimitive.Separator
|
|
142
|
+
ref={ref}
|
|
143
|
+
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
));
|
|
147
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
Select,
|
|
151
|
+
SelectGroup,
|
|
152
|
+
SelectValue,
|
|
153
|
+
SelectTrigger,
|
|
154
|
+
SelectContent,
|
|
155
|
+
SelectLabel,
|
|
156
|
+
SelectItem,
|
|
157
|
+
SelectSeparator,
|
|
158
|
+
SelectScrollUpButton,
|
|
159
|
+
SelectScrollDownButton,
|
|
160
|
+
};
|
|
@@ -10,8 +10,8 @@ export type { FormFieldProps } from './FormField';
|
|
|
10
10
|
export { SearchBox } from './SearchBox';
|
|
11
11
|
export type { SearchBoxProps } from './SearchBox';
|
|
12
12
|
|
|
13
|
-
export { Avatar } from './Avatar';
|
|
14
|
-
export type { AvatarProps } from './Avatar';
|
|
13
|
+
export { Avatar, AvatarImage, AvatarFallback } from './Avatar';
|
|
14
|
+
export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from './Avatar';
|
|
15
15
|
|
|
16
16
|
export { Chip } from './Chip';
|
|
17
17
|
export type { ChipProps } from './Chip';
|
|
@@ -19,7 +19,7 @@ export type { ChipProps } from './Chip';
|
|
|
19
19
|
export { Toggle } from './Toggle';
|
|
20
20
|
export type { ToggleProps } from './Toggle';
|
|
21
21
|
|
|
22
|
-
export { Select } from './Select';
|
|
22
|
+
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from './Select';
|
|
23
23
|
export type { SelectProps } from './Select';
|
|
24
24
|
|
|
25
25
|
export { Textarea } from './Textarea';
|