@shohojdhara/atomix 0.3.7 → 0.3.8

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 (53) hide show
  1. package/dist/atomix.css +77 -0
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +77 -0
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.d.ts +2 -2
  7. package/dist/core.js.map +1 -1
  8. package/dist/forms.js.map +1 -1
  9. package/dist/heavy.js.map +1 -1
  10. package/dist/index.d.ts +578 -515
  11. package/dist/index.esm.js +3157 -2626
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/index.js +10496 -9973
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.min.js +1 -1
  16. package/dist/index.min.js.map +1 -1
  17. package/dist/theme.d.ts +237 -420
  18. package/dist/theme.js +1629 -1701
  19. package/dist/theme.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/DataTable/DataTable.stories.tsx +238 -0
  22. package/src/components/DataTable/DataTable.test.tsx +450 -0
  23. package/src/components/DataTable/DataTable.tsx +384 -61
  24. package/src/components/DatePicker/DatePicker.tsx +29 -38
  25. package/src/components/Upload/Upload.tsx +539 -40
  26. package/src/lib/composables/useDataTable.ts +355 -15
  27. package/src/lib/composables/useDatePicker.ts +19 -0
  28. package/src/lib/constants/components.ts +10 -0
  29. package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
  30. package/src/lib/theme/adapters/index.ts +1 -4
  31. package/src/lib/theme/config/configLoader.ts +53 -35
  32. package/src/lib/theme/core/composeTheme.ts +22 -30
  33. package/src/lib/theme/core/createTheme.ts +49 -26
  34. package/src/lib/theme/core/index.ts +0 -1
  35. package/src/lib/theme/generators/generateCSSNested.ts +4 -3
  36. package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
  37. package/src/lib/theme/index.ts +10 -17
  38. package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
  39. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
  40. package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
  41. package/src/lib/theme/runtime/useTheme.ts +1 -1
  42. package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
  43. package/src/lib/theme/test/testTheme.ts +2 -1
  44. package/src/lib/theme/types.ts +14 -14
  45. package/src/lib/theme/utils/componentTheming.ts +35 -27
  46. package/src/lib/theme/utils/domUtils.ts +57 -15
  47. package/src/lib/theme/utils/injectCSS.ts +0 -1
  48. package/src/lib/theme/utils/themeHelpers.ts +1 -39
  49. package/src/lib/theme/utils/themeUtils.ts +1 -170
  50. package/src/lib/types/components.ts +145 -0
  51. package/src/lib/utils/dataTableExport.ts +143 -0
  52. package/src/styles/06-components/_components.data-table.scss +95 -0
  53. package/src/lib/hooks/useThemeTokens.ts +0 -105
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import { UPLOAD } from '../../lib/constants/components';
3
3
 
