angular-matecu 4.0.7 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +347 -130
  2. package/ng-package.json +7 -0
  3. package/package.json +6 -21
  4. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.html +11 -0
  5. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.scss +39 -0
  6. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.spec.ts +25 -0
  7. package/src/lib/components/matecu-alert-box/matecu-alert-box.component.ts +60 -0
  8. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.html +24 -0
  9. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.scss +5 -0
  10. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.spec.ts +25 -0
  11. package/src/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.ts +53 -0
  12. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.html +10 -0
  13. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.scss +34 -0
  14. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.spec.ts +25 -0
  15. package/src/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.ts +45 -0
  16. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.html +36 -0
  17. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.scss +56 -0
  18. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.spec.ts +23 -0
  19. package/src/lib/components/matecu-autocomplete/matecu-autocomplete.ts +336 -0
  20. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.html +64 -0
  21. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.scss +23 -0
  22. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.spec.ts +23 -0
  23. package/src/lib/components/matecu-autocomplete-multiple/matecu-autocomplete-multiple.ts +314 -0
  24. package/src/lib/components/matecu-file-input/matecu-file-input-customization.md +284 -0
  25. package/src/lib/components/matecu-file-input/matecu-file-input.example.md +228 -0
  26. package/src/lib/components/matecu-file-input/matecu-file-input.html +128 -0
  27. package/src/lib/components/matecu-file-input/matecu-file-input.scss +461 -0
  28. package/src/lib/components/matecu-file-input/matecu-file-input.spec.ts +340 -0
  29. package/src/lib/components/matecu-file-input/matecu-file-input.ts +438 -0
  30. package/src/lib/components/matecu-spinner/matecu-spinner.component.css +15 -0
  31. package/src/lib/components/matecu-spinner/matecu-spinner.component.html +44 -0
  32. package/src/lib/components/matecu-spinner/matecu-spinner.component.spec.ts +25 -0
  33. package/src/lib/components/matecu-spinner/matecu-spinner.component.ts +54 -0
  34. package/src/lib/components/matecu-spinner/spinner-loader.component.scss +13 -0
  35. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.html +1 -0
  36. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.scss +19 -0
  37. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.spec.ts +25 -0
  38. package/src/lib/components/matecu-topbar-action/matecu-topbar-action.component.ts +14 -0
  39. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.html +1 -0
  40. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.scss +14 -0
  41. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.spec.ts +25 -0
  42. package/src/lib/components/matecu-topbar-body/matecu-topbar-body.component.ts +11 -0
  43. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.html +3 -0
  44. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.scss +19 -0
  45. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.spec.ts +25 -0
  46. package/src/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.ts +31 -0
  47. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.html +1 -0
  48. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.scss +8 -0
  49. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.spec.ts +23 -0
  50. package/src/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.ts +11 -0
  51. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.html +9 -0
  52. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.scss +34 -0
  53. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.spec.ts +23 -0
  54. package/src/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.ts +18 -0
  55. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.html +7 -0
  56. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.scss +49 -0
  57. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.spec.ts +25 -0
  58. package/src/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.ts +112 -0
  59. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.html +20 -0
  60. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.scss +90 -0
  61. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.spec.ts +25 -0
  62. package/src/lib/components/matecu-topbar-search/matecu-topbar-search.component.ts +92 -0
  63. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.html +1 -0
  64. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.scss +91 -0
  65. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.spec.ts +25 -0
  66. package/src/lib/components/matecu-topbar-title/matecu-topbar-title.component.ts +14 -0
  67. package/src/lib/modules/matecu-alert-box/matecu-alert-box.module.ts +16 -0
  68. package/src/lib/modules/matecu-spinner/matecu-spinner.module.ts +14 -0
  69. package/src/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.ts +45 -0
  70. package/src/lib/services/matecu-snack-bar.service.spec.ts +16 -0
  71. package/src/lib/services/matecu-snack-bar.service.ts +66 -0
  72. package/src/lib/services/matecu-spinner.service.spec.ts +16 -0
  73. package/src/lib/services/matecu-spinner.service.ts +39 -0
  74. package/src/lib/types/matecu-alert-dialog.ts +10 -0
  75. package/{lib/types/matecu-alert-snackbar.d.ts → src/lib/types/matecu-alert-snackbar.ts} +5 -4
  76. package/src/lib/types/matecu-altert-box-type.ts +6 -0
  77. package/src/lib/types/matecu-autocomplete.ts +5 -0
  78. package/{public-api.d.ts → src/public-api.ts} +14 -0
  79. package/tsconfig.lib.json +17 -0
  80. package/tsconfig.lib.prod.json +11 -0
  81. package/tsconfig.spec.json +15 -0
  82. package/CHANGELOG.md +0 -22
  83. package/esm2022/angular-matecu.mjs +0 -5
  84. package/esm2022/lib/components/matecu-alert-box/matecu-alert-box.component.mjs +0 -67
  85. package/esm2022/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.mjs +0 -54
  86. package/esm2022/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.mjs +0 -43
  87. package/esm2022/lib/components/matecu-spinner/matecu-spinner.component.mjs +0 -58
  88. package/esm2022/lib/components/matecu-topbar-action/matecu-topbar-action.component.mjs +0 -18
  89. package/esm2022/lib/components/matecu-topbar-body/matecu-topbar-body.component.mjs +0 -17
  90. package/esm2022/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.mjs +0 -43
  91. package/esm2022/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.mjs +0 -12
  92. package/esm2022/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.mjs +0 -29
  93. package/esm2022/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.mjs +0 -112
  94. package/esm2022/lib/components/matecu-topbar-search/matecu-topbar-search.component.mjs +0 -93
  95. package/esm2022/lib/components/matecu-topbar-title/matecu-topbar-title.component.mjs +0 -18
  96. package/esm2022/lib/modules/matecu-alert-box/matecu-alert-box.module.mjs +0 -24
  97. package/esm2022/lib/modules/matecu-spinner/matecu-spinner.module.mjs +0 -22
  98. package/esm2022/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.mjs +0 -83
  99. package/esm2022/lib/services/matecu-snack-bar.service.mjs +0 -66
  100. package/esm2022/lib/services/matecu-spinner.service.mjs +0 -44
  101. package/esm2022/lib/types/matecu-alert-dialog.mjs +0 -2
  102. package/esm2022/lib/types/matecu-alert-snackbar.mjs +0 -2
  103. package/esm2022/lib/types/matecu-altert-box-type.mjs +0 -8
  104. package/esm2022/public-api.mjs +0 -32
  105. package/fesm2022/angular-matecu.mjs +0 -735
  106. package/fesm2022/angular-matecu.mjs.map +0 -1
  107. package/index.d.ts +0 -5
  108. package/lib/components/matecu-alert-box/matecu-alert-box.component.d.ts +0 -19
  109. package/lib/components/matecu-alert-dialog/matecu-alert-dialog.component.d.ts +0 -22
  110. package/lib/components/matecu-alert-snack-bar/matecu-alert-snack-bar.component.d.ts +0 -20
  111. package/lib/components/matecu-spinner/matecu-spinner.component.d.ts +0 -20
  112. package/lib/components/matecu-topbar-action/matecu-topbar-action.component.d.ts +0 -9
  113. package/lib/components/matecu-topbar-body/matecu-topbar-body.component.d.ts +0 -6
  114. package/lib/components/matecu-topbar-fab/matecu-topbar-fab.component.d.ts +0 -12
  115. package/lib/components/matecu-topbar-header-column/matecu-topbar-header-column.component.d.ts +0 -5
  116. package/lib/components/matecu-topbar-header-row/matecu-topbar-header-row.component.d.ts +0 -8
  117. package/lib/components/matecu-topbar-layout/matecu-topbar-layout.component.d.ts +0 -27
  118. package/lib/components/matecu-topbar-search/matecu-topbar-search.component.d.ts +0 -27
  119. package/lib/components/matecu-topbar-title/matecu-topbar-title.component.d.ts +0 -9
  120. package/lib/modules/matecu-alert-box/matecu-alert-box.module.d.ts +0 -14
  121. package/lib/modules/matecu-spinner/matecu-spinner.module.d.ts +0 -8
  122. package/lib/modules/matecu-topbar-layout/matecu-topbar-layout.module.d.ts +0 -19
  123. package/lib/services/matecu-snack-bar.service.d.ts +0 -17
  124. package/lib/services/matecu-spinner.service.d.ts +0 -15
  125. package/lib/types/matecu-alert-dialog.d.ts +0 -9
  126. package/lib/types/matecu-altert-box-type.d.ts +0 -6
