@umituz/web-design-system 3.1.21 → 4.0.0
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/README.md +137 -469
- package/dist/styles.css +2 -0
- package/package.json +30 -31
- package/src/__tests__/cn.test.ts +24 -0
- package/src/__tests__/components.smoke.test.tsx +78 -0
- package/src/__tests__/styles.contract.test.ts +60 -0
- package/src/domain/tokens/index.ts +8 -49
- package/src/index.ts +8 -10
- package/src/infrastructure/constants/component.constants.ts +3 -17
- package/src/infrastructure/constants/index.ts +4 -2
- package/src/infrastructure/constants/timing.constants.ts +46 -0
- package/src/infrastructure/utils/calculation.utils.ts +74 -0
- package/src/infrastructure/utils/index.ts +12 -0
- package/src/presentation/atoms/IconButton.tsx +55 -0
- package/src/presentation/atoms/Progress.tsx +2 -1
- package/src/presentation/atoms/Spinner.tsx +1 -1
- package/src/presentation/atoms/Switch.tsx +2 -2
- package/src/presentation/atoms/Text.tsx +15 -13
- package/src/presentation/atoms/Tooltip.tsx +2 -1
- package/src/presentation/atoms/index.ts +3 -0
- package/src/presentation/hooks/index.ts +0 -3
- package/src/presentation/hooks/useClickOutside.ts +14 -10
- package/src/presentation/hooks/useClipboard.ts +30 -12
- package/src/presentation/hooks/useDebounce.ts +8 -10
- package/src/presentation/hooks/useLocalStorage.ts +28 -18
- package/src/presentation/hooks/useMediaQuery.ts +2 -1
- package/src/presentation/hooks/useScrollLock.ts +40 -12
- package/src/presentation/hooks/useToggle.ts +8 -6
- package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
- package/src/presentation/molecules/Avatar.tsx +1 -1
- package/src/presentation/molecules/CodeBlock.tsx +51 -48
- package/src/presentation/molecules/ListItem.tsx +40 -29
- package/src/presentation/molecules/Select.tsx +1 -1
- package/src/presentation/molecules/Toggle.tsx +4 -4
- package/src/presentation/molecules/index.ts +0 -3
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/DataTable.tsx +24 -11
- package/src/presentation/organisms/Dialog.tsx +1 -1
- package/src/presentation/organisms/DropdownMenu.tsx +2 -2
- package/src/presentation/organisms/HoverCard.tsx +1 -1
- package/src/presentation/organisms/LoadingState.tsx +1 -5
- package/src/presentation/organisms/Popover.tsx +1 -1
- package/src/presentation/organisms/StatCard.tsx +2 -2
- package/src/presentation/organisms/Table.tsx +2 -2
- package/src/presentation/organisms/index.ts +0 -60
- package/src/styles/compile.css +18 -0
- package/src/styles/theme.css +131 -0
- package/src/domain/tokens/animation.tokens.ts +0 -87
- package/src/domain/tokens/color.tokens.ts +0 -95
- package/src/domain/tokens/radius.tokens.ts +0 -27
- package/src/domain/tokens/shadow.tokens.ts +0 -25
- package/src/domain/tokens/spacing.tokens.ts +0 -65
- package/src/domain/tokens/typography.tokens.ts +0 -74
- package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
- package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
- package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
- package/src/infrastructure/error/index.ts +0 -5
- package/src/infrastructure/performance/index.ts +0 -92
- package/src/infrastructure/performance/useLazyLoading.ts +0 -342
- package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
- package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
- package/src/infrastructure/security/index.ts +0 -10
- package/src/infrastructure/security/security-config.ts +0 -171
- package/src/infrastructure/security/useFormValidation.ts +0 -216
- package/src/infrastructure/security/validation.ts +0 -246
- package/src/presentation/hooks/useLanguage.ts +0 -109
- package/src/presentation/molecules/Breadcrumb.tsx +0 -54
- package/src/presentation/organisms/AlertDialog.tsx +0 -181
- package/src/presentation/organisms/Calendar.tsx +0 -79
- package/src/presentation/organisms/Comments.tsx +0 -103
- package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
- package/src/presentation/organisms/FilterBar.tsx +0 -244
- package/src/presentation/organisms/FilterSidebar.tsx +0 -172
- package/src/presentation/organisms/Footer.tsx +0 -86
- package/src/presentation/organisms/FormModal.tsx +0 -126
- package/src/presentation/organisms/Grid.tsx +0 -128
- package/src/presentation/organisms/ImageLightbox.tsx +0 -252
- package/src/presentation/organisms/MainNavbar.tsx +0 -320
- package/src/presentation/organisms/Modal.tsx +0 -173
- package/src/presentation/organisms/Navbar.tsx +0 -75
- package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
- package/src/presentation/organisms/QuickActionCard.tsx +0 -93
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modal Component (Organism)
|
|
3
|
-
* @description Dialog/overlay container with optimized transitions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { forwardRef, type HTMLAttributes, useEffect, useState, useRef } from 'react';
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { cn } from '../../infrastructure/utils';
|
|
9
|
-
import type { BaseProps } from '../../domain/types';
|
|
10
|
-
|
|
11
|
-
export interface ModalProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
12
|
-
open?: boolean;
|
|
13
|
-
onClose?: () => void;
|
|
14
|
-
showCloseButton?: boolean;
|
|
15
|
-
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const sizeStyles: Record<'sm' | 'md' | 'lg' | 'xl' | 'full', string> = {
|
|
19
|
-
sm: 'max-w-sm',
|
|
20
|
-
md: 'max-w-md',
|
|
21
|
-
lg: 'max-w-lg',
|
|
22
|
-
xl: 'max-w-xl',
|
|
23
|
-
full: 'max-w-full mx-4',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const Modal = React.memo(forwardRef<HTMLDivElement, ModalProps>(
|
|
27
|
-
({ open = false, onClose, showCloseButton = true, size = 'md', className, children, ...props }, ref) => {
|
|
28
|
-
const [shouldRender, setShouldRender] = useState(open);
|
|
29
|
-
const [isAnimating, setIsAnimating] = useState(false);
|
|
30
|
-
const rafRef = useRef<number | null>(null);
|
|
31
|
-
const timerRef = useRef<number | null>(null);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (open) {
|
|
35
|
-
setShouldRender(true);
|
|
36
|
-
// FIX: Store rafId and cleanup properly
|
|
37
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
38
|
-
setIsAnimating(true);
|
|
39
|
-
});
|
|
40
|
-
} else {
|
|
41
|
-
setIsAnimating(false);
|
|
42
|
-
// Wait for animation to complete before unmounting
|
|
43
|
-
timerRef.current = window.setTimeout(() => {
|
|
44
|
-
setShouldRender(false);
|
|
45
|
-
}, 200); // Match animation duration
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// FIX: Proper cleanup for both animation frame and timeout
|
|
49
|
-
return () => {
|
|
50
|
-
if (rafRef.current) {
|
|
51
|
-
cancelAnimationFrame(rafRef.current);
|
|
52
|
-
}
|
|
53
|
-
if (timerRef.current) {
|
|
54
|
-
clearTimeout(timerRef.current);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}, [open]);
|
|
58
|
-
|
|
59
|
-
if (!shouldRender) return null;
|
|
60
|
-
|
|
61
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
62
|
-
if (e.target === e.currentTarget && onClose) {
|
|
63
|
-
onClose();
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div
|
|
69
|
-
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
70
|
-
{...props}
|
|
71
|
-
>
|
|
72
|
-
{/* Backdrop */}
|
|
73
|
-
<div
|
|
74
|
-
className={`fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-200 ${
|
|
75
|
-
isAnimating ? 'opacity-100' : 'opacity-0'
|
|
76
|
-
}`}
|
|
77
|
-
onClick={handleBackdropClick}
|
|
78
|
-
aria-hidden="true"
|
|
79
|
-
/>
|
|
80
|
-
|
|
81
|
-
{/* Modal */}
|
|
82
|
-
<div
|
|
83
|
-
ref={ref}
|
|
84
|
-
className={cn(
|
|
85
|
-
'relative z-50 w-full rounded-lg border bg-card p-6 shadow-lg',
|
|
86
|
-
'transition-all duration-200',
|
|
87
|
-
isAnimating ? 'opacity-100 scale-100' : 'opacity-0 scale-95',
|
|
88
|
-
sizeStyles[size],
|
|
89
|
-
className
|
|
90
|
-
)}
|
|
91
|
-
role="dialog"
|
|
92
|
-
aria-modal="true"
|
|
93
|
-
>
|
|
94
|
-
{showCloseButton && (
|
|
95
|
-
<button
|
|
96
|
-
onClick={onClose}
|
|
97
|
-
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
98
|
-
aria-label="Close modal"
|
|
99
|
-
>
|
|
100
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
101
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
102
|
-
</svg>
|
|
103
|
-
<span className="sr-only">Close</span>
|
|
104
|
-
</button>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{children}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
));
|
|
113
|
-
|
|
114
|
-
Modal.displayName = 'Modal';
|
|
115
|
-
|
|
116
|
-
export const ModalHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
117
|
-
({ className, ...props }, ref) => (
|
|
118
|
-
<div
|
|
119
|
-
ref={ref}
|
|
120
|
-
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
|
|
121
|
-
{...props}
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
ModalHeader.displayName = 'ModalHeader';
|
|
127
|
-
|
|
128
|
-
export const ModalTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
129
|
-
({ className, ...props }, ref) => (
|
|
130
|
-
<h2
|
|
131
|
-
ref={ref}
|
|
132
|
-
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
|
133
|
-
{...props}
|
|
134
|
-
/>
|
|
135
|
-
)
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
ModalTitle.displayName = 'ModalTitle';
|
|
139
|
-
|
|
140
|
-
export const ModalDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
141
|
-
({ className, ...props }, ref) => (
|
|
142
|
-
<p
|
|
143
|
-
ref={ref}
|
|
144
|
-
className={cn('text-sm text-muted-foreground', className)}
|
|
145
|
-
{...props}
|
|
146
|
-
/>
|
|
147
|
-
)
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
ModalDescription.displayName = 'ModalDescription';
|
|
151
|
-
|
|
152
|
-
export const ModalContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
153
|
-
({ className, ...props }, ref) => (
|
|
154
|
-
<div ref={ref} className={cn('mt-4', className)} {...props} />
|
|
155
|
-
)
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
ModalContent.displayName = 'ModalContent';
|
|
159
|
-
|
|
160
|
-
export const ModalFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
161
|
-
({ className, ...props }, ref) => (
|
|
162
|
-
<div
|
|
163
|
-
ref={ref}
|
|
164
|
-
className={cn(
|
|
165
|
-
'mt-4 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
|
166
|
-
className
|
|
167
|
-
)}
|
|
168
|
-
{...props}
|
|
169
|
-
/>
|
|
170
|
-
)
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
ModalFooter.displayName = 'ModalFooter';
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navbar Component (Organism)
|
|
3
|
-
* @description Navigation bar with logo and links - optimized for performance
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { cn } from '../../infrastructure/utils';
|
|
9
|
-
import type { BaseProps } from '../../domain/types';
|
|
10
|
-
|
|
11
|
-
export interface NavbarProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
12
|
-
variant?: 'default' | 'sticky' | 'fixed';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const variantStyles: Record<'default' | 'sticky' | 'fixed', string> = {
|
|
16
|
-
default: 'relative',
|
|
17
|
-
sticky: 'sticky top-0 z-50',
|
|
18
|
-
fixed: 'fixed top-0 left-0 right-0 z-50',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const Navbar = React.memo(forwardRef<HTMLElement, NavbarProps>(
|
|
22
|
-
({ className, variant = 'default', children, ...props }, ref) => {
|
|
23
|
-
return (
|
|
24
|
-
<nav
|
|
25
|
-
ref={ref}
|
|
26
|
-
className={cn(
|
|
27
|
-
'flex h-16 items-center gap-4 border-b border-border/40 bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60',
|
|
28
|
-
variantStyles[variant],
|
|
29
|
-
className
|
|
30
|
-
)}
|
|
31
|
-
{...props}
|
|
32
|
-
>
|
|
33
|
-
{children}
|
|
34
|
-
</nav>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
));
|
|
38
|
-
|
|
39
|
-
Navbar.displayName = 'Navbar';
|
|
40
|
-
|
|
41
|
-
export const NavbarBrand = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
42
|
-
({ className, ...props }, ref) => (
|
|
43
|
-
<div
|
|
44
|
-
ref={ref}
|
|
45
|
-
className={cn('flex items-center gap-2 font-bold text-lg', className)}
|
|
46
|
-
{...props}
|
|
47
|
-
/>
|
|
48
|
-
)
|
|
49
|
-
));
|
|
50
|
-
|
|
51
|
-
NavbarBrand.displayName = 'NavbarBrand';
|
|
52
|
-
|
|
53
|
-
export const NavbarLinks = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
54
|
-
({ className, ...props }, ref) => (
|
|
55
|
-
<div
|
|
56
|
-
ref={ref}
|
|
57
|
-
className={cn('hidden items-center gap-6 md:flex', className)}
|
|
58
|
-
{...props}
|
|
59
|
-
/>
|
|
60
|
-
)
|
|
61
|
-
));
|
|
62
|
-
|
|
63
|
-
NavbarLinks.displayName = 'NavbarLinks';
|
|
64
|
-
|
|
65
|
-
export const NavbarActions = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
66
|
-
({ className, ...props }, ref) => (
|
|
67
|
-
<div
|
|
68
|
-
ref={ref}
|
|
69
|
-
className={cn('ml-auto flex items-center gap-2', className)}
|
|
70
|
-
{...props}
|
|
71
|
-
/>
|
|
72
|
-
)
|
|
73
|
-
));
|
|
74
|
-
|
|
75
|
-
NavbarActions.displayName = 'NavbarActions';
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NewsletterSignup Component (Organism)
|
|
3
|
-
* @description Newsletter subscription form with email validation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
7
|
-
import { cn } from '../../infrastructure/utils';
|
|
8
|
-
import type { BaseProps } from '../../domain/types';
|
|
9
|
-
import { Icon } from '../atoms/Icon';
|
|
10
|
-
|
|
11
|
-
const SUBSCRIBER_COUNT_MIN = 1000;
|
|
12
|
-
const SUBSCRIBER_COUNT_MAX = 6000;
|
|
13
|
-
|
|
14
|
-
export interface NewsletterSignupProps extends BaseProps {
|
|
15
|
-
onSubscribe?: (email: string) => Promise<void>;
|
|
16
|
-
subscriberCountMin?: number;
|
|
17
|
-
subscriberCountMax?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const NewsletterSignup = ({
|
|
21
|
-
onSubscribe,
|
|
22
|
-
subscriberCountMin = SUBSCRIBER_COUNT_MIN,
|
|
23
|
-
subscriberCountMax = SUBSCRIBER_COUNT_MAX,
|
|
24
|
-
className,
|
|
25
|
-
}: NewsletterSignupProps) => {
|
|
26
|
-
const [email, setEmail] = useState('');
|
|
27
|
-
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
28
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
-
const [error, setError] = useState('');
|
|
30
|
-
|
|
31
|
-
// FIXED: Generate random count per component instance, not at module load
|
|
32
|
-
const subscriberCount = useMemo(
|
|
33
|
-
() => Math.floor(Math.random() * (subscriberCountMax - subscriberCountMin + 1)) + subscriberCountMin,
|
|
34
|
-
[subscriberCountMin, subscriberCountMax]
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const validateEmail = (email: string) => {
|
|
38
|
-
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
39
|
-
return re.test(email);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Reset success message after 5 seconds with cleanup
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (isSubscribed) {
|
|
45
|
-
const timeout = setTimeout(() => {
|
|
46
|
-
setIsSubscribed(false);
|
|
47
|
-
}, 5000);
|
|
48
|
-
return () => clearTimeout(timeout);
|
|
49
|
-
}
|
|
50
|
-
}, [isSubscribed]);
|
|
51
|
-
|
|
52
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
53
|
-
e.preventDefault();
|
|
54
|
-
setError('');
|
|
55
|
-
|
|
56
|
-
// Validate email
|
|
57
|
-
if (!email) {
|
|
58
|
-
setError('Please enter your email address');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!validateEmail(email)) {
|
|
63
|
-
setError('Please enter a valid email address');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
setIsLoading(true);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
// Call the subscribe function if provided
|
|
71
|
-
if (onSubscribe) {
|
|
72
|
-
await onSubscribe(email);
|
|
73
|
-
} else {
|
|
74
|
-
// Simulate API call for demo purposes
|
|
75
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
setIsLoading(false);
|
|
79
|
-
setIsSubscribed(true);
|
|
80
|
-
setEmail('');
|
|
81
|
-
} catch (err) {
|
|
82
|
-
setIsLoading(false);
|
|
83
|
-
setError('Failed to subscribe. Please try again.');
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
if (isSubscribed) {
|
|
88
|
-
return (
|
|
89
|
-
<div className={cn('bg-bg-card rounded-xl p-4 border border-border', className)}>
|
|
90
|
-
<div className="flex items-center gap-3 text-green-600">
|
|
91
|
-
<Icon className="text-green-600" size="lg">
|
|
92
|
-
<path
|
|
93
|
-
strokeLinecap="round"
|
|
94
|
-
strokeLinejoin="round"
|
|
95
|
-
strokeWidth={2}
|
|
96
|
-
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
97
|
-
/>
|
|
98
|
-
</Icon>
|
|
99
|
-
<div>
|
|
100
|
-
<div className="font-semibold text-sm">Thanks for subscribing!</div>
|
|
101
|
-
<div className="text-xs text-text-secondary">Check your inbox to confirm your subscription</div>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div className={cn('bg-bg-card rounded-xl p-4 border border-border', className)}>
|
|
110
|
-
<div className="flex items-center gap-2 mb-3">
|
|
111
|
-
<Icon className="text-primary-light" size="lg">
|
|
112
|
-
<path
|
|
113
|
-
strokeLinecap="round"
|
|
114
|
-
strokeLinejoin="round"
|
|
115
|
-
strokeWidth={2}
|
|
116
|
-
d="M3 8l7.89 5.26a2 2 0 002.22 0l7.89-5.26a2 2 0 002.22 0L21 8V5a2 2 0 00-2-2H5a2 2 0 00-2 2v3a2 2 0 002.22 0z"
|
|
117
|
-
/>
|
|
118
|
-
</Icon>
|
|
119
|
-
<h3 className="text-sm font-semibold text-text-primary">Newsletter</h3>
|
|
120
|
-
</div>
|
|
121
|
-
<p className="text-xs text-text-secondary mb-3">
|
|
122
|
-
Get the latest articles delivered straight to your inbox. No spam, ever.
|
|
123
|
-
</p>
|
|
124
|
-
<form onSubmit={handleSubmit} className="space-y-2">
|
|
125
|
-
<input
|
|
126
|
-
type="email"
|
|
127
|
-
placeholder="your@email.com"
|
|
128
|
-
value={email}
|
|
129
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
130
|
-
className="w-full px-3 py-2 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
|
|
131
|
-
disabled={isLoading}
|
|
132
|
-
/>
|
|
133
|
-
{error && (
|
|
134
|
-
<p className="text-xs text-red-500">{error}</p>
|
|
135
|
-
)}
|
|
136
|
-
<button
|
|
137
|
-
type="submit"
|
|
138
|
-
disabled={isLoading}
|
|
139
|
-
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary-gradient text-text-primary rounded-lg font-medium hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
|
140
|
-
>
|
|
141
|
-
{isLoading ? (
|
|
142
|
-
<>Subscribing...</>
|
|
143
|
-
) : (
|
|
144
|
-
<>
|
|
145
|
-
<Icon size="sm">
|
|
146
|
-
<path
|
|
147
|
-
strokeLinecap="round"
|
|
148
|
-
strokeLinejoin="round"
|
|
149
|
-
strokeWidth={2}
|
|
150
|
-
d="M22 2L11 13M22 2l-7 20M2 2l15 15L2 2l15-15"
|
|
151
|
-
/>
|
|
152
|
-
</Icon>
|
|
153
|
-
Subscribe
|
|
154
|
-
</>
|
|
155
|
-
)}
|
|
156
|
-
</button>
|
|
157
|
-
</form>
|
|
158
|
-
<p className="text-xs text-text-secondary mt-2">
|
|
159
|
-
Join {subscriberCount.toLocaleString()}+ subscribers
|
|
160
|
-
</p>
|
|
161
|
-
</div>
|
|
162
|
-
);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
NewsletterSignup.displayName = 'NewsletterSignup';
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QuickActionCard Component (Organism)
|
|
3
|
-
* @description Card component for quick navigation actions with icon (Responsive)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { forwardRef } from 'react';
|
|
7
|
-
import { cn, getSpacing, getIconSize, getContainerSize, getTextSize, getGap } from '../../infrastructure/utils';
|
|
8
|
-
import type { BaseProps, SizeVariant } from '../../domain/types';
|
|
9
|
-
|
|
10
|
-
export interface QuickActionCardProps extends BaseProps {
|
|
11
|
-
label: string;
|
|
12
|
-
icon?: React.ComponentType<{ className?: string }>;
|
|
13
|
-
iconColor?: string;
|
|
14
|
-
iconBgColor?: string;
|
|
15
|
-
onClick?: () => void;
|
|
16
|
-
href?: string;
|
|
17
|
-
size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
|
|
18
|
-
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
19
|
-
responsiveLayout?: boolean; // Enable responsive layout changes
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const QuickActionCard = forwardRef<HTMLAnchorElement | HTMLDivElement, QuickActionCardProps>(
|
|
23
|
-
(
|
|
24
|
-
{
|
|
25
|
-
className,
|
|
26
|
-
label,
|
|
27
|
-
icon: Icon,
|
|
28
|
-
iconColor = 'text-white',
|
|
29
|
-
iconBgColor = 'bg-primary',
|
|
30
|
-
onClick,
|
|
31
|
-
href,
|
|
32
|
-
size = 'md',
|
|
33
|
-
target = '_self',
|
|
34
|
-
responsiveLayout = true,
|
|
35
|
-
...props
|
|
36
|
-
},
|
|
37
|
-
ref
|
|
38
|
-
) => {
|
|
39
|
-
const content = (
|
|
40
|
-
<>
|
|
41
|
-
{Icon && (
|
|
42
|
-
<div className={cn(getContainerSize(size), 'rounded-lg flex items-center justify-center flex-shrink-0', iconBgColor)}>
|
|
43
|
-
<Icon className={cn(getIconSize(size), iconColor)} />
|
|
44
|
-
</div>
|
|
45
|
-
)}
|
|
46
|
-
<span className={cn(
|
|
47
|
-
'font-medium text-foreground group-hover:text-primary transition-colors',
|
|
48
|
-
getTextSize(size),
|
|
49
|
-
responsiveLayout && 'text-center sm:text-left'
|
|
50
|
-
)}>
|
|
51
|
-
{label}
|
|
52
|
-
</span>
|
|
53
|
-
</>
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const baseClasses = cn(
|
|
57
|
-
'bg-card border border-border rounded-xl',
|
|
58
|
-
responsiveLayout ? 'flex flex-col sm:flex-row items-center justify-center sm:justify-start' : 'flex items-center',
|
|
59
|
-
'hover:border-primary/50 hover:shadow-md transition-all duration-200 group',
|
|
60
|
-
getSpacing('p', size),
|
|
61
|
-
getGap(size === 'sm' ? 'sm' : size),
|
|
62
|
-
className
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (href) {
|
|
66
|
-
return (
|
|
67
|
-
<a
|
|
68
|
-
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
69
|
-
href={href}
|
|
70
|
-
target={target}
|
|
71
|
-
className={baseClasses}
|
|
72
|
-
onClick={onClick}
|
|
73
|
-
{...props}
|
|
74
|
-
>
|
|
75
|
-
{content}
|
|
76
|
-
</a>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div
|
|
82
|
-
ref={ref as React.Ref<HTMLDivElement>}
|
|
83
|
-
className={baseClasses}
|
|
84
|
-
onClick={onClick}
|
|
85
|
-
{...props}
|
|
86
|
-
>
|
|
87
|
-
{content}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
QuickActionCard.displayName = 'QuickActionCard';
|