4
4
  export interface UploadProps {
@@ -52,6 +52,46 @@ export interface UploadProps {
52
52
  */
53
53
  icon?: React.ReactNode;
54
54
 
55
+ /**
56
+ * Upload endpoint URL. If not provided, upload will be simulated.
57
+ */
58
+ uploadEndpoint?: string;
59
+
60
+ /**
61
+ * HTTP method for upload request
62
+ */
63
+ uploadMethod?: 'POST' | 'PUT' | 'PATCH';
64
+
65
+ /**
66
+ * Additional headers to include in upload request
67
+ */
68
+ uploadHeaders?: Record<string, string>;
69
+
70
+ /**
71
+ * Additional form data fields to include in upload
72
+ */
73
+ uploadData?: Record<string, string>;
74
+
75
+ /**
76
+ * Chunk size in MB for chunked uploads (0 = no chunking)
77
+ */
78
+ chunkSizeInMB?: number;
79
+
80
+ /**
81
+ * Maximum number of retry attempts for failed uploads
82
+ */
83
+ maxRetries?: number;
84
+
85
+ /**
86
+ * Delay in milliseconds between retry attempts
87
+ */
88
+ retryDelay?: number;
89
+
90
+ /**
91
+ * Whether to automatically upload files after selection
92
+ */
93
+ autoUpload?: boolean;
94
+
55
95
  /**
56
96
  * Called when files are selected
57
97
  */
@@ -65,13 +105,18 @@ export interface UploadProps {
65
105
  /**
66
106
  * Called when file upload is complete
67
107
  */
68
- onFileUploadComplete?: (file: File) => void;
108
+ onFileUploadComplete?: (file: File, response?: any) => void;
69
109
 
70
110
  /**
71
111
  * Called on file upload errors
72
112
  */
73
113
  onFileUploadError?: (file: File, error: string) => void;
74
114
 
115
+ /**
116
+ * Called when upload is cancelled
117
+ */
118
+ onUploadCancel?: (file: File) => void;
119
+
75
120
  /**
76
121
  * Additional CSS class
77
122
  */
@@ -88,6 +133,18 @@ export interface UploadProps {
88
133
  */
89
134
  type UploadStatus = 'idle' | 'loading' | 'success' | 'error';
90
135
 
136
+ /**
137
+ * Upload request state
138
+ */
139
+ interface UploadRequest {
140
+ xhr: XMLHttpRequest | null;
141
+ abortController: AbortController;
142
+ file: File;
143
+ retryCount: number;
144
+ chunkXhrs?: XMLHttpRequest[];
145
+ intervalId?: NodeJS.Timeout;
146
+ }
147
+
91
148
  /**
92
149
  * Upload component for file uploads with drag and drop
93
150
  */
@@ -108,14 +165,25 @@ export const Upload: React.FC<UploadProps> = ({
108
165
  buttonText = 'Choose File',
109
166
  helperText = `Maximum size: ${maxSizeInMB}MB`,
110
167
  icon = <i className="icon-lux-cloud-arrow-up-fill"></i>,
168
+ uploadEndpoint,
169
+ uploadMethod = 'POST',
170
+ uploadHeaders = {},
171
+ uploadData = {},
172
+ chunkSizeInMB = 0,
173
+ maxRetries = 3,
174
+ retryDelay = 1000,
175
+ autoUpload = true,
111
176
  onFileSelect,
112
177
  onFileUpload,
113
178
  onFileUploadComplete,
114
179
  onFileUploadError,
180
+ onUploadCancel,
115
181
  className = '',
116
182
  style,
117
183
  }) => {
118
184
  const inputRef = useRef<HTMLInputElement>(null);
185
+ const uploadRequestRef = useRef<UploadRequest | null>(null);
186
+ const startTimeRef = useRef<number | null>(null);
119
187
 
120
188
  const [status, setStatus] = useState<UploadStatus>('idle');
121
189
  const [isDragging, setIsDragging] = useState(false);
@@ -206,9 +274,12 @@ export const Upload: React.FC<UploadProps> = ({
206
274
 
207
275
  // Process the first valid file
208
276
  if (validFiles.length) {
209
- setCurrentFile(validFiles[0] || null);
210
- if (validFiles[0]) {
211
- simulateUpload(validFiles[0]);
277
+ const fileToUpload = validFiles[0];
278
+ if (fileToUpload) {
279
+ setCurrentFile(fileToUpload);
280
+ if (autoUpload) {
281
+ uploadFile(fileToUpload);
282
+ }
212
283
  }
213
284
  }
214
285
  };
@@ -245,44 +316,433 @@ export const Upload: React.FC<UploadProps> = ({
245
316
  return true;
246
317
  };
247
318
 
248
- // Simulate upload (in a real component, this would be an actual upload)
249
- const simulateUpload = (file: File) => {
319
+ // Calculate time remaining based on upload speed
320
+ const calculateTimeRemaining = useCallback((progress: number, elapsedTime: number): string => {
321
+ if (progress <= 0 || elapsedTime <= 0) return '';
322
+
323
+ const estimatedTotalTime = (elapsedTime / progress) * 100;
324
+ const remainingTime = estimatedTotalTime - elapsedTime;
325
+
326
+ if (remainingTime <= 0) return 'Almost done...';
327
+
328
+ const seconds = Math.ceil(remainingTime / 1000);
329
+ if (seconds < 60) {
330
+ return `${seconds} second${seconds !== 1 ? 's' : ''} left`;
331
+ }
332
+
333
+ const minutes = Math.floor(seconds / 60);
334
+ const remainingSeconds = seconds % 60;
335
+ return `${minutes}m ${remainingSeconds}s left`;
336
+ }, []);
337
+
338
+ // Upload file chunk
339
+ const uploadChunk = useCallback((
340
+ file: File,
341
+ chunkIndex: number,
342
+ totalChunks: number,
343
+ chunkSize: number,
344
+ chunkXhrs: XMLHttpRequest[]
345
+ ): Promise<any> => {
346
+ return new Promise((resolve, reject) => {
347
+ const start = chunkIndex * chunkSize;
348
+ const end = Math.min(start + chunkSize, file.size);
349
+ const chunk = file.slice(start, end);
350
+
351
+ const formData = new FormData();
352
+ formData.append('file', chunk, file.name);
353
+ formData.append('chunkIndex', chunkIndex.toString());
354
+ formData.append('totalChunks', totalChunks.toString());
355
+ formData.append('fileName', file.name);
356
+ formData.append('fileSize', file.size.toString());
357
+
358
+ // Add additional form data
359
+ Object.entries(uploadData).forEach(([key, value]) => {
360
+ formData.append(key, value);
361
+ });
362
+
363
+ const xhr = new XMLHttpRequest();
364
+
365
+ // Store XHR for cancellation
366
+ chunkXhrs[chunkIndex] = xhr;
367
+
368
+ // Set up progress tracking for this chunk
369
+ xhr.upload.addEventListener('progress', (e) => {
370
+ if (e.lengthComputable && uploadRequestRef.current) {
371
+ const chunkProgress = (e.loaded / e.total) * 100;
372
+ const overallProgress = ((chunkIndex * chunkSize + e.loaded) / file.size) * 100;
373
+
374
+ setUploadProgress(Math.min(overallProgress, 100));
375
+
376
+ const elapsedTime = startTimeRef.current ? Date.now() - startTimeRef.current : 0;
377
+ const timeRemaining = calculateTimeRemaining(overallProgress, elapsedTime);
378
+ setTimeLeft(timeRemaining);
379
+
380
+ if (onFileUpload) {
381
+ onFileUpload(file, overallProgress);
382
+ }
383
+ }
384
+ });
385
+
386
+ xhr.addEventListener('load', () => {
387
+ if (xhr.status >= 200 && xhr.status < 300) {
388
+ try {
389
+ const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
390
+ resolve(response);
391
+ } catch {
392
+ resolve({});
393
+ }
394
+ } else {
395
+ reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`));
396
+ }
397
+ });
398
+
399
+ xhr.addEventListener('error', () => {
400
+ reject(new Error('Network error occurred during upload'));
401
+ });
402
+
403
+ xhr.addEventListener('abort', () => {
404
+ reject(new Error('Upload was cancelled'));
405
+ });
406
+
407
+ xhr.open(uploadMethod, uploadEndpoint!);
408
+
409
+ // Set headers
410
+ Object.entries(uploadHeaders).forEach(([key, value]) => {
411
+ xhr.setRequestHeader(key, value);
412
+ });
413
+
414
+ xhr.send(formData);
415
+ });
416
+ }, [uploadEndpoint, uploadMethod, uploadHeaders, uploadData, onFileUpload, calculateTimeRemaining]);
417
+
418
+ // Upload file (with chunking support)
419
+ const uploadFile = useCallback(async (file: File, retryCount: number = 0) => {
420
+ // If no endpoint is provided, simulate upload for backward compatibility
421
+ if (!uploadEndpoint) {
422
+ setStatus('loading');
423
+ setUploadProgress(0);
424
+ startTimeRef.current = Date.now();
425
+
426
+ // Simulate progress updates
427
+ let progress = 0;
428
+ const interval = setInterval(() => {
429
+ progress += 5;
430
+
431
+ if (progress < 100) {
432
+ setUploadProgress(progress);
433
+ const elapsedTime = Date.now() - (startTimeRef.current || Date.now());
434
+ const timeRemaining = calculateTimeRemaining(progress, elapsedTime);
435
+ setTimeLeft(timeRemaining);
436
+
437
+ if (onFileUpload) {
438
+ onFileUpload(file, progress);
439
+ }
440
+ } else {
441
+ clearInterval(interval);
442
+ setStatus('success');
443
+ setSuccessMessage('Upload successful');
444
+ startTimeRef.current = null;
445
+
446
+ if (onFileUploadComplete) {
447
+ onFileUploadComplete(file);
448
+ }
449
+
450
+ uploadRequestRef.current = null;
451
+ }
452
+ }, 500);
453
+
454
+ // Store interval for cleanup
455
+ uploadRequestRef.current = {
456
+ xhr: null,
457
+ abortController: new AbortController(),
458
+ file,
459
+ retryCount,
460
+ intervalId: interval,
461
+ };
462
+
463
+ return;
464
+ }
465
+
250
466
  setStatus('loading');
251
467
  setUploadProgress(0);
468
+ setErrorMessage(null);
469
+ setSuccessMessage(null);
470
+ startTimeRef.current = Date.now();
471
+
472
+ try {
473
+ const chunkSize = chunkSizeInMB > 0 ? chunkSizeInMB * 1024 * 1024 : file.size;
474
+ const totalChunks = chunkSizeInMB > 0 ? Math.ceil(file.size / chunkSize) : 1;
475
+
476
+ if (totalChunks > 1) {
477
+ // Chunked upload
478
+ const chunkXhrs: XMLHttpRequest[] = [];
479
+
480
+ // Initialize upload request with chunk array
481
+ uploadRequestRef.current = {
482
+ xhr: null,
483
+ abortController: new AbortController(),
484
+ file,
485
+ retryCount,
486
+ chunkXhrs,
487
+ };
488
+
489
+ try {
490
+ const chunkResults = [];
491
+ for (let i = 0; i < totalChunks; i++) {
492
+ // Check if upload was cancelled
493
+ if (!uploadRequestRef.current) {
494
+ throw new Error('Upload was cancelled');
495
+ }
496
+
497
+ const result = await uploadChunk(file, i, totalChunks, chunkSize, chunkXhrs);
498
+ chunkResults.push(result);
499
+ }
252
500
 
253
- // Simulate progress updates
254
- let progress = 0;
255
- const interval = setInterval(() => {
256
- progress += 5;
501
+ setStatus('success');
502
+ setSuccessMessage('Upload successful');
503
+ setUploadProgress(100);
504
+ setTimeLeft(null);
505
+ startTimeRef.current = null;
257
506
 
258
- if (progress < 100) {
259
- setUploadProgress(progress);
260
- setTimeLeft(`${Math.ceil((100 - progress) / 5)} seconds left`);
507
+ if (onFileUploadComplete) {
508
+ onFileUploadComplete(file, chunkResults);
509
+ }
261
510
 
262
- if (onFileUpload) {
263
- onFileUpload(file, progress);
511
+ uploadRequestRef.current = null;
512
+ } catch (error) {
513
+ // Abort all remaining chunks
514
+ chunkXhrs.forEach(xhr => {
515
+ if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
516
+ xhr.abort();
517
+ }
518
+ });
519
+ throw error;
264
520
  }
265
521
  } else {
266
- clearInterval(interval);
267
- setStatus('success');
268
- setSuccessMessage('Upload successful');
269
-
270
- if (onFileUploadComplete) {
271
- onFileUploadComplete(file);
522
+ // Single upload - wrap in Promise for proper error handling
523
+ await new Promise<void>((resolve, reject) => {
524
+ const formData = new FormData();
525
+ formData.append('file', file);
526
+
527
+ // Add additional form data
528
+ Object.entries(uploadData).forEach(([key, value]) => {
529
+ formData.append(key, value);
530
+ });
531
+
532
+ const xhr = new XMLHttpRequest();
533
+ const abortController = new AbortController();
534
+
535
+ // Set up progress tracking
536
+ xhr.upload.addEventListener('progress', (e) => {
537
+ if (e.lengthComputable) {
538
+ const progress = (e.loaded / e.total) * 100;
539
+ setUploadProgress(progress);
540
+
541
+ const elapsedTime = startTimeRef.current ? Date.now() - startTimeRef.current : 0;
542
+ const timeRemaining = calculateTimeRemaining(progress, elapsedTime);
543
+ setTimeLeft(timeRemaining);
544
+
545
+ if (onFileUpload) {
546
+ onFileUpload(file, progress);
547
+ }
548
+ }
549
+ });
550
+
551
+ xhr.addEventListener('load', () => {
552
+ if (xhr.status >= 200 && xhr.status < 300) {
553
+ setStatus('success');
554
+ setSuccessMessage('Upload successful');
555
+ setUploadProgress(100);
556
+ setTimeLeft(null);
557
+ startTimeRef.current = null;
558
+
559
+ try {
560
+ const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
561
+ if (onFileUploadComplete) {
562
+ onFileUploadComplete(file, response);
563
+ }
564
+ } catch {
565
+ if (onFileUploadComplete) {
566
+ onFileUploadComplete(file);
567
+ }
568
+ }
569
+
570
+ uploadRequestRef.current = null;
571
+ resolve();
572
+ } else {
573
+ reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`));
574
+ }
575
+ });
576
+
577
+ xhr.addEventListener('error', () => {
578
+ reject(new Error('Network error occurred during upload'));
579
+ });
580
+
581
+ xhr.addEventListener('abort', () => {
582
+ setStatus('idle');
583
+ setUploadProgress(0);
584
+ setTimeLeft(null);
585
+ startTimeRef.current = null;
586
+ uploadRequestRef.current = null;
587
+
588
+ if (onUploadCancel) {
589
+ onUploadCancel(file);
590
+ }
591
+
592
+ reject(new Error('Upload was cancelled'));
593
+ });
594
+
595
+ xhr.open(uploadMethod, uploadEndpoint);
596
+
597
+ // Set headers
598
+ Object.entries(uploadHeaders).forEach(([key, value]) => {
599
+ xhr.setRequestHeader(key, value);
600
+ });
601
+
602
+ // Store request for cancellation
603
+ uploadRequestRef.current = {
604
+ xhr,
605
+ abortController,
606
+ file,
607
+ retryCount,
608
+ };
609
+
610
+ xhr.send(formData);
611
+ });
612
+ }
613
+ } catch (error) {
614
+ const errorMessage = error instanceof Error ? error.message : 'Upload failed';
615
+
616
+ // Retry logic
617
+ if (retryCount < maxRetries && !errorMessage.includes('cancelled')) {
618
+ setStatus('loading');
619
+ setErrorMessage(`Upload failed. Retrying... (${retryCount + 1}/${maxRetries})`);
620
+
621
+ setTimeout(() => {
622
+ uploadFile(file, retryCount + 1);
623
+ }, retryDelay);
624
+ } else {
625
+ setStatus('error');
626
+ setErrorMessage(errorMessage);
627
+ setUploadProgress(0);
628
+ setTimeLeft(null);
629
+ startTimeRef.current = null;
630
+ uploadRequestRef.current = null;
631
+
632
+ if (onFileUploadError) {
633
+ onFileUploadError(file, errorMessage);
272
634
  }
273
635
  }
274
- }, 500);
275
- };
636
+ }
637
+ }, [
638
+ uploadEndpoint,
639
+ uploadMethod,
640
+ uploadHeaders,
641
+ uploadData,
642
+ chunkSizeInMB,
643
+ maxRetries,
644
+ retryDelay,
645
+ onFileUpload,
646
+ onFileUploadComplete,
647
+ onFileUploadError,
648
+ onUploadCancel,
649
+ uploadChunk,
650
+ calculateTimeRemaining,
651
+ ]);
276
652
 
277
653
  // Reset upload
278
- const resetUpload = () => {
654
+ const resetUpload = useCallback(() => {
655
+ // Cancel any ongoing upload
656
+ if (uploadRequestRef.current) {
657
+ // Cancel single XHR request
658
+ if (uploadRequestRef.current.xhr) {
659
+ uploadRequestRef.current.xhr.abort();
660
+ }
661
+
662
+ // Cancel all chunk XHR requests
663
+ if (uploadRequestRef.current.chunkXhrs) {
664
+ uploadRequestRef.current.chunkXhrs.forEach(xhr => {
665
+ if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
666
+ xhr.abort();
667
+ }
668
+ });
669
+ }
670
+
671
+ // Clear interval if it exists
672
+ if (uploadRequestRef.current.intervalId) {
673
+ clearInterval(uploadRequestRef.current.intervalId);
674
+ }
675
+
676
+ uploadRequestRef.current = null;
677
+ }
678
+
279
679
  setStatus('idle');
280
680
  setCurrentFile(null);
281
681
  setUploadProgress(0);
282
682
  setTimeLeft(null);
283
683
  setErrorMessage(null);
284
684
  setSuccessMessage(null);
285
- };
685
+ startTimeRef.current = null;
686
+ }, []);
687
+
688
+ // Cancel upload
689
+ const cancelUpload = useCallback(() => {
690
+ if (uploadRequestRef.current) {
691
+ // Cancel single XHR request
692
+ if (uploadRequestRef.current.xhr) {
693
+ uploadRequestRef.current.xhr.abort();
694
+ }
695
+
696
+ // Cancel all chunk XHR requests
697
+ if (uploadRequestRef.current.chunkXhrs) {
698
+ uploadRequestRef.current.chunkXhrs.forEach(xhr => {
699
+ if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
700
+ xhr.abort();
701
+ }
702
+ });
703
+ }
704
+
705
+ // Clear interval if it exists (for simulated uploads)
706
+ if (uploadRequestRef.current.intervalId) {
707
+ clearInterval(uploadRequestRef.current.intervalId);
708
+ }
709
+
710
+ uploadRequestRef.current.abortController.abort();
711
+
712
+ if (onUploadCancel && uploadRequestRef.current.file) {
713
+ onUploadCancel(uploadRequestRef.current.file);
714
+ }
715
+ }
716
+
717
+ uploadRequestRef.current = null;
718
+ resetUpload();
719
+ }, [onUploadCancel, resetUpload]);
720
+
721
+ // Cleanup on unmount
722
+ useEffect(() => {
723
+ return () => {
724
+ if (uploadRequestRef.current) {
725
+ // Cancel single XHR request
726
+ if (uploadRequestRef.current.xhr) {
727
+ uploadRequestRef.current.xhr.abort();
728
+ }
729
+
730
+ // Cancel all chunk XHR requests
731
+ if (uploadRequestRef.current.chunkXhrs) {
732
+ uploadRequestRef.current.chunkXhrs.forEach(xhr => {
733
+ if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
734
+ xhr.abort();
735
+ }
736
+ });
737
+ }
738
+
739
+ // Clear interval if it exists
740
+ if (uploadRequestRef.current.intervalId) {
741
+ clearInterval(uploadRequestRef.current.intervalId);
742
+ }
743
+ }
744
+ };
745
+ }, []);
286
746
 
