@pattern-stack/frontend-patterns 0.0.3 → 0.0.4
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/dist/index.es.js +1 -1
- package/dist/index.js +1 -0
- package/package.json +5 -3
- package/src/App.css +42 -0
- package/src/App.tsx +54 -0
- package/src/__tests__/README.md +221 -0
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
- package/src/__tests__/atoms/ui/button.test.tsx +68 -0
- package/src/__tests__/atoms/utils/simple.test.ts +18 -0
- package/src/__tests__/atoms/utils/utils.test.ts +77 -0
- package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
- package/src/__tests__/setup.ts +51 -0
- package/src/__tests__/utils.tsx +123 -0
- package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
- package/src/atoms/composed/Accordion/index.ts +1 -0
- package/src/atoms/composed/Alert/Alert.tsx +132 -0
- package/src/atoms/composed/Alert/index.ts +1 -0
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
- package/src/atoms/composed/Breadcrumb/index.ts +1 -0
- package/src/atoms/composed/Chart/Chart.tsx +425 -0
- package/src/atoms/composed/Chart/index.ts +2 -0
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
- package/src/atoms/composed/ColorSwatch/index.ts +1 -0
- package/src/atoms/composed/DarkModeToggle.tsx +66 -0
- package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
- package/src/atoms/composed/DataBadge/index.ts +1 -0
- package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
- package/src/atoms/composed/DataTable/index.ts +2 -0
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
- package/src/atoms/composed/DateTimePicker/index.ts +2 -0
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
- package/src/atoms/composed/DetailedCard/index.ts +2 -0
- package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
- package/src/atoms/composed/EmptyState/index.ts +1 -0
- package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
- package/src/atoms/composed/FileUpload/index.ts +2 -0
- package/src/atoms/composed/FormField/FormField.tsx +92 -0
- package/src/atoms/composed/FormField/index.ts +1 -0
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
- package/src/atoms/composed/GlobalSearch/index.ts +1 -0
- package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
- package/src/atoms/composed/IconBadge/index.ts +2 -0
- package/src/atoms/composed/Modal/Modal.tsx +223 -0
- package/src/atoms/composed/Modal/index.ts +2 -0
- package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
- package/src/atoms/composed/ProgressBar/index.ts +1 -0
- package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
- package/src/atoms/composed/StatCard/index.ts +1 -0
- package/src/atoms/composed/StyleGuide.tsx +717 -0
- package/src/atoms/composed/Toast/Toast.tsx +219 -0
- package/src/atoms/composed/Toast/index.ts +1 -0
- package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
- package/src/atoms/composed/Tooltip/index.ts +1 -0
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
- package/src/atoms/composed/UserAvatar/index.ts +1 -0
- package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
- package/src/atoms/composed/UserMenu/index.ts +1 -0
- package/src/atoms/composed/index.ts +29 -0
- package/src/atoms/hooks/useApi.ts +80 -0
- package/src/atoms/hooks/useHealth.ts +17 -0
- package/src/atoms/index.ts +13 -0
- package/src/atoms/services/api/client.ts +134 -0
- package/src/atoms/services/auth-service.ts +248 -0
- package/src/atoms/services/health.ts +15 -0
- package/src/atoms/services/index.ts +3 -0
- package/src/atoms/shared/config/constants.ts +17 -0
- package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
- package/src/atoms/shared/config/environment.ts +10 -0
- package/src/atoms/shared/index.ts +4 -0
- package/src/atoms/shared/styles/color-palettes.css +566 -0
- package/src/atoms/types/auth.ts +62 -0
- package/src/atoms/types/generated.ts +1469 -0
- package/src/atoms/types/index.ts +4 -0
- package/src/atoms/types/loading.ts +28 -0
- package/src/atoms/ui/Badge.tsx +30 -0
- package/src/atoms/ui/ErrorBoundary.tsx +59 -0
- package/src/atoms/ui/Select.tsx +53 -0
- package/src/atoms/ui/Switch.tsx +42 -0
- package/src/atoms/ui/Tabs.tsx +118 -0
- package/src/atoms/ui/avatar.tsx +48 -0
- package/src/atoms/ui/button.tsx +70 -0
- package/src/atoms/ui/card.tsx +76 -0
- package/src/atoms/ui/dropdown-menu.tsx +199 -0
- package/src/atoms/ui/index.ts +39 -0
- package/src/atoms/ui/input.tsx +23 -0
- package/src/atoms/ui/label.tsx +23 -0
- package/src/atoms/ui/skeleton.tsx +13 -0
- package/src/atoms/ui/spinner.tsx +49 -0
- package/src/atoms/ui/table.tsx +116 -0
- package/src/atoms/utils/animations.ts +135 -0
- package/src/atoms/utils/tooltip-helpers.ts +140 -0
- package/src/atoms/utils/utils.ts +9 -0
- package/src/features/auth/components/LoginForm.tsx +168 -0
- package/src/features/auth/components/LogoutButton.tsx +19 -0
- package/src/features/auth/components/ProtectedRoute.tsx +60 -0
- package/src/features/auth/components/index.ts +4 -0
- package/src/features/auth/hooks/index.ts +2 -0
- package/src/features/auth/hooks/useAuth.tsx +205 -0
- package/src/features/auth/hooks/usePermissions.ts +35 -0
- package/src/features/auth/index.ts +2 -0
- package/src/features/index.ts +2 -0
- package/src/index.css +704 -0
- package/src/index.ts +13 -0
- package/src/main.tsx +48 -0
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +75 -0
- package/src/molecules/forms/SearchInput.tsx +259 -0
- package/src/molecules/forms/index.ts +4 -0
- package/src/molecules/index.ts +4 -0
- package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
- package/src/molecules/layout/AppHeader/index.ts +1 -0
- package/src/molecules/layout/AppLayout.tsx +29 -0
- package/src/molecules/layout/PageTemplate.tsx +87 -0
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
- package/src/molecules/layout/SectionHeader/index.ts +1 -0
- package/src/molecules/layout/ShowcaseSection.tsx +57 -0
- package/src/molecules/layout/Sidebar.tsx +144 -0
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
- package/src/molecules/layout/SidebarButton/index.ts +1 -0
- package/src/molecules/layout/SidebarContext.tsx +31 -0
- package/src/molecules/layout/index.ts +7 -0
- package/src/molecules/navigation/NavMenu.tsx +188 -0
- package/src/molecules/navigation/Pagination.tsx +172 -0
- package/src/molecules/navigation/index.ts +4 -0
- package/src/organisms/index.ts +5 -0
- package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
- package/src/organisms/showcase/index.ts +1 -0
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
- package/src/pages/AdminShowcase/index.tsx +3 -0
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
- package/src/pages/ComponentShowcase/index.tsx +188 -0
- package/src/pages/index.ts +2 -0
- package/src/templates/AuthTemplate.tsx +216 -0
- package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
- package/src/templates/DashboardTemplate.tsx +232 -0
- package/src/templates/DataTemplate.tsx +319 -0
- package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
- package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
- package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
- package/src/templates/admin/index.ts +29 -0
- package/src/templates/factory.tsx +169 -0
- package/src/templates/index.ts +37 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { X, CheckCircle, AlertTriangle, AlertCircle, Info } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../utils/utils';
|
|
4
|
+
import { Button } from '../../ui/button';
|
|
5
|
+
import { getAnimationClasses, animationPresets } from '../../utils/animations';
|
|
6
|
+
|
|
7
|
+
export interface ToastProps {
|
|
8
|
+
/** Toast content */
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** Toast status type */
|
|
11
|
+
status?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
12
|
+
/** Toast title */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Auto-dismiss duration in milliseconds */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Whether toast can be dismissed */
|
|
17
|
+
dismissible?: boolean;
|
|
18
|
+
/** Dismiss handler */
|
|
19
|
+
onDismiss?: () => void;
|
|
20
|
+
/** Toast position */
|
|
21
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
|
22
|
+
/** Custom icon */
|
|
23
|
+
icon?: React.ReactNode;
|
|
24
|
+
/** Hide default icon */
|
|
25
|
+
hideIcon?: boolean;
|
|
26
|
+
/** Additional CSS classes */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Show progress bar for auto-dismiss */
|
|
29
|
+
showProgress?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const statusIcons = {
|
|
33
|
+
success: CheckCircle,
|
|
34
|
+
warning: AlertTriangle,
|
|
35
|
+
error: AlertCircle,
|
|
36
|
+
info: Info,
|
|
37
|
+
neutral: Info
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Toast: React.FC<ToastProps> = ({
|
|
41
|
+
children,
|
|
42
|
+
status = 'neutral',
|
|
43
|
+
title,
|
|
44
|
+
duration = 0, // 0 means no auto-dismiss
|
|
45
|
+
dismissible = true,
|
|
46
|
+
onDismiss,
|
|
47
|
+
position = 'top-right',
|
|
48
|
+
icon,
|
|
49
|
+
hideIcon = false,
|
|
50
|
+
className,
|
|
51
|
+
showProgress = false
|
|
52
|
+
}) => {
|
|
53
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
54
|
+
const [progress, setProgress] = useState(100);
|
|
55
|
+
|
|
56
|
+
const IconComponent = statusIcons[status];
|
|
57
|
+
const displayIcon = icon || (!hideIcon && <IconComponent className="w-5 h-5" />);
|
|
58
|
+
|
|
59
|
+
const handleDismiss = useCallback(() => {
|
|
60
|
+
setIsVisible(false);
|
|
61
|
+
setTimeout(() => onDismiss?.(), 300); // Wait for exit animation
|
|
62
|
+
}, [onDismiss]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (duration > 0) {
|
|
66
|
+
const dismissTimer = setTimeout(handleDismiss, duration);
|
|
67
|
+
|
|
68
|
+
if (showProgress) {
|
|
69
|
+
const progressInterval = setInterval(() => {
|
|
70
|
+
setProgress(prev => {
|
|
71
|
+
const decrement = 100 / (duration / 50); // Update every 50ms
|
|
72
|
+
return Math.max(prev - decrement, 0);
|
|
73
|
+
});
|
|
74
|
+
}, 50);
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
clearTimeout(dismissTimer);
|
|
78
|
+
clearInterval(progressInterval);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return () => clearTimeout(dismissTimer);
|
|
83
|
+
}
|
|
84
|
+
}, [duration, showProgress, handleDismiss]);
|
|
85
|
+
|
|
86
|
+
const positionClasses = {
|
|
87
|
+
'top-right': 'top-4 right-4',
|
|
88
|
+
'top-left': 'top-4 left-4',
|
|
89
|
+
'bottom-right': 'bottom-4 right-4',
|
|
90
|
+
'bottom-left': 'bottom-4 left-4',
|
|
91
|
+
'top-center': 'top-4 left-1/2 -translate-x-1/2',
|
|
92
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2'
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const statusClasses = {
|
|
96
|
+
success: 'bg-background text-status-success border-status-success/30 shadow-status-success/10',
|
|
97
|
+
warning: 'bg-background text-status-warning border-status-warning/30 shadow-status-warning/10',
|
|
98
|
+
error: 'bg-background text-status-error border-status-error/30 shadow-status-error/10',
|
|
99
|
+
info: 'bg-background text-status-info border-status-info/30 shadow-status-info/10',
|
|
100
|
+
neutral: 'bg-background text-foreground border-border shadow-muted/20'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!isVisible) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
'fixed z-50 w-full max-w-sm',
|
|
111
|
+
positionClasses[position]
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
'rounded-lg border p-4 shadow-lg backdrop-blur-sm',
|
|
117
|
+
'flex items-start gap-3 relative overflow-hidden',
|
|
118
|
+
statusClasses[status],
|
|
119
|
+
getAnimationClasses(animationPresets.card),
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
role="alert"
|
|
123
|
+
data-component-name="Toast"
|
|
124
|
+
>
|
|
125
|
+
{/* Icon */}
|
|
126
|
+
{displayIcon && (
|
|
127
|
+
<div className="flex-shrink-0 mt-0.5">
|
|
128
|
+
{displayIcon}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Content */}
|
|
133
|
+
<div className="flex-1 min-w-0">
|
|
134
|
+
{title && (
|
|
135
|
+
<h4 className="font-semibold mb-1 text-sm">
|
|
136
|
+
{title}
|
|
137
|
+
</h4>
|
|
138
|
+
)}
|
|
139
|
+
<div className="text-sm text-current">
|
|
140
|
+
{children}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Dismiss button */}
|
|
145
|
+
{dismissible && (
|
|
146
|
+
<Button
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="sm"
|
|
149
|
+
onClick={handleDismiss}
|
|
150
|
+
className={cn(
|
|
151
|
+
'flex-shrink-0 h-auto p-1 -mt-1 -mr-1',
|
|
152
|
+
'hover:bg-current/10 text-current'
|
|
153
|
+
)}
|
|
154
|
+
aria-label="Dismiss notification"
|
|
155
|
+
>
|
|
156
|
+
<X className="w-4 h-4" />
|
|
157
|
+
</Button>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Progress bar */}
|
|
161
|
+
{showProgress && duration > 0 && (
|
|
162
|
+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-current/10">
|
|
163
|
+
<div
|
|
164
|
+
className="h-full bg-current/30 transition-all duration-50 ease-linear"
|
|
165
|
+
style={{ width: `${progress}%` }}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Toast container component for managing multiple toasts
|
|
175
|
+
export interface ToastContainerProps {
|
|
176
|
+
/** Toast notifications */
|
|
177
|
+
toasts: Array<ToastProps & { id: string }>;
|
|
178
|
+
/** Container position */
|
|
179
|
+
position?: ToastProps['position'];
|
|
180
|
+
/** Maximum number of visible toasts */
|
|
181
|
+
maxToasts?: number;
|
|
182
|
+
/** Additional CSS classes */
|
|
183
|
+
className?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const ToastContainer: React.FC<ToastContainerProps> = ({
|
|
187
|
+
toasts,
|
|
188
|
+
position = 'top-right',
|
|
189
|
+
maxToasts = 5,
|
|
190
|
+
className
|
|
191
|
+
}) => {
|
|
192
|
+
const visibleToasts = toasts.slice(0, maxToasts);
|
|
193
|
+
|
|
194
|
+
const positionClasses = {
|
|
195
|
+
'top-right': 'top-4 right-4 flex-col',
|
|
196
|
+
'top-left': 'top-4 left-4 flex-col',
|
|
197
|
+
'bottom-right': 'bottom-4 right-4 flex-col-reverse',
|
|
198
|
+
'bottom-left': 'bottom-4 left-4 flex-col-reverse',
|
|
199
|
+
'top-center': 'top-4 left-1/2 -translate-x-1/2 flex-col',
|
|
200
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse'
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'fixed z-50 flex gap-2 pointer-events-none',
|
|
207
|
+
positionClasses[position],
|
|
208
|
+
className
|
|
209
|
+
)}
|
|
210
|
+
data-component-name="ToastContainer"
|
|
211
|
+
>
|
|
212
|
+
{visibleToasts.map(({ id, ...toastProps }) => (
|
|
213
|
+
<div key={id} className="pointer-events-auto">
|
|
214
|
+
<Toast {...toastProps} position={position} />
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toast, ToastContainer, type ToastProps, type ToastContainerProps } from './Toast';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { cn } from '../../utils/utils';
|
|
3
|
+
import { getAnimationClasses, animationPresets } from '../../utils/animations';
|
|
4
|
+
|
|
5
|
+
export interface TooltipProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
content: React.ReactNode;
|
|
8
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
9
|
+
variant?: 'default' | 'info' | 'warning' | 'error';
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
delay?: number;
|
|
13
|
+
offset?: number;
|
|
14
|
+
trigger?: 'hover' | 'focus' | 'both';
|
|
15
|
+
className?: string;
|
|
16
|
+
contentClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Tooltip = ({
|
|
20
|
+
children,
|
|
21
|
+
content,
|
|
22
|
+
position = 'top',
|
|
23
|
+
variant = 'default',
|
|
24
|
+
size = 'md',
|
|
25
|
+
disabled = false,
|
|
26
|
+
delay = 300,
|
|
27
|
+
offset = 8,
|
|
28
|
+
trigger = 'both',
|
|
29
|
+
className,
|
|
30
|
+
contentClassName
|
|
31
|
+
}: TooltipProps) => {
|
|
32
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
33
|
+
const [actualPosition, setActualPosition] = useState(position);
|
|
34
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
|
+
|
|
38
|
+
const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
|
|
39
|
+
sm: 'px-2 py-1 text-xs max-w-48',
|
|
40
|
+
md: 'px-3 py-2 text-sm max-w-64',
|
|
41
|
+
lg: 'px-4 py-3 text-base max-w-80',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const variantClasses: Record<'default' | 'info' | 'warning' | 'error', string> = {
|
|
45
|
+
default: 'bg-card/95 text-card-foreground border-border/30',
|
|
46
|
+
info: 'status-info text-card-foreground border-status-info/30',
|
|
47
|
+
warning: 'status-warning text-card-foreground border-status-warning/30',
|
|
48
|
+
error: 'status-error text-card-foreground border-status-error/30',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const positionClasses: Record<'top' | 'bottom' | 'left' | 'right', string> = {
|
|
52
|
+
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
|
|
53
|
+
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
|
|
54
|
+
left: 'right-full top-1/2 transform -translate-y-1/2 mr-2',
|
|
55
|
+
right: 'left-full top-1/2 transform -translate-y-1/2 ml-2',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const arrowClasses: Record<'top' | 'bottom' | 'left' | 'right', string> = {
|
|
59
|
+
top: 'top-full left-1/2 transform -translate-x-1/2 border-l-transparent border-r-transparent border-b-transparent',
|
|
60
|
+
bottom: 'bottom-full left-1/2 transform -translate-x-1/2 border-l-transparent border-r-transparent border-t-transparent',
|
|
61
|
+
left: 'left-full top-1/2 transform -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent',
|
|
62
|
+
right: 'right-full top-1/2 transform -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const showTooltip = () => {
|
|
66
|
+
if (disabled) return;
|
|
67
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
68
|
+
|
|
69
|
+
timeoutRef.current = setTimeout(() => {
|
|
70
|
+
setIsVisible(true);
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
checkAndAdjustPosition();
|
|
73
|
+
});
|
|
74
|
+
}, delay);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const hideTooltip = () => {
|
|
78
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
79
|
+
setIsVisible(false);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const checkAndAdjustPosition = () => {
|
|
83
|
+
if (!triggerRef.current || !tooltipRef.current) return;
|
|
84
|
+
|
|
85
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
86
|
+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
87
|
+
const viewport = {
|
|
88
|
+
width: window.innerWidth,
|
|
89
|
+
height: window.innerHeight,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let newPosition = position;
|
|
93
|
+
|
|
94
|
+
// Check if tooltip goes outside viewport and adjust position
|
|
95
|
+
switch (position) {
|
|
96
|
+
case 'top':
|
|
97
|
+
if (triggerRect.top - tooltipRect.height - offset < 0) {
|
|
98
|
+
newPosition = 'bottom';
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case 'bottom':
|
|
102
|
+
if (triggerRect.bottom + tooltipRect.height + offset > viewport.height) {
|
|
103
|
+
newPosition = 'top';
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case 'left':
|
|
107
|
+
if (triggerRect.left - tooltipRect.width - offset < 0) {
|
|
108
|
+
newPosition = 'right';
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'right':
|
|
112
|
+
if (triggerRect.right + tooltipRect.width + offset > viewport.width) {
|
|
113
|
+
newPosition = 'left';
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setActualPosition(newPosition);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
return () => {
|
|
123
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const triggerProps = {
|
|
128
|
+
...(trigger === 'hover' || trigger === 'both' ? {
|
|
129
|
+
onMouseEnter: showTooltip,
|
|
130
|
+
onMouseLeave: hideTooltip,
|
|
131
|
+
} : {}),
|
|
132
|
+
...(trigger === 'focus' || trigger === 'both' ? {
|
|
133
|
+
onFocus: showTooltip,
|
|
134
|
+
onBlur: hideTooltip,
|
|
135
|
+
} : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const tooltipBaseClasses = cn(
|
|
139
|
+
'absolute z-50 rounded-lg border backdrop-blur-sm shadow-lg pointer-events-none',
|
|
140
|
+
'animate-fade-in transition-all duration-200 ease-out',
|
|
141
|
+
sizeClasses[size],
|
|
142
|
+
variantClasses[variant],
|
|
143
|
+
positionClasses[actualPosition],
|
|
144
|
+
getAnimationClasses({
|
|
145
|
+
...animationPresets.subtle,
|
|
146
|
+
timing: 'fast'
|
|
147
|
+
}),
|
|
148
|
+
contentClassName
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const arrowBaseClasses = cn(
|
|
152
|
+
'absolute w-0 h-0 border-4',
|
|
153
|
+
arrowClasses[actualPosition]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const getArrowColor = () => {
|
|
157
|
+
switch (variant) {
|
|
158
|
+
case 'info':
|
|
159
|
+
return 'border-t-status-info';
|
|
160
|
+
case 'warning':
|
|
161
|
+
return 'border-t-status-warning';
|
|
162
|
+
case 'error':
|
|
163
|
+
return 'border-t-status-error';
|
|
164
|
+
default:
|
|
165
|
+
return 'border-t-card';
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className={cn("relative inline-block", className)}>
|
|
171
|
+
<div
|
|
172
|
+
ref={triggerRef}
|
|
173
|
+
{...triggerProps}
|
|
174
|
+
className="cursor-help"
|
|
175
|
+
aria-describedby={isVisible ? 'tooltip' : undefined}
|
|
176
|
+
data-component-name="TooltipTrigger"
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{isVisible && !disabled && (
|
|
182
|
+
<div
|
|
183
|
+
ref={tooltipRef}
|
|
184
|
+
id="tooltip"
|
|
185
|
+
role="tooltip"
|
|
186
|
+
aria-hidden={!isVisible}
|
|
187
|
+
className={tooltipBaseClasses}
|
|
188
|
+
style={{
|
|
189
|
+
marginTop: actualPosition === 'bottom' ? `${offset}px` : actualPosition === 'top' ? `-${offset}px` : '0',
|
|
190
|
+
marginLeft: actualPosition === 'right' ? `${offset}px` : actualPosition === 'left' ? `-${offset}px` : '0',
|
|
191
|
+
}}
|
|
192
|
+
data-component-name="Tooltip"
|
|
193
|
+
>
|
|
194
|
+
{content}
|
|
195
|
+
|
|
196
|
+
{/* Arrow */}
|
|
197
|
+
<div
|
|
198
|
+
className={cn(
|
|
199
|
+
arrowBaseClasses,
|
|
200
|
+
getArrowColor()
|
|
201
|
+
)}
|
|
202
|
+
style={{
|
|
203
|
+
borderTopColor: actualPosition === 'bottom' ? 'inherit' : 'transparent',
|
|
204
|
+
borderBottomColor: actualPosition === 'top' ? 'inherit' : 'transparent',
|
|
205
|
+
borderLeftColor: actualPosition === 'right' ? 'inherit' : 'transparent',
|
|
206
|
+
borderRightColor: actualPosition === 'left' ? 'inherit' : 'transparent',
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Tooltip';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { User, Settings, LogOut, UserCircle, Moon, Sun } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
AvatarFallback,
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
Button
|
|
12
|
+
} from '../../ui';
|
|
13
|
+
import { useAuth } from '../../../features/auth';
|
|
14
|
+
import { cn } from '../../utils/utils';
|
|
15
|
+
|
|
16
|
+
interface UserAvatarProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const UserAvatar: React.FC<UserAvatarProps> = ({ className, category = 1 }) => {
|
|
22
|
+
const { user, logout } = useAuth();
|
|
23
|
+
const [isDark, setIsDark] = React.useState(() =>
|
|
24
|
+
document.documentElement.classList.contains('dark')
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const toggleDarkMode = () => {
|
|
28
|
+
const newDarkMode = !isDark;
|
|
29
|
+
setIsDark(newDarkMode);
|
|
30
|
+
|
|
31
|
+
if (newDarkMode) {
|
|
32
|
+
document.documentElement.classList.add('dark');
|
|
33
|
+
} else {
|
|
34
|
+
document.documentElement.classList.remove('dark');
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const userInitials = (user as any)?.name
|
|
39
|
+
? (user as any).name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
|
|
40
|
+
: user?.email?.[0]?.toUpperCase() || 'U';
|
|
41
|
+
|
|
42
|
+
const avatarStyles = {
|
|
43
|
+
background: `linear-gradient(135deg, hsl(var(--category-${category}) / 0.1), hsl(var(--category-${category}) / 0.2))`,
|
|
44
|
+
borderColor: `hsl(var(--category-${category}) / 0.3)`,
|
|
45
|
+
color: `hsl(var(--category-${category}))`
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleLogout = () => {
|
|
49
|
+
logout();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn("flex items-center", className)}>
|
|
54
|
+
<DropdownMenu>
|
|
55
|
+
<DropdownMenuTrigger asChild>
|
|
56
|
+
<Button variant="ghost" className="relative h-9 w-9 rounded p-0 hover:bg-muted/30 transition-colors">
|
|
57
|
+
<Avatar className="h-9 w-9">
|
|
58
|
+
<AvatarFallback
|
|
59
|
+
className="border font-medium text-sm"
|
|
60
|
+
style={avatarStyles}
|
|
61
|
+
>
|
|
62
|
+
<User className="w-4 h-4" />
|
|
63
|
+
</AvatarFallback>
|
|
64
|
+
</Avatar>
|
|
65
|
+
</Button>
|
|
66
|
+
</DropdownMenuTrigger>
|
|
67
|
+
<DropdownMenuContent
|
|
68
|
+
className="w-56 bg-card/95 backdrop-blur-sm border-border/30"
|
|
69
|
+
align="end"
|
|
70
|
+
forceMount
|
|
71
|
+
>
|
|
72
|
+
{/* User Info */}
|
|
73
|
+
<div className="flex items-center space-x-3 p-3">
|
|
74
|
+
<Avatar className="h-8 w-8">
|
|
75
|
+
<AvatarFallback
|
|
76
|
+
className="border font-medium text-xs"
|
|
77
|
+
style={avatarStyles}
|
|
78
|
+
>
|
|
79
|
+
{userInitials}
|
|
80
|
+
</AvatarFallback>
|
|
81
|
+
</Avatar>
|
|
82
|
+
<div className="flex flex-col space-y-0.5">
|
|
83
|
+
<p className="text-sm font-medium leading-none text-foreground">
|
|
84
|
+
{(user as any)?.name || 'Design User'}
|
|
85
|
+
</p>
|
|
86
|
+
<p className="text-xs leading-none text-muted-foreground">
|
|
87
|
+
{user?.email || 'user@design.studio'}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<DropdownMenuSeparator />
|
|
93
|
+
|
|
94
|
+
{/* Dark Mode Toggle */}
|
|
95
|
+
<DropdownMenuItem
|
|
96
|
+
className="cursor-pointer focus:bg-muted/50"
|
|
97
|
+
onClick={toggleDarkMode}
|
|
98
|
+
>
|
|
99
|
+
{isDark ? (
|
|
100
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
101
|
+
) : (
|
|
102
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
103
|
+
)}
|
|
104
|
+
<span>{isDark ? 'Light Mode' : 'Dark Mode'}</span>
|
|
105
|
+
</DropdownMenuItem>
|
|
106
|
+
|
|
107
|
+
<DropdownMenuSeparator />
|
|
108
|
+
|
|
109
|
+
{/* Menu Items */}
|
|
110
|
+
<DropdownMenuItem
|
|
111
|
+
className="cursor-pointer focus:bg-muted/50"
|
|
112
|
+
onClick={() => alert('Profile coming soon!')}
|
|
113
|
+
>
|
|
114
|
+
<UserCircle className="mr-2 h-4 w-4" />
|
|
115
|
+
<span>Profile</span>
|
|
116
|
+
</DropdownMenuItem>
|
|
117
|
+
|
|
118
|
+
<DropdownMenuItem
|
|
119
|
+
className="cursor-pointer focus:bg-muted/50"
|
|
120
|
+
onClick={() => alert('Settings coming soon!')}
|
|
121
|
+
>
|
|
122
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
123
|
+
<span>Settings</span>
|
|
124
|
+
</DropdownMenuItem>
|
|
125
|
+
|
|
126
|
+
<DropdownMenuSeparator />
|
|
127
|
+
|
|
128
|
+
<DropdownMenuItem
|
|
129
|
+
className="cursor-pointer text-destructive focus:text-destructive focus:bg-destructive/10"
|
|
130
|
+
onClick={handleLogout}
|
|
131
|
+
>
|
|
132
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
133
|
+
<span>Logout</span>
|
|
134
|
+
</DropdownMenuItem>
|
|
135
|
+
</DropdownMenuContent>
|
|
136
|
+
</DropdownMenu>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UserAvatar } from './UserAvatar';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { UserAvatar } from '../UserAvatar';
|
|
3
|
+
import { cn } from '../../utils/utils';
|
|
4
|
+
|
|
5
|
+
interface UserMenuProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const UserMenu: React.FC<UserMenuProps> = ({ className, category }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn("flex items-center", className)}>
|
|
13
|
+
<UserAvatar category={category} />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UserMenu } from './UserMenu';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Business-specific shared components
|
|
2
|
+
export { DataBadge } from './DataBadge';
|
|
3
|
+
export { StatCard } from './StatCard';
|
|
4
|
+
export { DetailedCard } from './DetailedCard';
|
|
5
|
+
export { EmptyState } from './EmptyState';
|
|
6
|
+
export { ColorSwatch } from './ColorSwatch';
|
|
7
|
+
export { IconBadge } from './IconBadge';
|
|
8
|
+
export { UserAvatar } from './UserAvatar';
|
|
9
|
+
export { UserMenu } from './UserMenu';
|
|
10
|
+
export { GlobalSearch } from './GlobalSearch';
|
|
11
|
+
export { DataTable } from './DataTable';
|
|
12
|
+
export { DateTimePicker } from './DateTimePicker';
|
|
13
|
+
export { Chart } from './Chart';
|
|
14
|
+
export { Accordion } from './Accordion';
|
|
15
|
+
export { Modal } from './Modal';
|
|
16
|
+
export { Tooltip } from './Tooltip';
|
|
17
|
+
export { FileUpload } from './FileUpload';
|
|
18
|
+
|
|
19
|
+
// Moved from molecules - business UI components
|
|
20
|
+
export { Alert } from './Alert';
|
|
21
|
+
export { Toast, ToastContainer } from './Toast';
|
|
22
|
+
export { ProgressBar } from './ProgressBar';
|
|
23
|
+
export { FormField } from './FormField';
|
|
24
|
+
export { Breadcrumb } from './Breadcrumb';
|
|
25
|
+
|
|
26
|
+
// Re-export any remaining components
|
|
27
|
+
export { DarkModeToggle } from './DarkModeToggle';
|
|
28
|
+
export { PaletteSwitcher } from './PaletteSwitcher';
|
|
29
|
+
export { StyleGuide } from './StyleGuide';
|