@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.
Files changed (153) hide show
  1. package/README.md +6 -6
  2. package/package.json +3 -5
  3. package/src/App.css +0 -42
  4. package/src/App.tsx +0 -54
  5. package/src/__tests__/README.md +0 -221
  6. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
  7. package/src/__tests__/atoms/ui/button.test.tsx +0 -68
  8. package/src/__tests__/atoms/utils/simple.test.ts +0 -18
  9. package/src/__tests__/atoms/utils/utils.test.ts +0 -77
  10. package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
  11. package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
  12. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
  13. package/src/__tests__/setup.ts +0 -51
  14. package/src/__tests__/utils.tsx +0 -123
  15. package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
  16. package/src/atoms/composed/Accordion/index.ts +0 -1
  17. package/src/atoms/composed/Alert/Alert.tsx +0 -132
  18. package/src/atoms/composed/Alert/index.ts +0 -1
  19. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
  20. package/src/atoms/composed/Breadcrumb/index.ts +0 -1
  21. package/src/atoms/composed/Chart/Chart.tsx +0 -425
  22. package/src/atoms/composed/Chart/index.ts +0 -2
  23. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
  24. package/src/atoms/composed/ColorSwatch/index.ts +0 -1
  25. package/src/atoms/composed/DarkModeToggle.tsx +0 -66
  26. package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
  27. package/src/atoms/composed/DataBadge/index.ts +0 -1
  28. package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
  29. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
  30. package/src/atoms/composed/DataTable/index.ts +0 -2
  31. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
  32. package/src/atoms/composed/DateTimePicker/index.ts +0 -2
  33. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
  34. package/src/atoms/composed/DetailedCard/index.ts +0 -2
  35. package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
  36. package/src/atoms/composed/EmptyState/index.ts +0 -1
  37. package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
  38. package/src/atoms/composed/FileUpload/index.ts +0 -2
  39. package/src/atoms/composed/FormField/FormField.tsx +0 -92
  40. package/src/atoms/composed/FormField/index.ts +0 -1
  41. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
  42. package/src/atoms/composed/GlobalSearch/index.ts +0 -1
  43. package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
  44. package/src/atoms/composed/IconBadge/index.ts +0 -2
  45. package/src/atoms/composed/Modal/Modal.tsx +0 -223
  46. package/src/atoms/composed/Modal/index.ts +0 -2
  47. package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
  48. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
  49. package/src/atoms/composed/ProgressBar/index.ts +0 -1
  50. package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
  51. package/src/atoms/composed/StatCard/index.ts +0 -1
  52. package/src/atoms/composed/StyleGuide.tsx +0 -717
  53. package/src/atoms/composed/Toast/Toast.tsx +0 -219
  54. package/src/atoms/composed/Toast/index.ts +0 -1
  55. package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
  56. package/src/atoms/composed/Tooltip/index.ts +0 -1
  57. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
  58. package/src/atoms/composed/UserAvatar/index.ts +0 -1
  59. package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
  60. package/src/atoms/composed/UserMenu/index.ts +0 -1
  61. package/src/atoms/composed/index.ts +0 -29
  62. package/src/atoms/hooks/useApi.ts +0 -80
  63. package/src/atoms/hooks/useHealth.ts +0 -17
  64. package/src/atoms/index.ts +0 -13
  65. package/src/atoms/services/api/client.ts +0 -134
  66. package/src/atoms/services/auth-service.ts +0 -248
  67. package/src/atoms/services/health.ts +0 -15
  68. package/src/atoms/services/index.ts +0 -3
  69. package/src/atoms/shared/config/constants.ts +0 -17
  70. package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
  71. package/src/atoms/shared/config/environment.ts +0 -10
  72. package/src/atoms/shared/index.ts +0 -4
  73. package/src/atoms/shared/styles/color-palettes.css +0 -566
  74. package/src/atoms/types/auth.ts +0 -62
  75. package/src/atoms/types/generated.ts +0 -1469
  76. package/src/atoms/types/index.ts +0 -4
  77. package/src/atoms/types/loading.ts +0 -28
  78. package/src/atoms/ui/Badge.tsx +0 -30
  79. package/src/atoms/ui/ErrorBoundary.tsx +0 -59
  80. package/src/atoms/ui/Select.tsx +0 -53
  81. package/src/atoms/ui/Switch.tsx +0 -42
  82. package/src/atoms/ui/Tabs.tsx +0 -118
  83. package/src/atoms/ui/avatar.tsx +0 -48
  84. package/src/atoms/ui/button.tsx +0 -70
  85. package/src/atoms/ui/card.tsx +0 -76
  86. package/src/atoms/ui/dropdown-menu.tsx +0 -199
  87. package/src/atoms/ui/index.ts +0 -39
  88. package/src/atoms/ui/input.tsx +0 -23
  89. package/src/atoms/ui/label.tsx +0 -23
  90. package/src/atoms/ui/skeleton.tsx +0 -13
  91. package/src/atoms/ui/spinner.tsx +0 -49
  92. package/src/atoms/ui/table.tsx +0 -116
  93. package/src/atoms/utils/animations.ts +0 -135
  94. package/src/atoms/utils/tooltip-helpers.ts +0 -140
  95. package/src/atoms/utils/utils.ts +0 -9
  96. package/src/features/auth/components/LoginForm.tsx +0 -168
  97. package/src/features/auth/components/LogoutButton.tsx +0 -19
  98. package/src/features/auth/components/ProtectedRoute.tsx +0 -60
  99. package/src/features/auth/components/index.ts +0 -4
  100. package/src/features/auth/hooks/index.ts +0 -2
  101. package/src/features/auth/hooks/useAuth.tsx +0 -205
  102. package/src/features/auth/hooks/usePermissions.ts +0 -35
  103. package/src/features/auth/index.ts +0 -2
  104. package/src/features/index.ts +0 -2
  105. package/src/index.css +0 -704
  106. package/src/index.ts +0 -13
  107. package/src/main.tsx +0 -48
  108. package/src/molecules/.gitkeep +0 -0
  109. package/src/molecules/forms/FormGroup.tsx +0 -75
  110. package/src/molecules/forms/SearchInput.tsx +0 -259
  111. package/src/molecules/forms/index.ts +0 -4
  112. package/src/molecules/index.ts +0 -4
  113. package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
  114. package/src/molecules/layout/AppHeader/index.ts +0 -1
  115. package/src/molecules/layout/AppLayout.tsx +0 -29
  116. package/src/molecules/layout/PageTemplate.tsx +0 -87
  117. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
  118. package/src/molecules/layout/SectionHeader/index.ts +0 -1
  119. package/src/molecules/layout/ShowcaseSection.tsx +0 -57
  120. package/src/molecules/layout/Sidebar.tsx +0 -144
  121. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
  122. package/src/molecules/layout/SidebarButton/index.ts +0 -1
  123. package/src/molecules/layout/SidebarContext.tsx +0 -31
  124. package/src/molecules/layout/index.ts +0 -7
  125. package/src/molecules/navigation/NavMenu.tsx +0 -188
  126. package/src/molecules/navigation/Pagination.tsx +0 -172
  127. package/src/molecules/navigation/index.ts +0 -4
  128. package/src/organisms/index.ts +0 -5
  129. package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
  130. package/src/organisms/showcase/index.ts +0 -1
  131. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
  132. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
  133. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
  134. package/src/pages/AdminShowcase/index.tsx +0 -3
  135. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
  136. package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
  137. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
  138. package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
  139. package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
  140. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
  141. package/src/pages/ComponentShowcase/index.tsx +0 -188
  142. package/src/pages/index.ts +0 -2
  143. package/src/templates/AuthTemplate.tsx +0 -216
  144. package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
  145. package/src/templates/DashboardTemplate.tsx +0 -232
  146. package/src/templates/DataTemplate.tsx +0 -319
  147. package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
  148. package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
  149. package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
  150. package/src/templates/admin/index.ts +0 -29
  151. package/src/templates/factory.tsx +0 -169
  152. package/src/templates/index.ts +0 -37
  153. 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,2 +0,0 @@
1
- export { FileUpload } from './FileUpload';
2
- export type { FileUploadProps, FileUploadFile } from './FileUpload';
@@ -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';