287
747
  // Build CSS classes
288
748
  const uploadClasses = [
@@ -367,20 +827,59 @@ export const Upload: React.FC<UploadProps> = ({
367
827
 
368
828
  {(status === 'loading' || status === 'error' || status === 'success') && (
369
829
  <div className="c-upload__loader-control">
370
- <div className="c-upload__loader-bar">
371
- <svg>
372
- <circle cx="10" cy="10" r="10"></circle>
373
- <circle cx="10" cy="10" r="10"></circle>
374
- </svg>
375
- </div>
376
- <button
377
- type="button"
378
- className="c-upload__loader-close"
379
- onClick={resetUpload}
380
- aria-label="Close upload progress"
381
- >
382
- <i className="icon-lux-x"></i>
383
- </button>
830
+ {status === 'loading' && (
831
+ <div className="c-upload__loader-bar">
832
+ <svg>
833
+ <circle cx="10" cy="10" r="10"></circle>
834
+ <circle cx="10" cy="10" r="10"></circle>
835
+ </svg>
836
+ </div>
837
+ )}
838
+ {status === 'loading' && (
839
+ <button
840
+ type="button"
841
+ className="c-upload__loader-cancel"
842
+ onClick={cancelUpload}
843
+ aria-label="Cancel upload"
844
+ >
845
+ <i className="icon-lux-x"></i>
846
+ </button>
847
+ )}
848
+ {(status === 'error' || status === 'success') && (
849
+ <button
850
+ type="button"
851
+ className="c-upload__loader-close"
852
+ onClick={resetUpload}
853
+ aria-label="Close upload progress"
854
+ >
855
+ <i className="icon-lux-x"></i>
856
+ </button>
857
+ )}
858
+ </div>
859
+ )}
860
+
861
+ {errorMessage && status === 'error' && (
862
+ <div className="c-upload__error-message">
863
+ {errorMessage}
864
+ {uploadRequestRef.current && uploadRequestRef.current.retryCount < maxRetries && (
865
+ <button
866
+ type="button"
867
+ className="c-upload__retry-btn"
868
+ onClick={() => {
869
+ if (currentFile) {
870
+ uploadFile(currentFile, uploadRequestRef.current?.retryCount || 0);
871
+ }
872
+ }}
873
+ >
874
+ Retry
875
+ </button>
876
+ )}
877
+ </div>
878
+ )}
879
+
880
+ {successMessage && status === 'success' && (
881
+ <div className="c-upload__success-message">
882
+ {successMessage}
384
883
  </div>
385
884
  )}
386
885
  </div>