@pattern-stack/frontend-patterns 0.0.1 → 0.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/README.md +6 -6
- package/package.json +3 -5
- package/src/App.css +0 -42
- package/src/App.tsx +0 -54
- package/src/__tests__/README.md +0 -221
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
- package/src/__tests__/atoms/ui/button.test.tsx +0 -68
- package/src/__tests__/atoms/utils/simple.test.ts +0 -18
- package/src/__tests__/atoms/utils/utils.test.ts +0 -77
- package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
- package/src/__tests__/setup.ts +0 -51
- package/src/__tests__/utils.tsx +0 -123
- package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
- package/src/atoms/composed/Accordion/index.ts +0 -1
- package/src/atoms/composed/Alert/Alert.tsx +0 -132
- package/src/atoms/composed/Alert/index.ts +0 -1
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
- package/src/atoms/composed/Breadcrumb/index.ts +0 -1
- package/src/atoms/composed/Chart/Chart.tsx +0 -425
- package/src/atoms/composed/Chart/index.ts +0 -2
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
- package/src/atoms/composed/ColorSwatch/index.ts +0 -1
- package/src/atoms/composed/DarkModeToggle.tsx +0 -66
- package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
- package/src/atoms/composed/DataBadge/index.ts +0 -1
- package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
- package/src/atoms/composed/DataTable/index.ts +0 -2
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
- package/src/atoms/composed/DateTimePicker/index.ts +0 -2
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
- package/src/atoms/composed/DetailedCard/index.ts +0 -2
- package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
- package/src/atoms/composed/EmptyState/index.ts +0 -1
- package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
- package/src/atoms/composed/FileUpload/index.ts +0 -2
- package/src/atoms/composed/FormField/FormField.tsx +0 -92
- package/src/atoms/composed/FormField/index.ts +0 -1
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
- package/src/atoms/composed/GlobalSearch/index.ts +0 -1
- package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
- package/src/atoms/composed/IconBadge/index.ts +0 -2
- package/src/atoms/composed/Modal/Modal.tsx +0 -223
- package/src/atoms/composed/Modal/index.ts +0 -2
- package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
- package/src/atoms/composed/ProgressBar/index.ts +0 -1
- package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
- package/src/atoms/composed/StatCard/index.ts +0 -1
- package/src/atoms/composed/StyleGuide.tsx +0 -717
- package/src/atoms/composed/Toast/Toast.tsx +0 -219
- package/src/atoms/composed/Toast/index.ts +0 -1
- package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
- package/src/atoms/composed/Tooltip/index.ts +0 -1
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
- package/src/atoms/composed/UserAvatar/index.ts +0 -1
- package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
- package/src/atoms/composed/UserMenu/index.ts +0 -1
- package/src/atoms/composed/index.ts +0 -29
- package/src/atoms/hooks/useApi.ts +0 -80
- package/src/atoms/hooks/useHealth.ts +0 -17
- package/src/atoms/index.ts +0 -13
- package/src/atoms/services/api/client.ts +0 -134
- package/src/atoms/services/auth-service.ts +0 -248
- package/src/atoms/services/health.ts +0 -15
- package/src/atoms/services/index.ts +0 -3
- package/src/atoms/shared/config/constants.ts +0 -17
- package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
- package/src/atoms/shared/config/environment.ts +0 -10
- package/src/atoms/shared/index.ts +0 -4
- package/src/atoms/shared/styles/color-palettes.css +0 -566
- package/src/atoms/types/auth.ts +0 -62
- package/src/atoms/types/generated.ts +0 -1469
- package/src/atoms/types/index.ts +0 -4
- package/src/atoms/types/loading.ts +0 -28
- package/src/atoms/ui/Badge.tsx +0 -30
- package/src/atoms/ui/ErrorBoundary.tsx +0 -59
- package/src/atoms/ui/Select.tsx +0 -53
- package/src/atoms/ui/Switch.tsx +0 -42
- package/src/atoms/ui/Tabs.tsx +0 -118
- package/src/atoms/ui/avatar.tsx +0 -48
- package/src/atoms/ui/button.tsx +0 -70
- package/src/atoms/ui/card.tsx +0 -76
- package/src/atoms/ui/dropdown-menu.tsx +0 -199
- package/src/atoms/ui/index.ts +0 -39
- package/src/atoms/ui/input.tsx +0 -23
- package/src/atoms/ui/label.tsx +0 -23
- package/src/atoms/ui/skeleton.tsx +0 -13
- package/src/atoms/ui/spinner.tsx +0 -49
- package/src/atoms/ui/table.tsx +0 -116
- package/src/atoms/utils/animations.ts +0 -135
- package/src/atoms/utils/tooltip-helpers.ts +0 -140
- package/src/atoms/utils/utils.ts +0 -9
- package/src/features/auth/components/LoginForm.tsx +0 -168
- package/src/features/auth/components/LogoutButton.tsx +0 -19
- package/src/features/auth/components/ProtectedRoute.tsx +0 -60
- package/src/features/auth/components/index.ts +0 -4
- package/src/features/auth/hooks/index.ts +0 -2
- package/src/features/auth/hooks/useAuth.tsx +0 -205
- package/src/features/auth/hooks/usePermissions.ts +0 -35
- package/src/features/auth/index.ts +0 -2
- package/src/features/index.ts +0 -2
- package/src/index.css +0 -704
- package/src/index.ts +0 -13
- package/src/main.tsx +0 -48
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +0 -75
- package/src/molecules/forms/SearchInput.tsx +0 -259
- package/src/molecules/forms/index.ts +0 -4
- package/src/molecules/index.ts +0 -4
- package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
- package/src/molecules/layout/AppHeader/index.ts +0 -1
- package/src/molecules/layout/AppLayout.tsx +0 -29
- package/src/molecules/layout/PageTemplate.tsx +0 -87
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
- package/src/molecules/layout/SectionHeader/index.ts +0 -1
- package/src/molecules/layout/ShowcaseSection.tsx +0 -57
- package/src/molecules/layout/Sidebar.tsx +0 -144
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
- package/src/molecules/layout/SidebarButton/index.ts +0 -1
- package/src/molecules/layout/SidebarContext.tsx +0 -31
- package/src/molecules/layout/index.ts +0 -7
- package/src/molecules/navigation/NavMenu.tsx +0 -188
- package/src/molecules/navigation/Pagination.tsx +0 -172
- package/src/molecules/navigation/index.ts +0 -4
- package/src/organisms/index.ts +0 -5
- package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
- package/src/organisms/showcase/index.ts +0 -1
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
- package/src/pages/AdminShowcase/index.tsx +0 -3
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
- package/src/pages/ComponentShowcase/index.tsx +0 -188
- package/src/pages/index.ts +0 -2
- package/src/templates/AuthTemplate.tsx +0 -216
- package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
- package/src/templates/DashboardTemplate.tsx +0 -232
- package/src/templates/DataTemplate.tsx +0 -319
- package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
- package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
- package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
- package/src/templates/admin/index.ts +0 -29
- package/src/templates/factory.tsx +0 -169
- package/src/templates/index.ts +0 -37
- package/src/vite-env.d.ts +0 -1
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback, useId } from 'react';
|
|
2
|
-
import { Upload, X, File, Image, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
|
3
|
-
import { cn } from '../../utils/utils';
|
|
4
|
-
import { Button } from '../../ui/button';
|
|
5
|
-
import { ProgressBar } from '../ProgressBar';
|
|
6
|
-
import { getAnimationClasses, animationPresets } from '../../utils/animations';
|
|
7
|
-
|
|
8
|
-
export interface FileUploadFile {
|
|
9
|
-
id: string;
|
|
10
|
-
file: File;
|
|
11
|
-
name: string;
|
|
12
|
-
size: number;
|
|
13
|
-
type: string;
|
|
14
|
-
progress?: number;
|
|
15
|
-
status: 'pending' | 'uploading' | 'success' | 'error';
|
|
16
|
-
error?: string;
|
|
17
|
-
preview?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FileUploadProps {
|
|
21
|
-
/** Accept specific file types */
|
|
22
|
-
accept?: string;
|
|
23
|
-
/** Allow multiple file selection */
|
|
24
|
-
multiple?: boolean;
|
|
25
|
-
/** Maximum file size in bytes */
|
|
26
|
-
maxSize?: number;
|
|
27
|
-
/** Maximum number of files */
|
|
28
|
-
maxFiles?: number;
|
|
29
|
-
/** Visual variant */
|
|
30
|
-
variant?: 'default' | 'compact' | 'large';
|
|
31
|
-
/** Disabled state */
|
|
32
|
-
disabled?: boolean;
|
|
33
|
-
/** Show file previews for images */
|
|
34
|
-
showPreview?: boolean;
|
|
35
|
-
/** Upload function */
|
|
36
|
-
onUpload?: (files: FileUploadFile[]) => Promise<void>;
|
|
37
|
-
/** File change handler */
|
|
38
|
-
onChange?: (files: FileUploadFile[]) => void;
|
|
39
|
-
/** File remove handler */
|
|
40
|
-
onRemove?: (fileId: string) => void;
|
|
41
|
-
/** Custom upload text */
|
|
42
|
-
uploadText?: string;
|
|
43
|
-
/** Custom drag text */
|
|
44
|
-
dragText?: string;
|
|
45
|
-
/** Additional CSS classes */
|
|
46
|
-
className?: string;
|
|
47
|
-
/** Error state */
|
|
48
|
-
error?: string;
|
|
49
|
-
/** Loading state */
|
|
50
|
-
loading?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const formatFileSize = (bytes: number): string => {
|
|
54
|
-
if (bytes === 0) return '0 Bytes';
|
|
55
|
-
const k = 1024;
|
|
56
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
57
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
58
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const isImageFile = (file: File): boolean => {
|
|
62
|
-
return file.type.startsWith('image/');
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const createFilePreview = (file: File): Promise<string> => {
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
-
if (!isImageFile(file)) {
|
|
68
|
-
resolve('');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const reader = new FileReader();
|
|
73
|
-
reader.onload = (e) => resolve(e.target?.result as string || '');
|
|
74
|
-
reader.readAsDataURL(file);
|
|
75
|
-
});
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const FileUpload: React.FC<FileUploadProps> = ({
|
|
79
|
-
accept,
|
|
80
|
-
multiple = false,
|
|
81
|
-
maxSize = 10 * 1024 * 1024, // 10MB default
|
|
82
|
-
maxFiles = multiple ? 10 : 1,
|
|
83
|
-
variant = 'default',
|
|
84
|
-
disabled = false,
|
|
85
|
-
showPreview = true,
|
|
86
|
-
onUpload,
|
|
87
|
-
onChange,
|
|
88
|
-
onRemove,
|
|
89
|
-
uploadText = 'Choose files or drag and drop',
|
|
90
|
-
dragText = 'Drop files here',
|
|
91
|
-
className,
|
|
92
|
-
error,
|
|
93
|
-
loading = false
|
|
94
|
-
}) => {
|
|
95
|
-
const [files, setFiles] = useState<FileUploadFile[]>([]);
|
|
96
|
-
const [isDragActive, setIsDragActive] = useState(false);
|
|
97
|
-
const [, setDragCounter] = useState(0);
|
|
98
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
99
|
-
const dropZoneId = useId();
|
|
100
|
-
|
|
101
|
-
const validateFile = (file: File): string | null => {
|
|
102
|
-
if (maxSize && file.size > maxSize) {
|
|
103
|
-
return `File size exceeds ${formatFileSize(maxSize)}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (accept) {
|
|
107
|
-
const acceptedTypes = accept.split(',').map(type => type.trim().toLowerCase());
|
|
108
|
-
const fileType = file.type.toLowerCase();
|
|
109
|
-
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
|
110
|
-
|
|
111
|
-
const isValidType = acceptedTypes.some(acceptedType => {
|
|
112
|
-
if (acceptedType.startsWith('.')) {
|
|
113
|
-
return fileExtension === acceptedType;
|
|
114
|
-
}
|
|
115
|
-
if (acceptedType.includes('/*')) {
|
|
116
|
-
const baseType = acceptedType.split('/')[0];
|
|
117
|
-
return fileType.startsWith(baseType + '/');
|
|
118
|
-
}
|
|
119
|
-
return fileType === acceptedType;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (!isValidType) {
|
|
123
|
-
return `File type not accepted. Accepted types: ${accept}`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return null;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const processFiles = useCallback(async (fileList: FileList) => {
|
|
131
|
-
const newFiles: FileUploadFile[] = [];
|
|
132
|
-
|
|
133
|
-
// Check max files limit
|
|
134
|
-
if (files.length + fileList.length > maxFiles) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
for (let i = 0; i < fileList.length; i++) {
|
|
139
|
-
const file = fileList[i];
|
|
140
|
-
const validationError = validateFile(file);
|
|
141
|
-
|
|
142
|
-
// Check for duplicates
|
|
143
|
-
const isDuplicate = files.some(existingFile =>
|
|
144
|
-
existingFile.name === file.name && existingFile.size === file.size
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
if (isDuplicate) continue;
|
|
148
|
-
|
|
149
|
-
const fileUpload: FileUploadFile = {
|
|
150
|
-
id: `${Date.now()}-${i}`,
|
|
151
|
-
file,
|
|
152
|
-
name: file.name,
|
|
153
|
-
size: file.size,
|
|
154
|
-
type: file.type,
|
|
155
|
-
status: validationError ? 'error' : 'pending',
|
|
156
|
-
error: validationError || undefined
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Create preview for images
|
|
160
|
-
if (showPreview && isImageFile(file) && !validationError) {
|
|
161
|
-
try {
|
|
162
|
-
fileUpload.preview = await createFilePreview(file);
|
|
163
|
-
} catch {
|
|
164
|
-
// Ignore preview errors
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
newFiles.push(fileUpload);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const updatedFiles = [...files, ...newFiles];
|
|
172
|
-
setFiles(updatedFiles);
|
|
173
|
-
onChange?.(updatedFiles);
|
|
174
|
-
}, [files, maxFiles, maxSize, accept, showPreview, onChange, validateFile]);
|
|
175
|
-
|
|
176
|
-
const removeFile = useCallback((fileId: string) => {
|
|
177
|
-
const updatedFiles = files.filter(file => file.id !== fileId);
|
|
178
|
-
setFiles(updatedFiles);
|
|
179
|
-
onRemove?.(fileId);
|
|
180
|
-
onChange?.(updatedFiles);
|
|
181
|
-
}, [files, onRemove, onChange]);
|
|
182
|
-
|
|
183
|
-
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
184
|
-
const fileList = e.target.files;
|
|
185
|
-
if (fileList && fileList.length > 0) {
|
|
186
|
-
processFiles(fileList);
|
|
187
|
-
}
|
|
188
|
-
// Reset input value to allow same file selection
|
|
189
|
-
if (fileInputRef.current) {
|
|
190
|
-
fileInputRef.current.value = '';
|
|
191
|
-
}
|
|
192
|
-
}, [processFiles]);
|
|
193
|
-
|
|
194
|
-
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
195
|
-
e.preventDefault();
|
|
196
|
-
e.stopPropagation();
|
|
197
|
-
setDragCounter(prev => prev + 1);
|
|
198
|
-
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
|
199
|
-
setIsDragActive(true);
|
|
200
|
-
}
|
|
201
|
-
}, []);
|
|
202
|
-
|
|
203
|
-
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
204
|
-
e.preventDefault();
|
|
205
|
-
e.stopPropagation();
|
|
206
|
-
setDragCounter(prev => {
|
|
207
|
-
const newCount = prev - 1;
|
|
208
|
-
if (newCount === 0) {
|
|
209
|
-
setIsDragActive(false);
|
|
210
|
-
}
|
|
211
|
-
return newCount;
|
|
212
|
-
});
|
|
213
|
-
}, []);
|
|
214
|
-
|
|
215
|
-
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
216
|
-
e.preventDefault();
|
|
217
|
-
e.stopPropagation();
|
|
218
|
-
}, []);
|
|
219
|
-
|
|
220
|
-
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
221
|
-
e.preventDefault();
|
|
222
|
-
e.stopPropagation();
|
|
223
|
-
setIsDragActive(false);
|
|
224
|
-
setDragCounter(0);
|
|
225
|
-
|
|
226
|
-
const droppedFiles = e.dataTransfer.files;
|
|
227
|
-
if (droppedFiles && droppedFiles.length > 0) {
|
|
228
|
-
processFiles(droppedFiles);
|
|
229
|
-
}
|
|
230
|
-
}, [processFiles]);
|
|
231
|
-
|
|
232
|
-
const handleUpload = async () => {
|
|
233
|
-
if (!onUpload) return;
|
|
234
|
-
|
|
235
|
-
const validFiles = files.filter(file => file.status !== 'error');
|
|
236
|
-
if (validFiles.length === 0) return;
|
|
237
|
-
|
|
238
|
-
// Set all valid files to uploading
|
|
239
|
-
const uploadingFiles = files.map(file =>
|
|
240
|
-
file.status === 'error' ? file : { ...file, status: 'uploading' as const, progress: 0 }
|
|
241
|
-
);
|
|
242
|
-
setFiles(uploadingFiles);
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
await onUpload(validFiles);
|
|
246
|
-
|
|
247
|
-
// Mark as success
|
|
248
|
-
const successFiles = files.map(file =>
|
|
249
|
-
file.status === 'error' ? file : { ...file, status: 'success' as const, progress: 100 }
|
|
250
|
-
);
|
|
251
|
-
setFiles(successFiles);
|
|
252
|
-
} catch (uploadError) {
|
|
253
|
-
// Mark as error
|
|
254
|
-
const errorFiles = files.map(file =>
|
|
255
|
-
file.status === 'uploading' ? {
|
|
256
|
-
...file,
|
|
257
|
-
status: 'error' as const,
|
|
258
|
-
error: uploadError instanceof Error ? uploadError.message : 'Upload failed'
|
|
259
|
-
} : file
|
|
260
|
-
);
|
|
261
|
-
setFiles(errorFiles);
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const variantClasses = {
|
|
266
|
-
default: 'p-8',
|
|
267
|
-
compact: 'p-4',
|
|
268
|
-
large: 'p-12'
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const iconSizes = {
|
|
272
|
-
default: 'w-8 h-8',
|
|
273
|
-
compact: 'w-6 h-6',
|
|
274
|
-
large: 'w-12 h-12'
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const textSizes = {
|
|
278
|
-
default: 'text-base',
|
|
279
|
-
compact: 'text-sm',
|
|
280
|
-
large: 'text-lg'
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const getFileIcon = (file: FileUploadFile) => {
|
|
284
|
-
if (isImageFile(file.file)) {
|
|
285
|
-
return <Image className="w-4 h-4" />;
|
|
286
|
-
}
|
|
287
|
-
return <File className="w-4 h-4" />;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const getStatusIcon = (file: FileUploadFile) => {
|
|
291
|
-
switch (file.status) {
|
|
292
|
-
case 'uploading':
|
|
293
|
-
return <Loader2 className="w-4 h-4 animate-spin text-category-1" />;
|
|
294
|
-
case 'success':
|
|
295
|
-
return <CheckCircle className="w-4 h-4 text-status-success" />;
|
|
296
|
-
case 'error':
|
|
297
|
-
return <AlertCircle className="w-4 h-4 text-status-error" />;
|
|
298
|
-
default:
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
return (
|
|
304
|
-
<div className={cn("space-y-4", className)} data-component-name="FileUpload">
|
|
305
|
-
{/* Drop zone */}
|
|
306
|
-
<div
|
|
307
|
-
className={cn(
|
|
308
|
-
'border-2 border-dashed rounded-lg transition-all duration-200 cursor-pointer',
|
|
309
|
-
'flex flex-col items-center justify-center text-center',
|
|
310
|
-
variantClasses[variant],
|
|
311
|
-
disabled ?
|
|
312
|
-
'border-border/50 bg-muted/30 cursor-not-allowed' :
|
|
313
|
-
isDragActive ?
|
|
314
|
-
'border-category-1 bg-category-1/5 scale-[1.02]' :
|
|
315
|
-
error ?
|
|
316
|
-
'border-status-error bg-status-error/5 hover:border-status-error/70' :
|
|
317
|
-
'border-border hover:border-category-1/50 hover:bg-muted/30',
|
|
318
|
-
getAnimationClasses(animationPresets.subtle)
|
|
319
|
-
)}
|
|
320
|
-
onDragEnter={!disabled ? handleDragEnter : undefined}
|
|
321
|
-
onDragLeave={!disabled ? handleDragLeave : undefined}
|
|
322
|
-
onDragOver={!disabled ? handleDragOver : undefined}
|
|
323
|
-
onDrop={!disabled ? handleDrop : undefined}
|
|
324
|
-
onClick={() => !disabled && fileInputRef.current?.click()}
|
|
325
|
-
role="button"
|
|
326
|
-
tabIndex={disabled ? -1 : 0}
|
|
327
|
-
aria-label="File upload area"
|
|
328
|
-
aria-describedby={`${dropZoneId}-description`}
|
|
329
|
-
onKeyDown={(e) => {
|
|
330
|
-
if (!disabled && (e.key === 'Enter' || e.key === ' ')) {
|
|
331
|
-
e.preventDefault();
|
|
332
|
-
fileInputRef.current?.click();
|
|
333
|
-
}
|
|
334
|
-
}}
|
|
335
|
-
data-component-name="FileUploadDropZone"
|
|
336
|
-
>
|
|
337
|
-
<Upload className={cn(
|
|
338
|
-
iconSizes[variant],
|
|
339
|
-
disabled ? 'text-muted-foreground' :
|
|
340
|
-
isDragActive ? 'text-category-1' :
|
|
341
|
-
error ? 'text-status-error' : 'text-muted-foreground'
|
|
342
|
-
)} />
|
|
343
|
-
|
|
344
|
-
<div className="mt-4 space-y-1">
|
|
345
|
-
<p className={cn(
|
|
346
|
-
textSizes[variant],
|
|
347
|
-
'font-medium',
|
|
348
|
-
disabled ? 'text-muted-foreground' :
|
|
349
|
-
isDragActive ? 'text-category-1' :
|
|
350
|
-
error ? 'text-status-error' : 'text-foreground'
|
|
351
|
-
)}>
|
|
352
|
-
{isDragActive ? dragText : uploadText}
|
|
353
|
-
</p>
|
|
354
|
-
|
|
355
|
-
<p className="text-xs text-muted-foreground" id={`${dropZoneId}-description`}>
|
|
356
|
-
{accept && `Accepted types: ${accept}`}
|
|
357
|
-
{maxSize && ` • Max size: ${formatFileSize(maxSize)}`}
|
|
358
|
-
{multiple && ` • Max files: ${maxFiles}`}
|
|
359
|
-
</p>
|
|
360
|
-
</div>
|
|
361
|
-
|
|
362
|
-
{error && (
|
|
363
|
-
<div className="mt-2 text-xs text-status-error font-medium">
|
|
364
|
-
{error}
|
|
365
|
-
</div>
|
|
366
|
-
)}
|
|
367
|
-
</div>
|
|
368
|
-
|
|
369
|
-
{/* Hidden file input */}
|
|
370
|
-
<input
|
|
371
|
-
ref={fileInputRef}
|
|
372
|
-
type="file"
|
|
373
|
-
accept={accept}
|
|
374
|
-
multiple={multiple}
|
|
375
|
-
onChange={handleFileSelect}
|
|
376
|
-
className="hidden"
|
|
377
|
-
disabled={disabled}
|
|
378
|
-
aria-label="File input"
|
|
379
|
-
/>
|
|
380
|
-
|
|
381
|
-
{/* File list */}
|
|
382
|
-
{files.length > 0 && (
|
|
383
|
-
<div className="space-y-2" data-component-name="FileUploadList">
|
|
384
|
-
{files.map((file) => (
|
|
385
|
-
<div
|
|
386
|
-
key={file.id}
|
|
387
|
-
className={cn(
|
|
388
|
-
'flex items-center gap-3 p-3 rounded-lg border bg-card',
|
|
389
|
-
file.status === 'error' && 'border-status-error/30 bg-status-error/5'
|
|
390
|
-
)}
|
|
391
|
-
data-component-name="FileUploadItem"
|
|
392
|
-
>
|
|
393
|
-
{/* File preview or icon */}
|
|
394
|
-
<div className="flex-shrink-0">
|
|
395
|
-
{file.preview ? (
|
|
396
|
-
<img
|
|
397
|
-
src={file.preview}
|
|
398
|
-
alt={file.name}
|
|
399
|
-
className="w-10 h-10 object-cover rounded border"
|
|
400
|
-
/>
|
|
401
|
-
) : (
|
|
402
|
-
<div className="w-10 h-10 flex items-center justify-center bg-muted rounded border">
|
|
403
|
-
{getFileIcon(file)}
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
406
|
-
</div>
|
|
407
|
-
|
|
408
|
-
{/* File info */}
|
|
409
|
-
<div className="flex-1 min-w-0">
|
|
410
|
-
<div className="flex items-center gap-2">
|
|
411
|
-
<p className="text-sm font-medium text-foreground truncate">
|
|
412
|
-
{file.name}
|
|
413
|
-
</p>
|
|
414
|
-
{getStatusIcon(file)}
|
|
415
|
-
</div>
|
|
416
|
-
|
|
417
|
-
<div className="flex items-center gap-2 mt-1">
|
|
418
|
-
<p className="text-xs text-muted-foreground">
|
|
419
|
-
{formatFileSize(file.size)}
|
|
420
|
-
</p>
|
|
421
|
-
|
|
422
|
-
{file.status === 'uploading' && file.progress !== undefined && (
|
|
423
|
-
<div className="flex-1 max-w-24">
|
|
424
|
-
<ProgressBar value={file.progress || 0} size="sm" />
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
|
|
429
|
-
{file.error && (
|
|
430
|
-
<p className="text-xs text-status-error mt-1">
|
|
431
|
-
{file.error}
|
|
432
|
-
</p>
|
|
433
|
-
)}
|
|
434
|
-
</div>
|
|
435
|
-
|
|
436
|
-
{/* Remove button */}
|
|
437
|
-
<Button
|
|
438
|
-
variant="ghost"
|
|
439
|
-
size="sm"
|
|
440
|
-
onClick={(e) => {
|
|
441
|
-
e.stopPropagation();
|
|
442
|
-
removeFile(file.id);
|
|
443
|
-
}}
|
|
444
|
-
className="flex-shrink-0 h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive"
|
|
445
|
-
aria-label={`Remove ${file.name}`}
|
|
446
|
-
>
|
|
447
|
-
<X className="w-4 h-4" />
|
|
448
|
-
</Button>
|
|
449
|
-
</div>
|
|
450
|
-
))}
|
|
451
|
-
</div>
|
|
452
|
-
)}
|
|
453
|
-
|
|
454
|
-
{/* Upload button */}
|
|
455
|
-
{files.length > 0 && onUpload && (
|
|
456
|
-
<div className="flex items-center gap-2">
|
|
457
|
-
<Button
|
|
458
|
-
onClick={handleUpload}
|
|
459
|
-
disabled={loading || files.every(f => f.status === 'error') || files.some(f => f.status === 'uploading')}
|
|
460
|
-
className="flex items-center gap-2"
|
|
461
|
-
>
|
|
462
|
-
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
463
|
-
Upload {files.filter(f => f.status !== 'error').length} file{files.filter(f => f.status !== 'error').length !== 1 ? 's' : ''}
|
|
464
|
-
</Button>
|
|
465
|
-
|
|
466
|
-
<Button
|
|
467
|
-
variant="outline"
|
|
468
|
-
onClick={() => setFiles([])}
|
|
469
|
-
disabled={loading || files.some(f => f.status === 'uploading')}
|
|
470
|
-
>
|
|
471
|
-
Clear all
|
|
472
|
-
</Button>
|
|
473
|
-
</div>
|
|
474
|
-
)}
|
|
475
|
-
</div>
|
|
476
|
-
);
|
|
477
|
-
};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { cn } from '../../utils/utils';
|
|
3
|
-
import { Label } from '../../ui/label';
|
|
4
|
-
|
|
5
|
-
export interface FormFieldProps {
|
|
6
|
-
/** Field label */
|
|
7
|
-
label?: string;
|
|
8
|
-
/** Field description/help text */
|
|
9
|
-
description?: string;
|
|
10
|
-
/** Error message */
|
|
11
|
-
error?: string;
|
|
12
|
-
/** Whether field is required */
|
|
13
|
-
required?: boolean;
|
|
14
|
-
/** Field content */
|
|
15
|
-
children: React.ReactNode;
|
|
16
|
-
/** Additional CSS classes */
|
|
17
|
-
className?: string;
|
|
18
|
-
/** HTML for attribute for label */
|
|
19
|
-
htmlFor?: string;
|
|
20
|
-
/** Field layout */
|
|
21
|
-
layout?: 'vertical' | 'horizontal';
|
|
22
|
-
/** Label width for horizontal layout */
|
|
23
|
-
labelWidth?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const FormField: React.FC<FormFieldProps> = ({
|
|
27
|
-
label,
|
|
28
|
-
description,
|
|
29
|
-
error,
|
|
30
|
-
required = false,
|
|
31
|
-
children,
|
|
32
|
-
className,
|
|
33
|
-
htmlFor,
|
|
34
|
-
layout = 'vertical',
|
|
35
|
-
labelWidth = '120px'
|
|
36
|
-
}) => {
|
|
37
|
-
const isHorizontal = layout === 'horizontal';
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div
|
|
41
|
-
className={cn(
|
|
42
|
-
'space-y-2',
|
|
43
|
-
isHorizontal && 'flex items-start gap-4',
|
|
44
|
-
className
|
|
45
|
-
)}
|
|
46
|
-
data-component-name="FormField"
|
|
47
|
-
>
|
|
48
|
-
{/* Label section */}
|
|
49
|
-
{label && (
|
|
50
|
-
<div
|
|
51
|
-
className={cn(
|
|
52
|
-
isHorizontal && 'flex-shrink-0',
|
|
53
|
-
isHorizontal && 'pt-2' // Align with input top padding
|
|
54
|
-
)}
|
|
55
|
-
style={isHorizontal ? { width: labelWidth } : undefined}
|
|
56
|
-
>
|
|
57
|
-
<Label
|
|
58
|
-
htmlFor={htmlFor}
|
|
59
|
-
className={cn(
|
|
60
|
-
'text-sm font-medium text-foreground',
|
|
61
|
-
required && "after:content-['*'] after:ml-0.5 after:text-status-error"
|
|
62
|
-
)}
|
|
63
|
-
>
|
|
64
|
-
{label}
|
|
65
|
-
</Label>
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
68
|
-
|
|
69
|
-
{/* Field content section */}
|
|
70
|
-
<div className={cn(isHorizontal && 'flex-1 min-w-0')}>
|
|
71
|
-
{/* Field input */}
|
|
72
|
-
<div className="relative">
|
|
73
|
-
{children}
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{/* Description */}
|
|
77
|
-
{description && !error && (
|
|
78
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
79
|
-
{description}
|
|
80
|
-
</p>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{/* Error message */}
|
|
84
|
-
{error && (
|
|
85
|
-
<p className="mt-1 text-xs text-status-error font-medium">
|
|
86
|
-
{error}
|
|
87
|
-
</p>
|
|
88
|
-
)}
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { FormField, type FormFieldProps } from './FormField';
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Search } from 'lucide-react';
|
|
3
|
-
import { Input } from '../../ui/input';
|
|
4
|
-
import { cn } from '../../utils/utils';
|
|
5
|
-
|
|
6
|
-
interface GlobalSearchProps {
|
|
7
|
-
className?: string;
|
|
8
|
-
placeholder?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const GlobalSearch: React.FC<GlobalSearchProps> = ({
|
|
12
|
-
className,
|
|
13
|
-
placeholder = "Search components, colors, patterns..."
|
|
14
|
-
}) => {
|
|
15
|
-
const [searchValue, setSearchValue] = useState('');
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className={cn("relative", className)}>
|
|
19
|
-
<div className="absolute inset-y-0 left-0 flex items-center pl-4">
|
|
20
|
-
<Search className="w-5 h-5 text-muted-foreground" />
|
|
21
|
-
</div>
|
|
22
|
-
<Input
|
|
23
|
-
type="text"
|
|
24
|
-
value={searchValue}
|
|
25
|
-
onChange={(e) => setSearchValue(e.target.value)}
|
|
26
|
-
placeholder={placeholder}
|
|
27
|
-
className={cn(
|
|
28
|
-
"pl-12 pr-4 h-12 text-base",
|
|
29
|
-
"bg-muted/30 border-border/40 rounded-xl",
|
|
30
|
-
"focus:bg-card focus:border-category-1/30 focus:ring-category-1/20",
|
|
31
|
-
"transition-all duration-200",
|
|
32
|
-
"placeholder:text-muted-foreground/70"
|
|
33
|
-
)}
|
|
34
|
-
/>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { GlobalSearch } from './GlobalSearch';
|