@@ -0,0 +1,438 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { CommonModule } from '@angular/common';
3
+ import {
4
+ Component,
5
+ EventEmitter,
6
+ Input,
7
+ Output,
8
+ OnDestroy,
9
+ ElementRef,
10
+ ViewChild,
11
+ } from '@angular/core';
12
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
13
+
14
+ interface FileValidationResult {
15
+ isValid: boolean;
16
+ errors: string[];
17
+ }
18
+
19
+ interface ErrorMessages {
20
+ invalidSize: string;
21
+ invalidType: string;
22
+ tooManyFiles: string;
23
+ uploadError: string;
24
+ }
25
+
26
+ type FileSizeUnit = 'AUTO' | 'bytes' | 'KB' | 'MB' | 'GB' | 'TB';
27
+
28
+ export type { FileSizeUnit };
29
+
30
+ export enum FileInputState {
31
+ IDLE = 'idle',
32
+ LOADING = 'loading',
33
+ SUCCESS = 'success',
34
+ ERROR = 'error',
35
+ }
36
+
37
+ @Component({
38
+ selector: 'matecu-file-input',
39
+ imports: [CommonModule],
40
+ templateUrl: './matecu-file-input.html',
41
+ styleUrls: ['./matecu-file-input.scss'],
42
+ providers: [
43
+ {
44
+ provide: NG_VALUE_ACCESSOR,
45
+ multi: true,
46
+ useExisting: MatecuFileInput,
47
+ },
48
+ ],
49
+ })
50
+ export class MatecuFileInput implements ControlValueAccessor, OnDestroy {
51
+ @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
52
+
53
+ // State
54
+ files: File[] = [];
55
+ file?: File;
56
+ isDisabled = false;
57
+ fileName?: string;
58
+ selectedFileSize?: number;
59
+ hasInvalidSize = false;
60
+ hasInvalidType = false;
61
+ previewUrl?: string;
62
+ previewUrls = new Map<string, string>(); // Para múltiples archivos
63
+ currentState: FileInputState = FileInputState.IDLE;
64
+ validationErrors: string[] = [];
65
+ isDragOver = false;
66
+
67
+ private onChange: ((value: any) => void) | undefined;
68
+ private onTouched: (() => void) | undefined;
69
+ // Inputs - Funcionalidad básica
70
+ @Input() optimizeImage?: (img: File) => Promise<File>;
71
+ @Input() optimizeImageToSize?: number;
72
+ @Input() maxFileSize?: number; // tamaño máximo en bytes
73
+ @Input() maxFiles = 1;
74
+ @Input() multiple = false;
75
+ @Input() showFileSize = false;
76
+ @Input() fileSizeUnit: FileSizeUnit = 'AUTO';
77
+ @Input() displayName?: string;
78
+ @Input() placeholder = 'Select a file or drag here';
79
+ @Input() buttonText = 'Select file';
80
+ @Input() changeSelectedFileText = 'Change selected file(s)';
81
+ @Input() loadingText = 'Processing...';
82
+ @Input() ariaLabel?: string;
83
+
84
+ // Inputs - Validación
85
+ @Input() acceptedMimeTypes: string[] = [];
86
+ @Input() acceptedExtensions: string[] = [];
87
+ @Input() errorMessages: ErrorMessages = {
88
+ invalidSize: 'File exceeds maximum allowed size',
89
+ invalidType: 'File type not allowed',
90
+ tooManyFiles: 'Maximum number of files exceeded',
91
+ uploadError: 'Error processing file',
92
+ };
93
+
94
+ // Inputs - UI/UX
95
+ @Input() enableDragDrop = true;
96
+ @Input() showPreview = false;
97
+ @Input() previewMaxWidth = 200;
98
+ @Input() previewMaxHeight = 200;
99
+ @Input() showProgress = false;
100
+
101
+ // Outputs
102
+ @Output() fileSelected = new EventEmitter<File>();
103
+ @Output() filesSelected = new EventEmitter<File[]>();
104
+ @Output() fileRemoved = new EventEmitter<File>();
105
+ @Output() validationError = new EventEmitter<string[]>();
106
+ @Output() dragEnter = new EventEmitter<DragEvent>();
107
+ @Output() dragLeave = new EventEmitter<DragEvent>();
108
+ @Output() stateChange = new EventEmitter<FileInputState>();
109
+ // ControlValueAccessor Methods
110
+ writeValue(value: any): void {
111
+ if (value instanceof File) {
112
+ this.handleSingleFile(value);
113
+ } else if (Array.isArray(value)) {
114
+ this.handleMultipleFiles(value);
115
+ } else if (typeof value === 'string') {
116
+ this.fileName = value;
117
+ } else {
118
+ this.resetComponent();
119
+ }
120
+ }
121
+
122
+ registerOnChange(fn: (value: any) => void): void {
123
+ this.onChange = fn;
124
+ }
125
+
126
+ registerOnTouched(fn: () => void): void {
127
+ this.onTouched = fn;
128
+ }
129
+
130
+ setDisabledState(isDisabled: boolean): void {
131
+ this.isDisabled = isDisabled;
132
+ this.changeState(this.isDisabled ? FileInputState.IDLE : this.currentState);
133
+ }
134
+
135
+ ngOnDestroy(): void {
136
+ this.cleanupPreview();
137
+ }
138
+
139
+ // Event Handlers
140
+ onFileChange(event: Event): void {
141
+ const input = event.target as HTMLInputElement;
142
+ const selectedFiles = Array.from(input.files || []);
143
+ this.handleFileSelection(selectedFiles);
144
+ }
145
+
146
+ onDragOver(event: DragEvent): void {
147
+ if (!this.enableDragDrop || this.isDisabled) return;
148
+ event.preventDefault();
149
+ event.stopPropagation();
150
+ if (!this.isDragOver) {
151
+ this.isDragOver = true;
152
+ this.dragEnter.emit(event);
153
+ }
154
+ }
155
+
156
+ onDragLeave(event: DragEvent): void {
157
+ if (!this.enableDragDrop || this.isDisabled) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ this.isDragOver = false;
161
+ this.dragLeave.emit(event);
162
+ }
163
+
164
+ onDrop(event: DragEvent): void {
165
+ if (!this.enableDragDrop || this.isDisabled) return;
166
+ event.preventDefault();
167
+ event.stopPropagation();
168
+
169
+ this.isDragOver = false;
170
+ const files = Array.from(event.dataTransfer?.files || []);
171
+ this.handleFileSelection(files);
172
+ }
173
+
174
+ openFileDialog(): void {
175
+ if (this.isDisabled) return;
176
+ this.fileInput?.nativeElement.click();
177
+ }
178
+
179
+ removeFile(fileToRemove: File): void {
180
+ // Limpiar preview del archivo eliminado
181
+ const fileKey = `${fileToRemove.name}-${fileToRemove.size}-${fileToRemove.lastModified}`;
182
+ const previewUrl = this.previewUrls.get(fileKey);
183
+ if (previewUrl && previewUrl.startsWith('blob:')) {
184
+ URL.revokeObjectURL(previewUrl);
185
+ this.previewUrls.delete(fileKey);
186
+ }
187
+ if (this.multiple) {
188
+ this.files = this.files.filter((file) => file !== fileToRemove);
189
+ this.notifyChange(this.files.length > 0 ? this.files : null);
190
+ this.filesSelected.emit([...this.files]);
191
+ } else {
192
+ this.resetComponent();
193
+ this.notifyChange(null);
194
+ }
195
+ this.fileRemoved.emit(fileToRemove);
196
+ }
197
+ // Private Methods
198
+ private async handleFileSelection(selectedFiles: File[]): Promise<void> {
199
+ if (selectedFiles.length === 0) return;
200
+
201
+ this.changeState(FileInputState.LOADING);
202
+ this.validationErrors = [];
203
+
204
+ try {
205
+ // Validar número máximo de archivos
206
+ if (selectedFiles.length > this.maxFiles) {
207
+ this.validationErrors.push(this.errorMessages.tooManyFiles);
208
+ this.changeState(FileInputState.ERROR);
209
+ return;
210
+ }
211
+
212
+ const processedFiles: File[] = [];
213
+
214
+ for (const file of selectedFiles) {
215
+ const validation = this.validateFile(file);
216
+ if (!validation.isValid) {
217
+ this.validationErrors.push(...validation.errors);
218
+ continue;
219
+ }
220
+
221
+ try {
222
+ const processedFile = await this.processFile(file);
223
+ if (processedFile) {
224
+ processedFiles.push(processedFile);
225
+ }
226
+ } catch (error) {
227
+ this.validationErrors.push(this.errorMessages.uploadError);
228
+ }
229
+ }
230
+
231
+ if (this.validationErrors.length > 0) {
232
+ this.validationError.emit([...this.validationErrors]);
233
+ this.changeState(FileInputState.ERROR);
234
+ return;
235
+ }
236
+
237
+ if (processedFiles.length > 0) {
238
+ if (this.multiple) {
239
+ this.files = processedFiles;
240
+ this.filesSelected.emit([...this.files]);
241
+ this.notifyChange(this.files);
242
+ } else {
243
+ this.file = processedFiles[0];
244
+ this.handleSingleFile(this.file);
245
+ this.fileSelected.emit(this.file);
246
+ this.notifyChange(this.file);
247
+ }
248
+
249
+ this.changeState(FileInputState.SUCCESS);
250
+ }
251
+ } catch (error) {
252
+ this.validationErrors.push(this.errorMessages.uploadError);
253
+ this.validationError.emit([...this.validationErrors]);
254
+ this.changeState(FileInputState.ERROR);
255
+ }
256
+ }
257
+
258
+ private handleSingleFile(file: File): void {
259
+ this.file = file;
260
+ this.fileName = this.displayName || file.name;
261
+ this.selectedFileSize = this.calculateFileSizeInMB(file);
262
+ this.generatePreview(file);
263
+ }
264
+
265
+ private handleMultipleFiles(files: File[]): void {
266
+ this.files = files;
267
+ this.file = files[0] || undefined;
268
+ this.fileName = files.length > 0 ? `${files.length} file(s) selected` : undefined;
269
+ }
270
+
271
+ private async processFile(file: File): Promise<File | undefined> {
272
+ if (this.optimizeImage && file.type.includes('image') && this.optimizeImageToSize) {
273
+ try {
274
+ return await this.optimizeImage(file);
275
+ } catch (error) {
276
+ console.warn('Error optimizing image:', error);
277
+ return file; // Return original if optimization fails
278
+ }
279
+ }
280
+ return file;
281
+ }
282
+
283
+ private validateFile(file: File): FileValidationResult {
284
+ const errors: string[] = [];
285
+
286
+ // Validar tamaño
287
+ if (this.maxFileSize && file.size > this.maxFileSize) {
288
+ errors.push(this.errorMessages.invalidSize);
289
+ }
290
+
291
+ // Validar tipo MIME
292
+ if (this.acceptedMimeTypes.length > 0) {
293
+ if (!this.acceptedMimeTypes.some((type) => file.type.includes(type))) {
294
+ errors.push(this.errorMessages.invalidType);
295
+ }
296
+ }
297
+
298
+ // Validar extensión
299
+ if (this.acceptedExtensions.length > 0) {
300
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
301
+ if (!this.acceptedExtensions.some((ext) => ext.toLowerCase() === fileExtension)) {
302
+ errors.push(this.errorMessages.invalidType);
303
+ }
304
+ }
305
+
306
+ return {
307
+ isValid: errors.length === 0,
308
+ errors,
309
+ };
310
+ }
311
+
312
+ private generatePreview(file: File): void {
313
+ this.cleanupPreview();
314
+
315
+ if (this.showPreview && file.type.startsWith('image/')) {
316
+ const reader = new FileReader();
317
+ reader.onload = (e) => {
318
+ this.previewUrl = e.target?.result as string;
319
+ };
320
+ reader.readAsDataURL(file);
321
+ }
322
+ }
323
+
324
+ private cleanupPreview(): void {
325
+ // Limpiar preview simple
326
+ if (this.previewUrl && this.previewUrl.startsWith('blob:')) {
327
+ URL.revokeObjectURL(this.previewUrl);
328
+ }
329
+ this.previewUrl = undefined;
330
+
331
+ // Limpiar previews múltiples
332
+ this.previewUrls.forEach((url) => {
333
+ if (url.startsWith('blob:')) {
334
+ URL.revokeObjectURL(url);
335
+ }
336
+ });
337
+ this.previewUrls.clear();
338
+ }
339
+
340
+ private resetComponent(): void {
341
+ this.files = [];
342
+ this.file = undefined;
343
+ this.fileName = undefined;
344
+ this.selectedFileSize = undefined;
345
+ this.validationErrors = [];
346
+ this.hasInvalidSize = false;
347
+ this.hasInvalidType = false;
348
+ this.cleanupPreview();
349
+ this.changeState(FileInputState.IDLE);
350
+ }
351
+
352
+ private notifyChange(value: any): void {
353
+ if (this.onChange) {
354
+ this.onChange(value);
355
+ }
356
+ if (this.onTouched) {
357
+ this.onTouched();
358
+ }
359
+ }
360
+
361
+ private changeState(newState: FileInputState): void {
362
+ if (this.currentState !== newState) {
363
+ this.currentState = newState;
364
+ this.stateChange.emit(newState);
365
+ }
366
+ }
367
+
368
+ calculateFileSizeInMB(file: File): number {
369
+ const sizeInMB = file.size / 1024 / 1024;
370
+ return sizeInMB > 0.01 ? sizeInMB : 0.01;
371
+ }
372
+
373
+ formatFileSize(file: File): string {
374
+ const bytes = file.size;
375
+
376
+ if (bytes === 0) return '0 bytes';
377
+
378
+ const k = 1024;
379
+ const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB'];
380
+
381
+ // Si se especifica una unidad específica, usarla
382
+ if (this.fileSizeUnit !== 'AUTO') {
383
+ const targetUnitIndex = sizes.indexOf(this.fileSizeUnit);
384
+ if (targetUnitIndex !== -1) {
385
+ const size = bytes / Math.pow(k, targetUnitIndex);
386
+ const formattedSize = targetUnitIndex === 0 ? size.toString() : size.toFixed(2);
387
+ return `${formattedSize} ${this.fileSizeUnit}`;
388
+ }
389
+ }
390
+
391
+ // Comportamiento AUTO (original)
392
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
393
+ const size = bytes / Math.pow(k, i);
394
+ const formattedSize = i === 0 ? size.toString() : size.toFixed(2);
395
+
396
+ return `${formattedSize} ${sizes[i]}`;
397
+ }
398
+
399
+ // Public Utility Methods
400
+ getFileIcon(file?: File): string {
401
+ if (!file) return 'insert_drive_file';
402
+
403
+ if (file.type.startsWith('image/')) return '🖼️';
404
+ if (file.type.includes('pdf')) return '📄';
405
+ if (file.type.startsWith('video/')) return '🎥';
406
+ if (file.type.startsWith('audio/')) return '🎵';
407
+ if (file.type.includes('zip') || file.type.includes('rar')) return '🗜️';
408
+ if (file.type.includes('word')) return '📝';
409
+ if (file.type.includes('excel') || file.type.includes('sheet')) return '📊';
410
+
411
+ return '📄';
412
+ }
413
+
414
+ getPreviewUrl(file: File): string {
415
+ if (!file.type.startsWith('image/')) return '';
416
+
417
+ const fileKey = `${file.name}-${file.size}-${file.lastModified}`;
418
+
419
+ if (!this.previewUrls.has(fileKey)) {
420
+ const url = URL.createObjectURL(file);
421
+ this.previewUrls.set(fileKey, url);
422
+ }
423
+
424
+ return this.previewUrls.get(fileKey) || '';
425
+ }
426
+
427
+ get hasErrors(): boolean {
428
+ return this.validationErrors.length > 0 || this.currentState === FileInputState.ERROR;
429
+ }
430
+
431
+ get isLoading(): boolean {
432
+ return this.currentState === FileInputState.LOADING;
433
+ }
434
+
435
+ get hasFiles(): boolean {
436
+ return this.multiple ? this.files.length > 0 : !!this.file;
437
+ }
438
+ }
@@ -0,0 +1,15 @@
1
+
2
+ .matecu-spinner {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+
7
+
8
+ }
9
+ .matecu-spinner > svg {
10
+ width: 80px;;
11
+ height: 80px;;
12
+ }
13
+ .matecu-spinner__hidden {
14
+ display: none;
15
+ }
@@ -0,0 +1,44 @@
1
+ @if(active){
2
+
3
+ <svg xmlns:svg="http://www.w3.org/2000/svg" [attr.width]="size" [attr.height]="size" xmlns="http://www.w3.org/2000/svg"
4
+ xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" viewBox="0 0 128 128" xml:space="preserve">
5
+ <rect x="0" y="0" width="100%" height="100%" fill="transparent" />
6
+ <g>
7
+ <path class="matecu-spinner__100" d="M122.5 69.25H96.47a33.1 33.1 0 0 0 0-10.5h26.05a5.25 5.25 0 0 1 0 10.5z"
8
+ [attr.fill]="color" fill-opacity="1" />
9
+ <path class="matecu-spinner__30"
10
+ d="M112.04 97.83L89.47 84.8a33.1 33.1 0 0 0 5.25-9.1l22.57 13.03a5.25 5.25 0 0 1-5.28 9.1z"
11
+ [attr.fill]="color" fill-opacity="0.3" />
12
+ <path class="matecu-spinner__30"
13
+ d="M88.68 117.35L75.65 94.78a33.1 33.1 0 0 0 9.1-5.25l13.02 22.57a5.25 5.25 0 1 1-9.1 5.25z"
14
+ [attr.fill]="color" fill-opacity="0.3" />
15
+ <path class="matecu-spinner__30" d="M58.7 122.57V96.5a33.1 33.1 0 0 0 10.5 0v26.07a5.25 5.25 0 0 1-10.5 0z"
16
+ [attr.fill]="color" fill-opacity="0.3" />
17
+ <path class="matecu-spinner__30"
18
+ d="M30.1 112.1l13.04-22.57a33.1 33.1 0 0 0 9.1 5.25L39.2 117.35a5.25 5.25 0 1 1-9.1-5.25z"
19
+ [attr.fill]="color" fill-opacity="0.3" />
20
+ <path class="matecu-spinner__30"
21
+ d="M10.6 88.74L33.16 75.7a33.1 33.1 0 0 0 5.25 9.1L15.88 97.83a5.25 5.25 0 1 1-5.25-9.1z"
22
+ [attr.fill]="color" fill-opacity="0.3" />
23
+ <path class="matecu-spinner__40" d="M5.37 58.75h26.06a33.1 33.1 0 0 0 0 10.5H5.37a5.25 5.25 0 0 1 0-10.5z"
24
+ [attr.fill]="color" fill-opacity="0.4" />
25
+ <path class="matecu-spinner__50"
26
+ d="M15.85 30.17L38.4 43.2a33.1 33.1 0 0 0-5.24 9.1L10.6 39.25a5.25 5.25 0 1 1 5.25-9.1z" [attr.fill]="color"
27
+ fill-opacity="0.5" />
28
+ <path class="matecu-spinner__60"
29
+ d="M39.2 10.65l13.03 22.57a33.1 33.1 0 0 0-9.1 5.25l-13-22.57a5.25 5.25 0 1 1 9.1-5.25z" [attr.fill]="color"
30
+ fill-opacity="0.6" />
31
+ <path class="matecu-spinner__70" d="M69.2 5.43V31.5a33.1 33.1 0 0 0-10.5 0V5.42a5.25 5.25 0 1 1 10.5 0z"
32
+ [attr.fill]="color" fill-opacity="0.7" />
33
+ <path class="matecu-spinner__80"
34
+ d="M97.77 15.9L84.75 38.47a33.1 33.1 0 0 0-9.1-5.25l13.03-22.57a5.25 5.25 0 1 1 9.1 5.25z"
35
+ [attr.fill]="color" fill-opacity="0.8" />
36
+ <path class="matecu-spinner__90"
37
+ d="M117.3 39.26L94.7 52.3a33.1 33.1 0 0 0-5.25-9.1l22.57-13.03a5.25 5.25 0 0 1 5.25 9.1z"
38
+ [attr.fill]="color" fill-opacity="0.9" />
39
+ <animateTransform attributeName="transform" type="rotate"
40
+ values="0 64 64;30 64 64;60 64 64;90 64 64;120 64 64;150 64 64;180 64 64;210 64 64;240 64 64;270 64 64;300 64 64;330 64 64"
41
+ calcMode="discrete" dur="600ms" repeatCount="indefinite"></animateTransform>
42
+ </g>
43
+ </svg>
44
+ }
@@ -0,0 +1,25 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { MatecuSpinnerComponent } from './matecu-spinner.component';
4
+
5
+ describe('MatecuSpinnerComponent', () => {
6
+ let component: MatecuSpinnerComponent;
7
+ let fixture: ComponentFixture<MatecuSpinnerComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [ MatecuSpinnerComponent ]
12
+ })
13
+ .compileComponents();
14
+ });
15
+
16
+ beforeEach(() => {
17
+ fixture = TestBed.createComponent(MatecuSpinnerComponent);
18
+ component = fixture.componentInstance;
19
+ fixture.detectChanges();
20
+ });
21
+
22
+ it('should create', () => {
23
+ expect(component).toBeTruthy();
24
+ });
25
+ });
@@ -0,0 +1,54 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
3
+ import { Subject } from 'rxjs';
4
+ import { takeUntil, tap } from 'rxjs/operators';
5
+ import { MatecuSpinnerService } from '../../services/matecu-spinner.service';
6
+ @Component({
7
+ selector: 'matecu-spinner',
8
+ templateUrl: './matecu-spinner.component.html',
9
+ styleUrls: ['./matecu-spinner.component.css'],
10
+ standalone: true,
11
+ imports: [CommonModule],
12
+ })
13
+ export class MatecuSpinnerComponent implements OnInit, OnDestroy {
14
+ private destroy$ = new Subject<void>();
15
+ private hiddenClass = 'matecu-spinner--hidden';
16
+ private activeCache = false;
17
+ @HostBinding('class') className = 'matecu-spinner';
18
+ @Input() color = '#2196F3';
19
+ @Input() global = false;
20
+ get active(): boolean {
21
+ return this.activeCache;
22
+ }
23
+ @Input() set active(value: boolean) {
24
+ if (this.global) {
25
+ return;
26
+ }
27
+ this.activeCache = value;
28
+ this.className = value
29
+ ? this.className.replace(` ${this.hiddenClass}`, '')
30
+ : (this.className += ` ${this.hiddenClass}`);
31
+ }
32
+ @Input() size = '70px';
33
+
34
+ constructor(
35
+ private spinnerService: MatecuSpinnerService,
36
+ private cdr: ChangeDetectorRef,
37
+ ) {}
38
+ ngOnDestroy(): void {
39
+ this.destroy$.next();
40
+ this.destroy$.complete();
41
+ }
42
+ ngOnInit(): void {
43
+ if (this.global) {
44
+ this.spinnerService
45
+ .watch()
46
+ .pipe(
47
+ tap((value) => (this.activeCache = value)),
48
+ tap(() => this.cdr.detectChanges()),
49
+ takeUntil(this.destroy$),
50
+ )
51
+ .subscribe();
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,13 @@
1
+ $size: 80px;
2
+ :host, .matecu-spinner {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ > svg {
7
+ width: $size;
8
+ height: $size;
9
+ }
10
+ &__hidden {
11
+ display: none;
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ :host {
2
+ --mtb-bar-height: 64px;
3
+ --mtb-action-padding: 10px;
4
+ --mtb-action-margin: 0px;
5
+ }
6
+ :host,
7
+ .matecu-topbar-action {
8
+ &,
9
+ * {
10
+ box-sizing: border-box;
11
+ outline: none;
12
+ }
13
+ height: 100%;
14
+ display: flex;
15
+ align-items: flex-start;
16
+ max-height: var(--mtb-bar-height);
17
+ padding: var(--mtb-action-padding);
18
+ margin: var(--mtb-action-margin);
19
+ }
@@ -0,0 +1,25 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { MatecuTopbarActionComponent } from './matecu-topbar-action.component';
4
+
5
+ describe('MatecuTopbarActionsComponent', () => {
6
+ let component: MatecuTopbarActionComponent;
7
+ let fixture: ComponentFixture<MatecuTopbarActionComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ declarations: [MatecuTopbarActionComponent]
12
+ })
13
+ .compileComponents();
14
+ });
15
+
16
+ beforeEach(() => {
17
+ fixture = TestBed.createComponent(MatecuTopbarActionComponent);
18
+ component = fixture.componentInstance;
19
+ fixture.detectChanges();
20
+ });
21
+
22
+ it('should create', () => {
23
+ expect(component).toBeTruthy();
24
+ });
25
+ });
@@ -0,0 +1,14 @@
1
+ import { Component, HostBinding, OnInit } from '@angular/core';
2
+
3
+ @Component({
4
+ selector: 'matecu-topbar-action',
5
+ templateUrl: './matecu-topbar-action.component.html',
6
+ styleUrls: ['./matecu-topbar-action.component.scss'],
7
+ standalone: true,
8
+ })
9
+ export class MatecuTopbarActionComponent implements OnInit {
10
+ @HostBinding('class') className = 'matecu-topbar-action';
11
+ constructor() {}
12
+
13
+ ngOnInit(): void {}
14
+ }
@@ -0,0 +1 @@
1
+ <ng-content></ng-content>
@@ -0,0 +1,14 @@
1
+ :host,
2
+ .matecu-topbar-body {
3
+ display: block;
4
+ &,
5
+ * {
6
+ box-sizing: border-box;
7
+ outline: none;
8
+ }
9
+
10
+ padding: var(--mtb-body-padding, 30px);
11
+ background-color: var(--mtb-body-background, auto);
12
+ overflow-y: auto;
13
+ padding-bottom: var(--mtb-body-padding-button, 60px);
14
+ }