ngx-eiffage-material 0.3.0 → 0.4.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.
@@ -7,7 +7,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
7
7
  import { Subject, EMPTY, merge, fromEvent, startWith, map, catchError, of, skip, debounceTime, distinctUntilChanged } from 'rxjs';
8
8
  import { toSignal, takeUntilDestroyed, rxResource, toObservable } from '@angular/core/rxjs-interop';
9
9
  import * as i3$1 from '@angular/forms';
10
- import { NgControl, FormGroupDirective, NgForm, Validators, FormControl, ReactiveFormsModule, FormArray, FormGroup, FormBuilder, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
10
+ import { NgControl, FormGroupDirective, NgForm, Validators, FormControl, ReactiveFormsModule, FormArray, FormGroup, FormBuilder, FormsModule } from '@angular/forms';
11
11
  import * as i2$1 from '@angular/material/chips';
12
12
  import { MatChipGrid, MatChipsModule } from '@angular/material/chips';
13
13
  import * as i2$2 from '@angular/material/form-field';
@@ -471,7 +471,7 @@ const NGX_EIFFAGE_DEFAULT_TEXTS = {
471
471
  remove: name => `Eliminar ${name}`,
472
472
  removed: name => `Archivo ${name} eliminado`,
473
473
  added: count => plural(count, '1 archivo añadido', `${count} archivos añadidos`),
474
- maxFiles: max => `Máximo ${max} archivos permitidos`,
474
+ maxFiles: max => plural(max, 'Solo se permite 1 archivo', `Máximo ${max} archivos permitidos`),
475
475
  typeNotAllowed: type => `Tipo no permitido: ${type}`,
476
476
  tooLarge: maxSize => `Excede el tamaño máximo (${maxSize})`,
477
477
  },
@@ -730,7 +730,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
730
730
  }, template: "<!--\r\n Mientras carga: disabledInteractive mantiene el foco en el bot\u00F3n (el lector de pantalla sigue en \u00E9l\r\n y oye aria-busy). Material NO bloquea el clic de un <button> en ese estado: lo hace blockWhileLoading.\r\n Con disabled, el bot\u00F3n se deshabilita del todo y el navegador ya no emite clics.\r\n-->\r\n<button\r\n class=\"loadingButton\"\r\n [matButton]=\"matButton()\"\r\n [type]=\"type()\"\r\n [disabled]=\"disabled() || isLoading()\"\r\n [disabledInteractive]=\"isLoading() && !disabled()\"\r\n [attr.aria-busy]=\"isLoading() || null\"\r\n (click)=\"blockWhileLoading($event)\"\r\n>\r\n <span class=\"loadingButton__content\" [class.loadingButton__content--hidden]=\"overlaySpinner()\" [class.loadingButton__content--gone]=\"inlineSpinner()\">\r\n {{ label() }}\r\n <ng-content />\r\n </span>\r\n\r\n @if (inlineSpinner()) {\r\n <span class=\"loadingButton__content\">\r\n <mat-spinner class=\"loadingButton__inlineSpinner\" diameter=\"18\" strokeWidth=\"2.5\" aria-hidden=\"true\" />\r\n {{ loadingText() }}\r\n </span>\r\n }\r\n\r\n @if (overlaySpinner()) {\r\n <mat-spinner\r\n class=\"loadingButton__overlaySpinner\"\r\n [diameter]=\"spinnerDiameter()\"\r\n [strokeWidth]=\"spinnerStrokeWidth()\"\r\n [attr.aria-label]=\"resolvedBusyLabel()\"\r\n />\r\n }\r\n</button>\r\n", styles: [":host{display:inline-flex;vertical-align:middle}.loadingButton{width:100%}.loadingButton__content{display:inline-flex;align-items:center;justify-content:center;gap:8px}.loadingButton__content--hidden{visibility:hidden}.loadingButton__content--gone{display:none}.loadingButton__content ::ng-deep .mat-icon{width:18px;height:18px;font-size:18px}.loadingButton__overlaySpinner,.loadingButton__inlineSpinner{--mat-progress-spinner-active-indicator-color: currentColor}.loadingButton__overlaySpinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none}.loadingButton__inlineSpinner{flex:none}.loadingButton[aria-busy=true]{cursor:progress}:host(.ngx-loading-button--loading){--mat-button-filled-disabled-container-color: var(--mat-button-filled-container-color, var(--mat-sys-primary));--mat-button-filled-disabled-label-text-color: var(--mat-button-filled-label-text-color, var(--mat-sys-on-primary));--mat-button-tonal-disabled-container-color: var(--mat-button-tonal-container-color, var(--mat-sys-secondary-container));--mat-button-tonal-disabled-label-text-color: var(--mat-button-tonal-label-text-color, var(--mat-sys-on-secondary-container));--mat-button-protected-disabled-container-color: var(--mat-button-protected-container-color, var(--mat-sys-surface));--mat-button-protected-disabled-container-elevation-shadow: var(--mat-button-protected-container-elevation-shadow, var(--mat-sys-level1));--mat-button-protected-disabled-label-text-color: var(--mat-button-protected-label-text-color, var(--mat-sys-primary));--mat-button-outlined-disabled-outline-color: var(--mat-button-outlined-outline-color, var(--mat-sys-outline));--mat-button-outlined-disabled-label-text-color: var(--mat-button-outlined-label-text-color, var(--mat-sys-primary));--mat-button-text-disabled-label-text-color: var(--mat-button-text-label-text-color, var(--mat-sys-primary))}:host(.ngx-loading-button--secondary){--mat-button-filled-container-color: var(--mat-sys-secondary);--mat-button-filled-label-text-color: var(--mat-sys-on-secondary);--mat-button-filled-state-layer-color: var(--mat-sys-on-secondary);--mat-button-tonal-container-color: var(--mat-sys-secondary-container);--mat-button-tonal-label-text-color: var(--mat-sys-on-secondary-container);--mat-button-tonal-state-layer-color: var(--mat-sys-on-secondary-container);--mat-button-outlined-label-text-color: var(--mat-sys-secondary);--mat-button-outlined-state-layer-color: var(--mat-sys-secondary);--mat-button-outlined-ripple-color: var(--mat-sys-secondary-container);--mat-button-protected-label-text-color: var(--mat-sys-secondary);--mat-button-protected-state-layer-color: var(--mat-sys-secondary);--mat-button-protected-ripple-color: var(--mat-sys-secondary-container);--mat-button-text-label-text-color: var(--mat-sys-secondary);--mat-button-text-state-layer-color: var(--mat-sys-secondary);--mat-button-text-ripple-color: var(--mat-sys-secondary-container)}:host(.ngx-loading-button--tertiary){--mat-button-filled-container-color: var(--mat-sys-tertiary);--mat-button-filled-label-text-color: var(--mat-sys-on-tertiary);--mat-button-filled-state-layer-color: var(--mat-sys-on-tertiary);--mat-button-tonal-container-color: var(--mat-sys-tertiary-container);--mat-button-tonal-label-text-color: var(--mat-sys-on-tertiary-container);--mat-button-tonal-state-layer-color: var(--mat-sys-on-tertiary-container);--mat-button-outlined-label-text-color: var(--mat-sys-tertiary);--mat-button-outlined-state-layer-color: var(--mat-sys-tertiary);--mat-button-outlined-ripple-color: var(--mat-sys-tertiary-container);--mat-button-protected-label-text-color: var(--mat-sys-tertiary);--mat-button-protected-state-layer-color: var(--mat-sys-tertiary);--mat-button-protected-ripple-color: var(--mat-sys-tertiary-container);--mat-button-text-label-text-color: var(--mat-sys-tertiary);--mat-button-text-state-layer-color: var(--mat-sys-tertiary);--mat-button-text-ripple-color: var(--mat-sys-tertiary-container)}:host(.ngx-loading-button--error){--mat-button-filled-container-color: var(--mat-sys-error);--mat-button-filled-label-text-color: var(--mat-sys-on-error);--mat-button-filled-state-layer-color: var(--mat-sys-on-error);--mat-button-tonal-container-color: var(--mat-sys-error-container);--mat-button-tonal-label-text-color: var(--mat-sys-on-error-container);--mat-button-tonal-state-layer-color: var(--mat-sys-on-error-container);--mat-button-outlined-label-text-color: var(--mat-sys-error);--mat-button-outlined-state-layer-color: var(--mat-sys-error);--mat-button-outlined-ripple-color: var(--mat-sys-error-container);--mat-button-protected-label-text-color: var(--mat-sys-error);--mat-button-protected-state-layer-color: var(--mat-sys-error);--mat-button-protected-ripple-color: var(--mat-sys-error-container);--mat-button-text-label-text-color: var(--mat-sys-error);--mat-button-text-state-layer-color: var(--mat-sys-error);--mat-button-text-ripple-color: var(--mat-sys-error-container)}\n"] }]
731
731
  }], propDecorators: { isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], matButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "matButton", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], spinnerDiameter: [{ type: i0.Input, args: [{ isSignal: true, alias: "spinnerDiameter", required: false }] }], spinnerStrokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "spinnerStrokeWidth", required: false }] }], busyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "busyLabel", required: false }] }] } });
732
732
 
733
- // Utilidades internas de ngx-input-file (no se exportan en el barril).
733
+ // Utilidades internas de ngx-input-file y ngx-file-input (no se exportan en el barril).
734
734
  /** Normaliza `accept` (array o cadena separada por comas) a patrones en minúsculas. `*` o vacío = sin filtro. */
735
735
  function parseAccept(accept) {
736
736
  const parts = Array.isArray(accept) ? accept : String(accept ?? '').split(',');
@@ -786,6 +786,18 @@ function fileIcon(file) {
786
786
  return 'folder_zip';
787
787
  return 'draft';
788
788
  }
789
+ function fileKind(file) {
790
+ const type = file.type.toLowerCase();
791
+ if (type.startsWith('image/'))
792
+ return 'image';
793
+ if (type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))
794
+ return 'pdf';
795
+ if (type.startsWith('video/'))
796
+ return 'video';
797
+ if (type.startsWith('audio/'))
798
+ return 'audio';
799
+ return 'generic';
800
+ }
789
801
 
790
802
  /**
791
803
  * Selector de ficheros con forma de campo de formulario: un `mat-form-field` con los ficheros como chips.
@@ -4653,309 +4665,306 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
4653
4665
  }, template: "<div class=\"ngx-skeleton-lines\" [style.gap]=\"isMultiLine() ? lineSpacing() : null\">\n @for (i of lineIndices(); track i; let last = $last) {\n <div\n class=\"ngx-skeleton-bone\"\n [class.ngx-skeleton-shimmer]=\"animation() === 'shimmer'\"\n [class.ngx-skeleton-pulse]=\"animation() === 'pulse'\"\n [style.width]=\"isMultiLine() && last ? lastLineWidth() : computedWidth()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"computedBorderRadius()\"\n ></div>\n }\n</div>\n", styles: [":host{display:block}.ngx-skeleton-lines{display:flex;flex-direction:column}.ngx-skeleton-bone{position:relative;max-width:100%;overflow:hidden;background-color:var(--ngx-skeleton-color, var(--mat-sys-surface-container-highest))}.ngx-skeleton-bone.ngx-skeleton-shimmer:after{content:\"\";position:absolute;inset:0;transform:translate(-100%);background:linear-gradient(90deg,transparent 0%,var(--ngx-skeleton-highlight, color-mix(in srgb, var(--mat-sys-surface) 60%, transparent)) 50%,transparent 100%);will-change:transform;animation:ngx-skeleton-shimmer var(--ngx-skeleton-duration, 1.5s) infinite ease-in-out}.ngx-skeleton-bone.ngx-skeleton-pulse{animation:ngx-skeleton-pulse var(--ngx-skeleton-duration, 1.5s) infinite ease-in-out}@keyframes ngx-skeleton-shimmer{to{transform:translate(100%)}}@keyframes ngx-skeleton-pulse{0%,to{opacity:1}50%{opacity:.4}}@media (prefers-reduced-motion: reduce){.ngx-skeleton-bone.ngx-skeleton-shimmer:after{animation:none;opacity:0}.ngx-skeleton-bone.ngx-skeleton-pulse{animation:none}}\n"] }]
4654
4666
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], lineSpacing: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineSpacing", required: false }] }], lastLineWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastLineWidth", required: false }] }], animation: [{ type: i0.Input, args: [{ isSignal: true, alias: "animation", required: false }] }], animationDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "animationDuration", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], decorative: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorative", required: false }] }] } });
4655
4667
 
4656
- let _uid = 0;
4668
+ let nextId = 0;
4669
+ /**
4670
+ * Zona para soltar o seleccionar ficheros, con lista de ficheros y previsualización de imágenes.
4671
+ * Se usa con `formControlName` / `[formControl]` / `ngModel` (valor: los `File[]` válidos) o con `[(files)]`.
4672
+ */
4657
4673
  class NgxFileInput {
4658
- _platform = inject(PLATFORM_ID);
4659
- _inputId = `ngx-fi-${++_uid}`;
4660
- _nativeInput = viewChild('nativeInput', ...(ngDevMode ? [{ debugName: "_nativeInput" }] : []));
4674
+ // Se registra como value accessor a mano para poder leer el control (obligatorio y error), como ngx-input-file
4675
+ _ngControl = inject(NgControl, { self: true, optional: true });
4676
+ _parentForm = inject(FormGroupDirective, { optional: true }) ?? inject(NgForm, { optional: true });
4677
+ _host = inject(ElementRef);
4678
+ _locale = inject(LOCALE_ID);
4679
+ _isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
4680
+ _nativeInput = viewChild.required('nativeInput');
4681
+ texts = inject(NgxTexts).texts;
4661
4682
  // ── Inputs ──────────────────────────────────────────────────────────────────
4662
- /** Accepted MIME types or extensions (e.g. `'image/*,.pdf'`). */
4683
+ /** Tipos admitidos: extensiones (`.pdf`), MIME (`application/pdf`) o comodines (`image/*`). `'*'` = cualquiera. */
4663
4684
  accept = input('*', ...(ngDevMode ? [{ debugName: "accept" }] : []));
4664
- /** Allow selecting multiple files. */
4685
+ /** Admite varios ficheros: cada selección se añade a la anterior. */
4665
4686
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4666
- /** Maximum file size in bytes per file. `null` means no limit. */
4687
+ /** Tamaño máximo por fichero, en bytes. `null` = sin límite. */
4667
4688
  maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
4668
- /** Maximum number of files when `multiple` is true. `null` means no limit. */
4689
+ /** Número máximo de ficheros válidos (con `multiple`). `null` = sin límite. */
4669
4690
  maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
4670
- /** Disables the component. */
4671
4691
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4672
- /** Label text shown inside the drop zone. Por defecto, el texto `fileInput.dropLabel` de la librería. */
4692
+ /** Texto principal. Por defecto, el texto `fileInput.dropLabel` de la librería. */
4673
4693
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
4674
- texts = inject(NgxTexts).texts;
4675
- resolvedLabel = computed(() => this.label() ?? this.texts().fileInput.dropLabel, ...(ngDevMode ? [{ debugName: "resolvedLabel" }] : []));
4676
- /** Secondary hint text (formats, sizes…). */
4694
+ /** Texto secundario (formatos, tamaños…). */
4677
4695
  hint = input(null, ...(ngDevMode ? [{ debugName: "hint" }] : []));
4678
- /** Show the file preview list below the drop zone. */
4696
+ /** Mensaje de error. Se ve cuando el control es inválido y está tocado o el formulario se ha enviado. */
4697
+ error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
4698
+ /** Muestra la lista de ficheros (con miniatura de las imágenes). */
4679
4699
  showPreview = input(true, ...(ngDevMode ? [{ debugName: "showPreview", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4680
- /** Visual variant: `'default'` (large zone), `'compact'` (horizontal), or `'button'`. */
4700
+ /** `'default'` (zona grande), `'compact'` (barra horizontal) o `'button'` (solo un botón). */
4681
4701
  variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
4702
+ /** Ficheros válidos seleccionados. Admite `[(files)]` para usarlo sin formularios. */
4703
+ files = model([], ...(ngDevMode ? [{ debugName: "files" }] : []));
4682
4704
  // ── Outputs ─────────────────────────────────────────────────────────────────
4683
- /** Emits when valid files are added. */
4705
+ /** Ficheros válidos añadidos en una selección o al soltar. */
4684
4706
  filesSelected = output();
4685
- /** Emits when a file is removed from the list. */
4707
+ /** Fichero quitado de la lista (válido o rechazado). */
4686
4708
  fileRemoved = output();
4687
- /** Emits for each file that fails validation. */
4709
+ /** Un fichero rechazado (tipo, tamaño o número). */
4688
4710
  validationError = output();
4689
- // ── Internal state ──────────────────────────────────────────────────────────
4690
- /** All file items (valid + invalid). */
4691
- items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
4692
- /** Whether the user is dragging files over the drop zone. */
4693
- isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4694
- /** Brief flash after a successful drop. */
4695
- showDropConfirm = signal(false, ...(ngDevMode ? [{ debugName: "showDropConfirm" }] : []));
4696
- /** Announcement text for screen readers. */
4697
- liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
4698
- /** Unique id for the hidden file input. */
4699
- inputId = this._inputId;
4700
- /** Disabled state coming from `setDisabledState()` (reactive forms). */
4711
+ // ── Estado ──────────────────────────────────────────────────────────────────
4712
+ id = `ngx-fi-${nextId++}`;
4713
+ _entries = signal([], ...(ngDevMode ? [{ debugName: "_entries" }] : []));
4701
4714
  _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : []));
4702
- /**
4703
- * Effective disabled state: the `disabled` input OR the state pushed by the
4704
- * parent FormControl through `setDisabledState()`.
4705
- */
4715
+ /** Deshabilitado por el input `disabled` o por el control del formulario. */
4706
4716
  isDisabled = computed(() => this.disabled() || this._cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
4707
- /** Derived: valid files only (used as FormControl value). */
4708
- validFiles = computed(() => this.items().filter(i => i.status === 'valid').map(i => i.file), ...(ngDevMode ? [{ debugName: "validFiles" }] : []));
4709
- // ── ControlValueAccessor ────────────────────────────────────────────────────
4717
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4718
+ showDropConfirm = signal(false, ...(ngDevMode ? [{ debugName: "showDropConfirm" }] : []));
4719
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : []));
4720
+ isRequired = signal(false, ...(ngDevMode ? [{ debugName: "isRequired" }] : []));
4721
+ showError = signal(false, ...(ngDevMode ? [{ debugName: "showError" }] : []));
4722
+ resolvedLabel = computed(() => this.label() ?? this.texts().fileInput.dropLabel, ...(ngDevMode ? [{ debugName: "resolvedLabel" }] : []));
4723
+ acceptAttr = computed(() => parseAccept(this.accept()).join(','), ...(ngDevMode ? [{ debugName: "acceptAttr" }] : []));
4724
+ describedBy = computed(() => [this.hint() ? `${this.id}-hint` : '', this.showError() && this.error() ? `${this.id}-error` : ''].filter(Boolean).join(' ') || null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
4725
+ /** Lo que pinta la lista: icono, color, tamaño legible y mensaje de error ya traducido. */
4726
+ views = computed(() => this._entries().map(({ item, code }) => ({
4727
+ item,
4728
+ icon: fileIcon(item.file),
4729
+ kind: fileKind(item.file),
4730
+ size: formatFileSize(item.size, this._locale),
4731
+ error: code ? this._message(item.file, code) : null,
4732
+ })), ...(ngDevMode ? [{ debugName: "views" }] : []));
4710
4733
  _onChange = () => { };
4711
4734
  _onTouched = () => { };
4712
- writeValue(files) {
4713
- if (!files || files.length === 0) {
4714
- this.items.set([]);
4735
+ _controlSub;
4736
+ _confirmTimer;
4737
+ constructor() {
4738
+ if (this._ngControl)
4739
+ this._ngControl.valueAccessor = this;
4740
+ // [files] o [(files)] desde fuera: reconstruye la lista si no coincide con los válidos actuales
4741
+ effect(() => {
4742
+ const files = this.files();
4743
+ untracked(() => {
4744
+ const valid = this._validFiles();
4745
+ if (files.length !== valid.length || files.some((file, i) => file !== valid[i]))
4746
+ this._resetFrom(files);
4747
+ });
4748
+ });
4749
+ inject(DestroyRef).onDestroy(() => {
4750
+ this._controlSub?.unsubscribe();
4751
+ clearTimeout(this._confirmTimer);
4752
+ this._entries().forEach(({ item }) => this._revokePreview(item));
4753
+ });
4754
+ }
4755
+ ngAfterViewInit() {
4756
+ // formControlName asigna su control después de registerOnChange: aquí ya está
4757
+ if (this._ngControl?.control)
4758
+ this._bindControl(this._ngControl.control);
4759
+ }
4760
+ ngDoCheck() {
4761
+ // resetForm quita el «enviado» sin emitir evento: se recalcula en cada comprobación
4762
+ this._syncFormState();
4763
+ }
4764
+ // ── API pública ─────────────────────────────────────────────────────────────
4765
+ /** Abre el selector de ficheros del sistema (no hace nada si está deshabilitado). */
4766
+ openFilePicker() {
4767
+ if (!this.isDisabled())
4768
+ this._nativeInput().nativeElement.click();
4769
+ }
4770
+ /** Quita un fichero de la lista, válido o rechazado. */
4771
+ removeFile(item) {
4772
+ const entry = this._entries().find(e => e.item.id === item.id);
4773
+ if (!entry)
4715
4774
  return;
4716
- }
4717
- this.items.set(files.map(f => this._toFileItem(f)));
4718
- this._generatePreviews(this.items());
4775
+ this._revokePreview(item);
4776
+ this._entries.update(list => list.filter(e => e !== entry));
4777
+ this.fileRemoved.emit(item);
4778
+ this.announcement.set(this.texts().fileInput.removed(item.name));
4779
+ if (!entry.code)
4780
+ this._commit();
4719
4781
  }
4720
- registerOnChange(fn) {
4721
- this._onChange = fn;
4782
+ // ── Eventos de la plantilla ─────────────────────────────────────────────────
4783
+ onZoneKeydown(event) {
4784
+ if (event.key === 'Enter' || event.key === ' ') {
4785
+ event.preventDefault();
4786
+ this.openFilePicker();
4787
+ }
4722
4788
  }
4723
- registerOnTouched(fn) {
4724
- this._onTouched = fn;
4789
+ onZoneBlur() {
4790
+ // Al abrir el diálogo del sistema la ventana pierde el foco: eso no cuenta como salir del campo
4791
+ if (typeof document !== 'undefined' && document.hasFocus())
4792
+ this._markTouched();
4725
4793
  }
4726
- setDisabledState(isDisabled) {
4727
- this._cvaDisabled.set(isDisabled);
4794
+ onNativeChange(event) {
4795
+ const input = event.target;
4796
+ this._addFiles(Array.from(input.files ?? []));
4797
+ input.value = '';
4728
4798
  }
4729
- // ── Drag & Drop ─────────────────────────────────────────────────────────────
4730
4799
  onDragOver(event) {
4800
+ // Siempre: si no, el navegador abre el fichero al soltarlo
4731
4801
  event.preventDefault();
4732
- event.stopPropagation();
4802
+ if (event.dataTransfer)
4803
+ event.dataTransfer.dropEffect = this.isDisabled() ? 'none' : 'copy';
4733
4804
  if (!this.isDisabled())
4734
4805
  this.isDragOver.set(true);
4735
4806
  }
4736
4807
  onDragLeave(event) {
4737
- event.preventDefault();
4738
- event.stopPropagation();
4739
- this.isDragOver.set(false);
4808
+ // dragleave también salta al pasar por encima de un hijo: solo cuenta si sale del componente
4809
+ if (!this._host.nativeElement.contains(event.relatedTarget))
4810
+ this.isDragOver.set(false);
4740
4811
  }
4741
4812
  onDrop(event) {
4742
4813
  event.preventDefault();
4743
- event.stopPropagation();
4744
4814
  this.isDragOver.set(false);
4745
4815
  if (this.isDisabled())
4746
4816
  return;
4747
- const files = event.dataTransfer?.files;
4748
- if (files && files.length > 0) {
4749
- this._processFiles(Array.from(files));
4750
- this._flashDropConfirm();
4751
- }
4817
+ const files = Array.from(event.dataTransfer?.files ?? []);
4818
+ if (!files.length)
4819
+ return;
4820
+ this._addFiles(files);
4821
+ this._flashDropConfirm();
4752
4822
  }
4753
- // ── File input ──────────────────────────────────────────────────────────────
4754
- onFileInputChange(event) {
4755
- const input = event.target;
4756
- if (input.files && input.files.length > 0) {
4757
- this._processFiles(Array.from(input.files));
4758
- }
4759
- input.value = '';
4823
+ // ── ControlValueAccessor ────────────────────────────────────────────────────
4824
+ writeValue(files) {
4825
+ this.files.set(Array.isArray(files) ? files : []);
4760
4826
  }
4761
- openFilePicker() {
4762
- if (this.isDisabled())
4763
- return;
4764
- this._onTouched();
4765
- this._nativeInput()?.nativeElement.click();
4827
+ registerOnChange(fn) {
4828
+ this._onChange = fn;
4829
+ if (this._ngControl?.control)
4830
+ this._bindControl(this._ngControl.control);
4766
4831
  }
4767
- // ── Remove ──────────────────────────────────────────────────────────────────
4768
- removeFile(item) {
4769
- this.items.update(list => list.filter(i => i.id !== item.id));
4770
- this.fileRemoved.emit(item);
4771
- this._emitChange();
4772
- this.liveAnnouncement.set(this.texts().fileInput.removed(item.name));
4773
- }
4774
- // ── Helpers ─────────────────────────────────────────────────────────────────
4775
- /** Format bytes to human-readable string. */
4776
- formatSize(bytes) {
4777
- if (bytes < 1024)
4778
- return `${bytes} B`;
4779
- if (bytes < 1024 * 1024)
4780
- return `${(bytes / 1024).toFixed(1)} KB`;
4781
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4782
- }
4783
- /** Get a Material icon name based on MIME type. */
4784
- getFileIcon(type) {
4785
- if (type.startsWith('image/'))
4786
- return 'image';
4787
- if (type === 'application/pdf')
4788
- return 'picture_as_pdf';
4789
- if (type.startsWith('video/'))
4790
- return 'videocam';
4791
- if (type.startsWith('audio/'))
4792
- return 'audiotrack';
4793
- if (type.includes('spreadsheet') || type.includes('excel'))
4794
- return 'table_chart';
4795
- if (type.includes('document') || type.includes('word'))
4796
- return 'description';
4797
- if (type.includes('zip') || type.includes('compressed'))
4798
- return 'folder_zip';
4799
- return 'insert_drive_file';
4800
- }
4801
- /** Get an accent color class based on MIME type. */
4802
- getFileColorClass(type) {
4803
- if (type.startsWith('image/'))
4804
- return 'ngx-fi-icon--image';
4805
- if (type === 'application/pdf')
4806
- return 'ngx-fi-icon--pdf';
4807
- if (type.startsWith('video/'))
4808
- return 'ngx-fi-icon--video';
4809
- if (type.startsWith('audio/'))
4810
- return 'ngx-fi-icon--audio';
4811
- return 'ngx-fi-icon--generic';
4812
- }
4813
- // ── Private ─────────────────────────────────────────────────────────────────
4814
- _processFiles(files) {
4815
- this._onTouched();
4816
- let incoming = files;
4817
- const currentCount = this.items().length;
4818
- const max = this.maxFiles();
4819
- // Max files check
4820
- if (this.multiple() && max !== null && currentCount + incoming.length > max) {
4821
- const allowed = Math.max(0, max - currentCount);
4822
- const rejected = incoming.slice(allowed);
4823
- incoming = incoming.slice(0, allowed);
4824
- for (const f of rejected) {
4825
- const err = {
4826
- file: f,
4827
- code: 'MAX_FILES_EXCEEDED',
4828
- message: this.texts().fileInput.maxFiles(max),
4829
- };
4830
- this.validationError.emit(err);
4831
- }
4832
- }
4833
- // Single mode replaces current
4834
- if (!this.multiple()) {
4835
- incoming = incoming.slice(0, 1);
4832
+ registerOnTouched(fn) {
4833
+ this._onTouched = fn;
4834
+ }
4835
+ setDisabledState(isDisabled) {
4836
+ this._cvaDisabled.set(isDisabled);
4837
+ }
4838
+ // ── Privado ─────────────────────────────────────────────────────────────────
4839
+ _validFiles() {
4840
+ return this._entries().filter(e => !e.code).map(e => e.item.file);
4841
+ }
4842
+ _addFiles(incoming) {
4843
+ const patterns = parseAccept(this.accept());
4844
+ const maxSize = this.maxSize();
4845
+ const max = this.multiple() ? this.maxFiles() : 1;
4846
+ const current = this.multiple() ? this._entries() : [];
4847
+ let validCount = current.filter(e => !e.code).length;
4848
+ const added = [];
4849
+ for (const file of this.multiple() ? incoming : incoming.slice(0, 1)) {
4850
+ // Repetidos (ya en la lista o dos veces en la misma tanda): se ignoran
4851
+ if ([...current, ...added].some(e => isSameFile(e.item.file, file)))
4852
+ continue;
4853
+ let code;
4854
+ if (!matchesAccept(file, patterns))
4855
+ code = 'TYPE_NOT_ALLOWED';
4856
+ else if (maxSize != null && file.size > maxSize)
4857
+ code = 'SIZE_EXCEEDED';
4858
+ else if (max != null && validCount >= max)
4859
+ code = 'MAX_FILES_EXCEEDED';
4860
+ else
4861
+ validCount++;
4862
+ added.push({ item: this._toItem(file, code), code });
4836
4863
  }
4837
- const newItems = incoming.map(f => this._validateAndCreate(f));
4838
- if (this.multiple()) {
4839
- this.items.update(list => [...list, ...newItems]);
4864
+ if (!added.length)
4865
+ return;
4866
+ if (!this.multiple())
4867
+ this._entries().forEach(({ item }) => this._revokePreview(item));
4868
+ this._entries.set([...current, ...added]);
4869
+ const rejected = added.filter(e => e.code);
4870
+ for (const { item, code } of rejected)
4871
+ this.validationError.emit({ file: item.file, code: code, message: item.error });
4872
+ const valid = added.filter(e => !e.code).map(e => e.item);
4873
+ if (valid.length) {
4874
+ this.filesSelected.emit(valid);
4875
+ this.announcement.set(this.texts().fileInput.added(valid.length));
4840
4876
  }
4841
4877
  else {
4842
- this.items.set(newItems);
4843
- }
4844
- this._generatePreviews(newItems);
4845
- this._emitChange();
4846
- const validCount = newItems.filter(i => i.status === 'valid').length;
4847
- if (validCount > 0) {
4848
- this.filesSelected.emit(newItems.filter(i => i.status === 'valid'));
4849
- this.liveAnnouncement.set(this.texts().fileInput.added(validCount));
4878
+ this.announcement.set(rejected.map(e => `${e.item.name}: ${e.item.error}`).join('. '));
4850
4879
  }
4880
+ this._commit();
4851
4881
  }
4852
- _validateAndCreate(file) {
4853
- const item = this._toFileItem(file);
4854
- // Type validation
4855
- if (this.accept() !== '*' && !this._isTypeAllowed(file)) {
4856
- item.status = 'error';
4857
- item.error = this.texts().fileInput.typeNotAllowed(file.type || file.name.split('.').pop() || '');
4858
- this.validationError.emit({
4859
- file,
4860
- code: 'TYPE_NOT_ALLOWED',
4861
- message: item.error,
4862
- });
4863
- return item;
4864
- }
4865
- // Size validation
4866
- const maxSz = this.maxSize();
4867
- if (maxSz !== null && file.size > maxSz) {
4868
- item.status = 'error';
4869
- item.error = this.texts().fileInput.tooLarge(this.formatSize(maxSz));
4870
- this.validationError.emit({
4871
- file,
4872
- code: 'SIZE_EXCEEDED',
4873
- message: item.error,
4874
- });
4875
- return item;
4876
- }
4877
- return item;
4882
+ /** Publica los ficheros válidos: model, formulario y estado de tocado. */
4883
+ _commit() {
4884
+ const files = this._validFiles();
4885
+ this.files.set(files);
4886
+ this._onChange(files);
4887
+ this._markTouched();
4878
4888
  }
4879
- _toFileItem(file) {
4880
- return {
4889
+ /** Lista nueva a partir de un valor externo (writeValue o [files]); los rechazados se descartan. */
4890
+ _resetFrom(files) {
4891
+ const previous = this._entries();
4892
+ previous.filter(e => !files.includes(e.item.file)).forEach(({ item }) => this._revokePreview(item));
4893
+ this._entries.set(files.map(file => previous.find(e => e.item.file === file && !e.code) ?? { item: this._toItem(file) }));
4894
+ }
4895
+ _toItem(file, code) {
4896
+ const item = {
4881
4897
  file,
4882
- id: this._generateId(),
4898
+ id: `${this.id}-file-${nextId++}`,
4883
4899
  name: file.name,
4884
4900
  size: file.size,
4885
4901
  type: file.type,
4886
- status: 'valid',
4902
+ status: code ? 'error' : 'valid',
4887
4903
  };
4904
+ if (code)
4905
+ item.error = this._message(file, code);
4906
+ // URL temporal en vez de leer la imagen entera a base64; se libera al quitarla
4907
+ else if (this._isBrowser && file.type.startsWith('image/'))
4908
+ item.preview = URL.createObjectURL(file);
4909
+ return item;
4888
4910
  }
4889
- _isTypeAllowed(file) {
4890
- const accept = this.accept();
4891
- const parts = accept.split(',').map(p => p.trim().toLowerCase());
4892
- const fileType = file.type.toLowerCase();
4893
- const fileName = file.name.toLowerCase();
4894
- return parts.some(pattern => {
4895
- // Extension pattern like .pdf
4896
- if (pattern.startsWith('.')) {
4897
- return fileName.endsWith(pattern);
4898
- }
4899
- // Wildcard MIME like image/*
4900
- if (pattern.endsWith('/*')) {
4901
- const prefix = pattern.slice(0, -2);
4902
- return fileType.startsWith(prefix + '/');
4903
- }
4904
- // Exact MIME
4905
- return fileType === pattern;
4906
- });
4911
+ _revokePreview(item) {
4912
+ if (item.preview?.startsWith('blob:'))
4913
+ URL.revokeObjectURL(item.preview);
4907
4914
  }
4908
- _generatePreviews(items) {
4909
- if (!isPlatformBrowser(this._platform))
4910
- return;
4911
- for (const item of items) {
4912
- if (item.file.type.startsWith('image/') && item.status === 'valid') {
4913
- const reader = new FileReader();
4914
- reader.onload = () => {
4915
- this.items.update(list => list.map(i => i.id === item.id ? { ...i, preview: reader.result } : i));
4916
- };
4917
- reader.readAsDataURL(item.file);
4915
+ _message(file, code) {
4916
+ const texts = this.texts().fileInput;
4917
+ switch (code) {
4918
+ case 'TYPE_NOT_ALLOWED': {
4919
+ // La extensión («.exe») se entiende mejor que el MIME («application/x-msdownload»)
4920
+ const dot = file.name.lastIndexOf('.');
4921
+ return texts.typeNotAllowed(dot > 0 ? file.name.slice(dot).toLowerCase() : file.type || file.name);
4918
4922
  }
4923
+ case 'SIZE_EXCEEDED':
4924
+ return texts.tooLarge(formatFileSize(this.maxSize() ?? 0, this._locale));
4925
+ case 'MAX_FILES_EXCEEDED':
4926
+ return texts.maxFiles(this.maxFiles() ?? 1);
4919
4927
  }
4920
4928
  }
4921
- _emitChange() {
4922
- this._onChange(this.validFiles());
4929
+ _bindControl(control) {
4930
+ this._controlSub?.unsubscribe();
4931
+ const submit$ = this._parentForm?.ngSubmit ?? EMPTY;
4932
+ this._controlSub = merge(control.events, submit$).subscribe(() => this._syncFormState());
4933
+ this._syncFormState();
4934
+ }
4935
+ _syncFormState() {
4936
+ const control = this._ngControl?.control;
4937
+ // Signals con el mismo valor no repintan: se puede llamar en cada ngDoCheck
4938
+ this.isRequired.set(control?.hasValidator(Validators.required) ?? false);
4939
+ this.showError.set(!!control && control.invalid && (control.touched || !!this._parentForm?.submitted));
4940
+ }
4941
+ _markTouched() {
4942
+ this._onTouched();
4943
+ this._syncFormState();
4923
4944
  }
4924
4945
  _flashDropConfirm() {
4925
4946
  this.showDropConfirm.set(true);
4926
- setTimeout(() => this.showDropConfirm.set(false), 450);
4927
- }
4928
- _generateId() {
4929
- if (isPlatformBrowser(this._platform) && typeof crypto !== 'undefined' && crypto.randomUUID) {
4930
- return crypto.randomUUID();
4931
- }
4932
- return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
4947
+ clearTimeout(this._confirmTimer);
4948
+ this._confirmTimer = setTimeout(() => this.showDropConfirm.set(false), 450);
4933
4949
  }
4934
4950
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxFileInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
4935
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: NgxFileInput, isStandalone: true, selector: "ngx-file-input", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesSelected: "filesSelected", fileRemoved: "fileRemoved", validationError: "validationError" }, host: { properties: { "class.ngx-fi--disabled": "isDisabled()", "class.ngx-fi--default": "variant() === 'default'", "class.ngx-fi--compact": "variant() === 'compact'", "class.ngx-fi--button": "variant() === 'button'" }, classAttribute: "ngx-file-input" }, providers: [
4936
- {
4937
- provide: NG_VALUE_ACCESSOR,
4938
- useExisting: forwardRef(() => NgxFileInput),
4939
- multi: true,
4940
- },
4941
- ], viewQueries: [{ propertyName: "_nativeInput", first: true, predicate: ["nativeInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Hidden native file input \u2014 always in DOM for accessibility -->\r\n<input\r\n #nativeInput\r\n [id]=\"inputId\"\r\n class=\"ngx-fi-native-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n [disabled]=\"isDisabled()\"\r\n (change)=\"onFileInputChange($event)\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n/>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: DEFAULT \u2014 large drop zone\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'default') {\r\n <div\r\n class=\"ngx-fi-zone\"\r\n [class.ngx-fi-zone--dragover]=\"isDragOver()\"\r\n [class.ngx-fi-zone--disabled]=\"isDisabled()\"\r\n role=\"region\"\r\n [attr.aria-label]=\"resolvedLabel()\"\r\n [attr.aria-disabled]=\"isDisabled()\"\r\n tabindex=\"0\"\r\n (click)=\"openFilePicker()\"\r\n (keydown.enter)=\"openFilePicker()\"\r\n (keydown.space)=\"openFilePicker(); $event.preventDefault()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n >\r\n <mat-icon class=\"ngx-fi-zone__icon\">cloud_upload</mat-icon>\r\n <span class=\"ngx-fi-zone__label\">{{ resolvedLabel() }}</span>\r\n @if (hint()) {\r\n <span class=\"ngx-fi-zone__hint\">{{ hint() }}</span>\r\n }\r\n <span class=\"ngx-fi-zone__btn-text\">{{ texts().fileInput.browse }}</span>\r\n\r\n <!-- Drop confirmation flash -->\r\n @if (showDropConfirm()) {\r\n <div class=\"ngx-fi-zone__confirm\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: COMPACT \u2014 horizontal bar\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'compact') {\r\n <div\r\n class=\"ngx-fi-compact\"\r\n [class.ngx-fi-compact--dragover]=\"isDragOver()\"\r\n [class.ngx-fi-compact--disabled]=\"isDisabled()\"\r\n role=\"region\"\r\n [attr.aria-label]=\"resolvedLabel()\"\r\n [attr.aria-disabled]=\"isDisabled()\"\r\n tabindex=\"0\"\r\n (click)=\"openFilePicker()\"\r\n (keydown.enter)=\"openFilePicker()\"\r\n (keydown.space)=\"openFilePicker(); $event.preventDefault()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n >\r\n <mat-icon class=\"ngx-fi-compact__icon\">cloud_upload</mat-icon>\r\n <div class=\"ngx-fi-compact__text\">\r\n <span class=\"ngx-fi-compact__label\">{{ resolvedLabel() }}</span>\r\n @if (hint()) {\r\n <span class=\"ngx-fi-compact__hint\">{{ hint() }}</span>\r\n }\r\n </div>\r\n\r\n @if (showDropConfirm()) {\r\n <div class=\"ngx-fi-zone__confirm\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: BUTTON \u2014 only a button\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'button') {\r\n <div\r\n class=\"ngx-fi-button-wrap\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [class.ngx-fi-button-wrap--dragover]=\"isDragOver()\"\r\n >\r\n <button\r\n mat-raised-button\r\n color=\"primary\"\r\n [disabled]=\"isDisabled()\"\r\n (click)=\"openFilePicker()\"\r\n type=\"button\"\r\n >\r\n <mat-icon>attach_file</mat-icon>\r\n {{ resolvedLabel() }}\r\n </button>\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n FILE PREVIEW LIST\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (showPreview() && items().length > 0) {\r\n <div class=\"ngx-fi-list\">\r\n @for (item of items(); track item.id) {\r\n <div\r\n class=\"ngx-fi-item\"\r\n [class.ngx-fi-item--error]=\"item.status === 'error'\"\r\n >\r\n <!-- Thumbnail / Icon -->\r\n <div class=\"ngx-fi-item__thumb\" [class]=\"getFileColorClass(item.type)\">\r\n @if (item.preview) {\r\n <img [src]=\"item.preview\" [alt]=\"item.name\" />\r\n } @else {\r\n <mat-icon>{{ getFileIcon(item.type) }}</mat-icon>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"ngx-fi-item__info\">\r\n <span class=\"ngx-fi-item__name\" [matTooltip]=\"item.name\">{{ item.name }}</span>\r\n <span class=\"ngx-fi-item__meta\">\r\n {{ formatSize(item.size) }}\r\n @if (item.error) {\r\n <span class=\"ngx-fi-item__error\">\r\n <mat-icon>warning</mat-icon> {{ item.error }}\r\n </span>\r\n }\r\n </span>\r\n </div>\r\n\r\n <!-- Remove -->\r\n <button\r\n class=\"ngx-fi-item__remove\"\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"removeFile(item); $event.stopPropagation()\"\r\n [matTooltip]=\"texts().fileInput.remove(item.name)\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- Screen reader announcements -->\r\n<div class=\"ngx-fi-sr-only\" aria-live=\"polite\">{{ liveAnnouncement() }}</div>\r\n", styles: [".ngx-fi-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.ngx-fi-native-input{position:absolute;width:0;height:0;opacity:0;pointer-events:none}.ngx-fi-zone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:180px;padding:2rem 1.5rem;border:2px dashed var(--mat-sys-outline-variant, #c4c4c4);border-radius:12px;background:var(--mat-sys-surface-container-low, #fafafa);cursor:pointer;transition:border-color .2s,background-color .2s,box-shadow .2s;outline:none;-webkit-user-select:none;user-select:none;overflow:hidden}.ngx-fi-zone:hover:not(.ngx-fi-zone--disabled){border-style:solid;background:var(--mat-sys-surface-container, #f0f0f0)}.ngx-fi-zone:focus-visible:not(.ngx-fi-zone--disabled){box-shadow:0 0 0 3px #5c6bc040;border-color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-zone--dragover{border-color:var(--mat-sys-primary, #5c6bc0);border-style:solid;background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 16px #5c6bc033;animation:ngx-fi-glow 1s ease-in-out infinite}.ngx-fi-zone--dragover .ngx-fi-zone__icon{transform:scale(1.15);color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-zone--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.ngx-fi-zone__icon{font-size:48px;width:48px;height:48px;color:var(--mat-sys-on-surface-variant, #666);transition:transform .2s,color .2s}.ngx-fi-zone__label{font-size:1.05rem;font-weight:500;color:var(--mat-sys-on-surface, #333);text-align:center}.ngx-fi-zone__hint{font-size:.82rem;color:var(--mat-sys-on-surface-variant, #666);text-align:center}.ngx-fi-zone__btn-text{font-size:.82rem;color:var(--mat-sys-primary, #5c6bc0);font-weight:500;margin-top:.25rem}.ngx-fi-zone__confirm{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#43a0471f;pointer-events:none;animation:ngx-fi-confirm-flash .4s ease-out forwards}.ngx-fi-zone__confirm mat-icon{font-size:56px;width:56px;height:56px;color:var(--mat-sys-tertiary, #43a047)}.ngx-fi-compact{position:relative;display:flex;align-items:center;gap:1rem;height:80px;padding:0 1.25rem;border:2px dashed var(--mat-sys-outline-variant, #c4c4c4);border-radius:12px;background:var(--mat-sys-surface-container-low, #fafafa);cursor:pointer;transition:border-color .2s,background-color .2s,box-shadow .2s;outline:none;-webkit-user-select:none;user-select:none;overflow:hidden}.ngx-fi-compact:hover:not(.ngx-fi-compact--disabled){border-style:solid;background:var(--mat-sys-surface-container, #f0f0f0)}.ngx-fi-compact:focus-visible:not(.ngx-fi-compact--disabled){box-shadow:0 0 0 3px #5c6bc040;border-color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-compact--dragover{border-color:var(--mat-sys-primary, #5c6bc0);border-style:solid;background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 16px #5c6bc033;animation:ngx-fi-glow 1s ease-in-out infinite}.ngx-fi-compact--dragover .ngx-fi-compact__icon{transform:scale(1.1);color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-compact--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.ngx-fi-compact__icon{font-size:32px;width:32px;height:32px;color:var(--mat-sys-on-surface-variant, #666);flex-shrink:0;transition:transform .2s,color .2s}.ngx-fi-compact__text{display:flex;flex-direction:column;gap:.15rem;min-width:0}.ngx-fi-compact__label{font-size:.95rem;font-weight:500;color:var(--mat-sys-on-surface, #333)}.ngx-fi-compact__hint{font-size:.78rem;color:var(--mat-sys-on-surface-variant, #666)}.ngx-fi-button-wrap{display:inline-block;position:relative;padding:4px;border-radius:8px;transition:background-color .2s,box-shadow .2s}.ngx-fi-button-wrap--dragover{background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:0 0 12px #5c6bc040}.ngx-fi-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.75rem}.ngx-fi-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .65rem;border-radius:10px;background:var(--mat-sys-surface-container-low, #fafafa);border:1px solid var(--mat-sys-outline-variant, #e0e0e0);transition:border-color .2s,background-color .2s;animation:ngx-fi-item-enter .2s ease-out both}.ngx-fi-item--error{border-color:var(--mat-sys-error, #d32f2f);background:#d32f2f0a}.ngx-fi-item__thumb{width:42px;height:42px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden}.ngx-fi-item__thumb img{width:100%;height:100%;object-fit:cover;border-radius:8px}.ngx-fi-item__thumb mat-icon{font-size:24px;width:24px;height:24px}.ngx-fi-item__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.1rem}.ngx-fi-item__name{font-size:.88rem;font-weight:500;color:var(--mat-sys-on-surface, #333);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngx-fi-item__meta{font-size:.75rem;color:var(--mat-sys-on-surface-variant, #666);display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.ngx-fi-item__error{color:var(--mat-sys-error, #d32f2f);display:inline-flex;align-items:center;gap:.2rem;font-weight:500}.ngx-fi-item__error mat-icon{font-size:14px;width:14px;height:14px}.ngx-fi-item__remove{flex-shrink:0}.ngx-fi-icon--image{background:#5c6bc01a}.ngx-fi-icon--image mat-icon{color:#5c6bc0}.ngx-fi-icon--pdf{background:#d32f2f1a}.ngx-fi-icon--pdf mat-icon{color:#d32f2f}.ngx-fi-icon--video{background:#1e88e51a}.ngx-fi-icon--video mat-icon{color:#1e88e5}.ngx-fi-icon--audio{background:#8e24aa1a}.ngx-fi-icon--audio mat-icon{color:#8e24aa}.ngx-fi-icon--generic{background:#0000000f}.ngx-fi-icon--generic mat-icon{color:#757575}@keyframes ngx-fi-item-enter{0%{opacity:0;transform:translateY(8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes ngx-fi-confirm-flash{0%{opacity:1}to{opacity:0}}.ngx-fi-item:nth-child(1){animation-delay:0ms}.ngx-fi-item:nth-child(2){animation-delay:50ms}.ngx-fi-item:nth-child(3){animation-delay:.1s}.ngx-fi-item:nth-child(4){animation-delay:.15s}.ngx-fi-item:nth-child(5){animation-delay:.2s}.ngx-fi-item:nth-child(6){animation-delay:.25s}.ngx-fi-item:nth-child(7){animation-delay:.3s}.ngx-fi-item:nth-child(8){animation-delay:.35s}.ngx-fi-item:nth-child(9){animation-delay:.4s}.ngx-fi-item:nth-child(10){animation-delay:.45s}.ngx-fi-item:nth-child(11){animation-delay:.5s}.ngx-fi-item:nth-child(12){animation-delay:.55s}.ngx-fi-item:nth-child(13){animation-delay:.6s}.ngx-fi-item:nth-child(14){animation-delay:.65s}.ngx-fi-item:nth-child(15){animation-delay:.7s}.ngx-fi-item:nth-child(16){animation-delay:.75s}.ngx-fi-item:nth-child(17){animation-delay:.8s}.ngx-fi-item:nth-child(18){animation-delay:.85s}.ngx-fi-item:nth-child(19){animation-delay:.9s}.ngx-fi-item:nth-child(20){animation-delay:.95s}@keyframes ngx-fi-glow{0%,to{box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 12px #5c6bc02e}50%{box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 22px #5c6bc059}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], encapsulation: i0.ViewEncapsulation.None });
4951
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: NgxFileInput, isStandalone: true, selector: "ngx-file-input", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { files: "filesChange", filesSelected: "filesSelected", fileRemoved: "fileRemoved", validationError: "validationError" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "class.ngx-fi--default": "variant() === 'default'", "class.ngx-fi--compact": "variant() === 'compact'", "class.ngx-fi--button": "variant() === 'button'", "class.ngx-fi--disabled": "isDisabled()", "class.ngx-fi--dragover": "isDragOver()", "class.ngx-fi--invalid": "showError()" }, classAttribute: "ngx-file-input" }, viewQueries: [{ propertyName: "_nativeInput", first: true, predicate: ["nativeInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Input nativo oculto: lo abre la zona o el bot\u00F3n -->\n<input\n #nativeInput\n class=\"ngx-fi__native\"\n type=\"file\"\n hidden\n tabindex=\"-1\"\n [accept]=\"acceptAttr()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onNativeChange($event)\"\n (cancel)=\"onZoneBlur()\" />\n\n@switch (variant()) {\n @case ('button') {\n <button\n matButton=\"filled\"\n type=\"button\"\n class=\"ngx-fi__button\"\n [disabled]=\"isDisabled()\"\n [attr.aria-describedby]=\"describedBy()\"\n (click)=\"openFilePicker()\"\n (blur)=\"onZoneBlur()\">\n <mat-icon>attach_file</mat-icon>\n {{ resolvedLabel() }}@if (isRequired()) {<span class=\"ngx-fi__required\" aria-hidden=\"true\">*</span>}\n </button>\n @if (hint()) {\n <span class=\"ngx-fi__hint\" [id]=\"id + '-hint'\">{{ hint() }}</span>\n }\n }\n\n @default {\n <!-- Zona de arrastre: es un bot\u00F3n (abre el selector) con la etiqueta como nombre accesible -->\n <div\n class=\"ngx-fi__zone\"\n [class.ngx-fi__zone--compact]=\"variant() === 'compact'\"\n role=\"button\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-describedby]=\"describedBy()\"\n (click)=\"openFilePicker()\"\n (keydown)=\"onZoneKeydown($event)\"\n (blur)=\"onZoneBlur()\">\n <mat-icon class=\"ngx-fi__zone-icon\" aria-hidden=\"true\">cloud_upload</mat-icon>\n\n <span class=\"ngx-fi__zone-text\">\n <span class=\"ngx-fi__label\">\n {{ resolvedLabel() }}@if (isRequired()) {<span class=\"ngx-fi__required\" aria-hidden=\"true\">*</span>}\n </span>\n @if (hint()) {\n <span class=\"ngx-fi__hint\" [id]=\"id + '-hint'\">{{ hint() }}</span>\n }\n @if (variant() === 'default') {\n <span class=\"ngx-fi__browse\">{{ texts().fileInput.browse }}</span>\n }\n </span>\n\n @if (showDropConfirm()) {\n <span class=\"ngx-fi__confirm\" aria-hidden=\"true\"><mat-icon>check_circle</mat-icon></span>\n }\n </div>\n }\n}\n\n@if (showError() && error()) {\n <span class=\"ngx-fi__error\" [id]=\"id + '-error'\">{{ error() }}</span>\n}\n\n@if (showPreview() && views().length) {\n <ul class=\"ngx-fi__list\">\n @for (view of views(); track view.item.id) {\n <li class=\"ngx-fi__item\" [class.ngx-fi__item--error]=\"view.error\">\n <span class=\"ngx-fi__thumb\" [attr.data-kind]=\"view.kind\">\n @if (view.item.preview) {\n <img [src]=\"view.item.preview\" alt=\"\" />\n } @else {\n <mat-icon aria-hidden=\"true\">{{ view.icon }}</mat-icon>\n }\n </span>\n\n <span class=\"ngx-fi__info\">\n <span class=\"ngx-fi__name\" [title]=\"view.item.name\">{{ view.item.name }}</span>\n <span class=\"ngx-fi__meta\">\n {{ view.size }}\n @if (view.error) {\n <span class=\"ngx-fi__item-error\"><mat-icon aria-hidden=\"true\">error</mat-icon>{{ view.error }}</span>\n }\n </span>\n </span>\n\n <button\n matIconButton\n type=\"button\"\n class=\"ngx-fi__remove\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"texts().fileInput.remove(view.item.name)\"\n [matTooltip]=\"texts().fileInput.remove(view.item.name)\"\n (click)=\"removeFile(view.item)\">\n <mat-icon>close</mat-icon>\n </button>\n </li>\n }\n </ul>\n}\n\n<!-- Regi\u00F3n viva fija: a\u00F1adidos, quitados y rechazados -->\n<div class=\"ngx-fi__live\" aria-live=\"polite\">{{ announcement() }}</div>\n", styles: [":host{--ngx-file-input-image-color: var(--mat-sys-primary);--ngx-file-input-pdf-color: var(--mat-sys-error);--ngx-file-input-video-color: var(--mat-sys-tertiary);--ngx-file-input-audio-color: var(--mat-sys-secondary);--ngx-file-input-generic-color: var(--mat-sys-on-surface-variant);display:flex;flex-direction:column;gap:8px}:host(.ngx-fi--button){display:inline-flex;align-items:flex-start}.ngx-fi__zone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;min-height:180px;padding:32px 24px;border:2px dashed var(--mat-sys-outline-variant);border-radius:var(--mat-sys-corner-large, 16px);background:var(--mat-sys-surface-container-low);color:var(--mat-sys-on-surface);text-align:center;cursor:pointer;-webkit-user-select:none;user-select:none;overflow:hidden;transition:border-color .15s,background-color .15s}.ngx-fi__zone:hover{border-color:var(--mat-sys-outline);background:var(--mat-sys-surface-container)}.ngx-fi__zone:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:2px}.ngx-fi__zone--compact{flex-direction:row;justify-content:flex-start;gap:16px;min-height:0;padding:16px 20px;border-radius:var(--mat-sys-corner-medium, 12px);text-align:start}.ngx-fi__zone--compact .ngx-fi__zone-icon{width:32px;height:32px;font-size:32px}.ngx-fi__zone--compact .ngx-fi__zone-text{align-items:flex-start}.ngx-fi__zone-icon{flex:none;width:48px;height:48px;font-size:48px;color:var(--mat-sys-on-surface-variant);transition:transform .15s,color .15s}.ngx-fi__zone-text{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:0}.ngx-fi__label{font:var(--mat-sys-title-medium)}.ngx-fi__required{margin-inline-start:2px}.ngx-fi__hint{font:var(--mat-sys-body-small);color:var(--mat-sys-on-surface-variant)}.ngx-fi__browse{margin-top:4px;font:var(--mat-sys-label-large);color:var(--mat-sys-primary)}:host(.ngx-fi--dragover) .ngx-fi__zone{border-style:solid;border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,var(--mat-sys-surface-container-low))}:host(.ngx-fi--dragover) .ngx-fi__zone .ngx-fi__zone-icon{color:var(--mat-sys-primary);transform:scale(1.1)}:host(.ngx-fi--dragover) .ngx-fi__button{outline:2px dashed var(--mat-sys-primary);outline-offset:3px}:host(.ngx-fi--invalid) .ngx-fi__zone{border-color:var(--mat-sys-error)}:host(.ngx-fi--invalid) .ngx-fi__zone .ngx-fi__label,:host(.ngx-fi--invalid) .ngx-fi__zone .ngx-fi__zone-icon{color:var(--mat-sys-error)}:host(.ngx-fi--disabled) .ngx-fi__zone{cursor:default;border-color:color-mix(in srgb,var(--mat-sys-on-surface) 12%,transparent);background:transparent;color:color-mix(in srgb,var(--mat-sys-on-surface) 38%,transparent)}:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__zone-icon,:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__hint,:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__browse{color:inherit}.ngx-fi__confirm{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--mat-sys-primary) 12%,transparent);pointer-events:none;animation:ngx-fi-flash .45s ease-out forwards}.ngx-fi__confirm mat-icon{width:56px;height:56px;font-size:56px;color:var(--mat-sys-primary)}.ngx-fi__error{padding-inline:4px;font:var(--mat-sys-body-small);color:var(--mat-sys-error)}.ngx-fi__list{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;list-style:none}:host(.ngx-fi--button) .ngx-fi__list{align-self:stretch}.ngx-fi__item{display:flex;align-items:center;gap:12px;padding:8px 4px 8px 8px;border:1px solid var(--mat-sys-outline-variant);border-radius:var(--mat-sys-corner-medium, 12px);background:var(--mat-sys-surface-container-lowest);animation:ngx-fi-enter .2s ease-out both}.ngx-fi__item--error{border-color:color-mix(in srgb,var(--mat-sys-error) 40%,transparent);background:color-mix(in srgb,var(--mat-sys-error) 6%,var(--mat-sys-surface-container-lowest))}.ngx-fi__thumb{--ngx-fi-kind-color: var(--ngx-file-input-generic-color);flex:none;display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--mat-sys-corner-small, 8px);background:color-mix(in srgb,var(--ngx-fi-kind-color) 12%,var(--mat-sys-surface));overflow:hidden}.ngx-fi__thumb[data-kind=image]{--ngx-fi-kind-color: var(--ngx-file-input-image-color)}.ngx-fi__thumb[data-kind=pdf]{--ngx-fi-kind-color: var(--ngx-file-input-pdf-color)}.ngx-fi__thumb[data-kind=video]{--ngx-fi-kind-color: var(--ngx-file-input-video-color)}.ngx-fi__thumb[data-kind=audio]{--ngx-fi-kind-color: var(--ngx-file-input-audio-color)}.ngx-fi__thumb mat-icon{color:var(--ngx-fi-kind-color)}.ngx-fi__thumb img{width:100%;height:100%;object-fit:cover}.ngx-fi__info{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0}.ngx-fi__name{overflow:hidden;font:var(--mat-sys-body-medium);color:var(--mat-sys-on-surface);text-overflow:ellipsis;white-space:nowrap}.ngx-fi__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;font:var(--mat-sys-body-small);color:var(--mat-sys-on-surface-variant)}.ngx-fi__item-error{display:inline-flex;align-items:center;gap:4px;color:var(--mat-sys-error)}.ngx-fi__item-error mat-icon{width:16px;height:16px;font-size:16px}.ngx-fi__remove{flex:none}.ngx-fi__live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}@keyframes ngx-fi-enter{0%{opacity:0;transform:translateY(-4px)}}@keyframes ngx-fi-flash{0%{opacity:1}to{opacity:0}}@media (prefers-reduced-motion: reduce){.ngx-fi__item,.ngx-fi__confirm{animation:none}.ngx-fi__zone,.ngx-fi__zone-icon{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4942
4952
  }
4943
4953
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NgxFileInput, decorators: [{
4944
4954
  type: Component,
4945
- args: [{ selector: 'ngx-file-input', imports: [MatButtonModule, MatIconModule, MatTooltipModule], encapsulation: ViewEncapsulation.None, providers: [
4946
- {
4947
- provide: NG_VALUE_ACCESSOR,
4948
- useExisting: forwardRef(() => NgxFileInput),
4949
- multi: true,
4950
- },
4951
- ], host: {
4952
- 'class': 'ngx-file-input',
4953
- '[class.ngx-fi--disabled]': 'isDisabled()',
4955
+ args: [{ selector: 'ngx-file-input', imports: [MatButtonModule, MatIconModule, MatTooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
4956
+ class: 'ngx-file-input',
4954
4957
  '[class.ngx-fi--default]': "variant() === 'default'",
4955
4958
  '[class.ngx-fi--compact]': "variant() === 'compact'",
4956
4959
  '[class.ngx-fi--button]': "variant() === 'button'",
4957
- }, template: "<!-- Hidden native file input \u2014 always in DOM for accessibility -->\r\n<input\r\n #nativeInput\r\n [id]=\"inputId\"\r\n class=\"ngx-fi-native-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n [disabled]=\"isDisabled()\"\r\n (change)=\"onFileInputChange($event)\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n/>\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: DEFAULT \u2014 large drop zone\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'default') {\r\n <div\r\n class=\"ngx-fi-zone\"\r\n [class.ngx-fi-zone--dragover]=\"isDragOver()\"\r\n [class.ngx-fi-zone--disabled]=\"isDisabled()\"\r\n role=\"region\"\r\n [attr.aria-label]=\"resolvedLabel()\"\r\n [attr.aria-disabled]=\"isDisabled()\"\r\n tabindex=\"0\"\r\n (click)=\"openFilePicker()\"\r\n (keydown.enter)=\"openFilePicker()\"\r\n (keydown.space)=\"openFilePicker(); $event.preventDefault()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n >\r\n <mat-icon class=\"ngx-fi-zone__icon\">cloud_upload</mat-icon>\r\n <span class=\"ngx-fi-zone__label\">{{ resolvedLabel() }}</span>\r\n @if (hint()) {\r\n <span class=\"ngx-fi-zone__hint\">{{ hint() }}</span>\r\n }\r\n <span class=\"ngx-fi-zone__btn-text\">{{ texts().fileInput.browse }}</span>\r\n\r\n <!-- Drop confirmation flash -->\r\n @if (showDropConfirm()) {\r\n <div class=\"ngx-fi-zone__confirm\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: COMPACT \u2014 horizontal bar\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'compact') {\r\n <div\r\n class=\"ngx-fi-compact\"\r\n [class.ngx-fi-compact--dragover]=\"isDragOver()\"\r\n [class.ngx-fi-compact--disabled]=\"isDisabled()\"\r\n role=\"region\"\r\n [attr.aria-label]=\"resolvedLabel()\"\r\n [attr.aria-disabled]=\"isDisabled()\"\r\n tabindex=\"0\"\r\n (click)=\"openFilePicker()\"\r\n (keydown.enter)=\"openFilePicker()\"\r\n (keydown.space)=\"openFilePicker(); $event.preventDefault()\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n >\r\n <mat-icon class=\"ngx-fi-compact__icon\">cloud_upload</mat-icon>\r\n <div class=\"ngx-fi-compact__text\">\r\n <span class=\"ngx-fi-compact__label\">{{ resolvedLabel() }}</span>\r\n @if (hint()) {\r\n <span class=\"ngx-fi-compact__hint\">{{ hint() }}</span>\r\n }\r\n </div>\r\n\r\n @if (showDropConfirm()) {\r\n <div class=\"ngx-fi-zone__confirm\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n VARIANT: BUTTON \u2014 only a button\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (variant() === 'button') {\r\n <div\r\n class=\"ngx-fi-button-wrap\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n [class.ngx-fi-button-wrap--dragover]=\"isDragOver()\"\r\n >\r\n <button\r\n mat-raised-button\r\n color=\"primary\"\r\n [disabled]=\"isDisabled()\"\r\n (click)=\"openFilePicker()\"\r\n type=\"button\"\r\n >\r\n <mat-icon>attach_file</mat-icon>\r\n {{ resolvedLabel() }}\r\n </button>\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n FILE PREVIEW LIST\r\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (showPreview() && items().length > 0) {\r\n <div class=\"ngx-fi-list\">\r\n @for (item of items(); track item.id) {\r\n <div\r\n class=\"ngx-fi-item\"\r\n [class.ngx-fi-item--error]=\"item.status === 'error'\"\r\n >\r\n <!-- Thumbnail / Icon -->\r\n <div class=\"ngx-fi-item__thumb\" [class]=\"getFileColorClass(item.type)\">\r\n @if (item.preview) {\r\n <img [src]=\"item.preview\" [alt]=\"item.name\" />\r\n } @else {\r\n <mat-icon>{{ getFileIcon(item.type) }}</mat-icon>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"ngx-fi-item__info\">\r\n <span class=\"ngx-fi-item__name\" [matTooltip]=\"item.name\">{{ item.name }}</span>\r\n <span class=\"ngx-fi-item__meta\">\r\n {{ formatSize(item.size) }}\r\n @if (item.error) {\r\n <span class=\"ngx-fi-item__error\">\r\n <mat-icon>warning</mat-icon> {{ item.error }}\r\n </span>\r\n }\r\n </span>\r\n </div>\r\n\r\n <!-- Remove -->\r\n <button\r\n class=\"ngx-fi-item__remove\"\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"removeFile(item); $event.stopPropagation()\"\r\n [matTooltip]=\"texts().fileInput.remove(item.name)\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n}\r\n\r\n<!-- Screen reader announcements -->\r\n<div class=\"ngx-fi-sr-only\" aria-live=\"polite\">{{ liveAnnouncement() }}</div>\r\n", styles: [".ngx-fi-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.ngx-fi-native-input{position:absolute;width:0;height:0;opacity:0;pointer-events:none}.ngx-fi-zone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;min-height:180px;padding:2rem 1.5rem;border:2px dashed var(--mat-sys-outline-variant, #c4c4c4);border-radius:12px;background:var(--mat-sys-surface-container-low, #fafafa);cursor:pointer;transition:border-color .2s,background-color .2s,box-shadow .2s;outline:none;-webkit-user-select:none;user-select:none;overflow:hidden}.ngx-fi-zone:hover:not(.ngx-fi-zone--disabled){border-style:solid;background:var(--mat-sys-surface-container, #f0f0f0)}.ngx-fi-zone:focus-visible:not(.ngx-fi-zone--disabled){box-shadow:0 0 0 3px #5c6bc040;border-color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-zone--dragover{border-color:var(--mat-sys-primary, #5c6bc0);border-style:solid;background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 16px #5c6bc033;animation:ngx-fi-glow 1s ease-in-out infinite}.ngx-fi-zone--dragover .ngx-fi-zone__icon{transform:scale(1.15);color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-zone--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.ngx-fi-zone__icon{font-size:48px;width:48px;height:48px;color:var(--mat-sys-on-surface-variant, #666);transition:transform .2s,color .2s}.ngx-fi-zone__label{font-size:1.05rem;font-weight:500;color:var(--mat-sys-on-surface, #333);text-align:center}.ngx-fi-zone__hint{font-size:.82rem;color:var(--mat-sys-on-surface-variant, #666);text-align:center}.ngx-fi-zone__btn-text{font-size:.82rem;color:var(--mat-sys-primary, #5c6bc0);font-weight:500;margin-top:.25rem}.ngx-fi-zone__confirm{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#43a0471f;pointer-events:none;animation:ngx-fi-confirm-flash .4s ease-out forwards}.ngx-fi-zone__confirm mat-icon{font-size:56px;width:56px;height:56px;color:var(--mat-sys-tertiary, #43a047)}.ngx-fi-compact{position:relative;display:flex;align-items:center;gap:1rem;height:80px;padding:0 1.25rem;border:2px dashed var(--mat-sys-outline-variant, #c4c4c4);border-radius:12px;background:var(--mat-sys-surface-container-low, #fafafa);cursor:pointer;transition:border-color .2s,background-color .2s,box-shadow .2s;outline:none;-webkit-user-select:none;user-select:none;overflow:hidden}.ngx-fi-compact:hover:not(.ngx-fi-compact--disabled){border-style:solid;background:var(--mat-sys-surface-container, #f0f0f0)}.ngx-fi-compact:focus-visible:not(.ngx-fi-compact--disabled){box-shadow:0 0 0 3px #5c6bc040;border-color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-compact--dragover{border-color:var(--mat-sys-primary, #5c6bc0);border-style:solid;background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 16px #5c6bc033;animation:ngx-fi-glow 1s ease-in-out infinite}.ngx-fi-compact--dragover .ngx-fi-compact__icon{transform:scale(1.1);color:var(--mat-sys-primary, #5c6bc0)}.ngx-fi-compact--disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.ngx-fi-compact__icon{font-size:32px;width:32px;height:32px;color:var(--mat-sys-on-surface-variant, #666);flex-shrink:0;transition:transform .2s,color .2s}.ngx-fi-compact__text{display:flex;flex-direction:column;gap:.15rem;min-width:0}.ngx-fi-compact__label{font-size:.95rem;font-weight:500;color:var(--mat-sys-on-surface, #333)}.ngx-fi-compact__hint{font-size:.78rem;color:var(--mat-sys-on-surface-variant, #666)}.ngx-fi-button-wrap{display:inline-block;position:relative;padding:4px;border-radius:8px;transition:background-color .2s,box-shadow .2s}.ngx-fi-button-wrap--dragover{background:var(--mat-sys-primary-container, rgba(92, 107, 192, .08));box-shadow:0 0 12px #5c6bc040}.ngx-fi-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.75rem}.ngx-fi-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .65rem;border-radius:10px;background:var(--mat-sys-surface-container-low, #fafafa);border:1px solid var(--mat-sys-outline-variant, #e0e0e0);transition:border-color .2s,background-color .2s;animation:ngx-fi-item-enter .2s ease-out both}.ngx-fi-item--error{border-color:var(--mat-sys-error, #d32f2f);background:#d32f2f0a}.ngx-fi-item__thumb{width:42px;height:42px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden}.ngx-fi-item__thumb img{width:100%;height:100%;object-fit:cover;border-radius:8px}.ngx-fi-item__thumb mat-icon{font-size:24px;width:24px;height:24px}.ngx-fi-item__info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.1rem}.ngx-fi-item__name{font-size:.88rem;font-weight:500;color:var(--mat-sys-on-surface, #333);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngx-fi-item__meta{font-size:.75rem;color:var(--mat-sys-on-surface-variant, #666);display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.ngx-fi-item__error{color:var(--mat-sys-error, #d32f2f);display:inline-flex;align-items:center;gap:.2rem;font-weight:500}.ngx-fi-item__error mat-icon{font-size:14px;width:14px;height:14px}.ngx-fi-item__remove{flex-shrink:0}.ngx-fi-icon--image{background:#5c6bc01a}.ngx-fi-icon--image mat-icon{color:#5c6bc0}.ngx-fi-icon--pdf{background:#d32f2f1a}.ngx-fi-icon--pdf mat-icon{color:#d32f2f}.ngx-fi-icon--video{background:#1e88e51a}.ngx-fi-icon--video mat-icon{color:#1e88e5}.ngx-fi-icon--audio{background:#8e24aa1a}.ngx-fi-icon--audio mat-icon{color:#8e24aa}.ngx-fi-icon--generic{background:#0000000f}.ngx-fi-icon--generic mat-icon{color:#757575}@keyframes ngx-fi-item-enter{0%{opacity:0;transform:translateY(8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes ngx-fi-confirm-flash{0%{opacity:1}to{opacity:0}}.ngx-fi-item:nth-child(1){animation-delay:0ms}.ngx-fi-item:nth-child(2){animation-delay:50ms}.ngx-fi-item:nth-child(3){animation-delay:.1s}.ngx-fi-item:nth-child(4){animation-delay:.15s}.ngx-fi-item:nth-child(5){animation-delay:.2s}.ngx-fi-item:nth-child(6){animation-delay:.25s}.ngx-fi-item:nth-child(7){animation-delay:.3s}.ngx-fi-item:nth-child(8){animation-delay:.35s}.ngx-fi-item:nth-child(9){animation-delay:.4s}.ngx-fi-item:nth-child(10){animation-delay:.45s}.ngx-fi-item:nth-child(11){animation-delay:.5s}.ngx-fi-item:nth-child(12){animation-delay:.55s}.ngx-fi-item:nth-child(13){animation-delay:.6s}.ngx-fi-item:nth-child(14){animation-delay:.65s}.ngx-fi-item:nth-child(15){animation-delay:.7s}.ngx-fi-item:nth-child(16){animation-delay:.75s}.ngx-fi-item:nth-child(17){animation-delay:.8s}.ngx-fi-item:nth-child(18){animation-delay:.85s}.ngx-fi-item:nth-child(19){animation-delay:.9s}.ngx-fi-item:nth-child(20){animation-delay:.95s}@keyframes ngx-fi-glow{0%,to{box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 12px #5c6bc02e}50%{box-shadow:inset 0 0 0 2px var(--mat-sys-primary, #5c6bc0),0 0 22px #5c6bc059}}\n"] }]
4958
- }], propDecorators: { _nativeInput: [{ type: i0.ViewChild, args: ['nativeInput', { isSignal: true }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], validationError: [{ type: i0.Output, args: ["validationError"] }] } });
4960
+ '[class.ngx-fi--disabled]': 'isDisabled()',
4961
+ '[class.ngx-fi--dragover]': 'isDragOver()',
4962
+ '[class.ngx-fi--invalid]': 'showError()',
4963
+ '(dragover)': 'onDragOver($event)',
4964
+ '(dragleave)': 'onDragLeave($event)',
4965
+ '(drop)': 'onDrop($event)',
4966
+ }, template: "<!-- Input nativo oculto: lo abre la zona o el bot\u00F3n -->\n<input\n #nativeInput\n class=\"ngx-fi__native\"\n type=\"file\"\n hidden\n tabindex=\"-1\"\n [accept]=\"acceptAttr()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onNativeChange($event)\"\n (cancel)=\"onZoneBlur()\" />\n\n@switch (variant()) {\n @case ('button') {\n <button\n matButton=\"filled\"\n type=\"button\"\n class=\"ngx-fi__button\"\n [disabled]=\"isDisabled()\"\n [attr.aria-describedby]=\"describedBy()\"\n (click)=\"openFilePicker()\"\n (blur)=\"onZoneBlur()\">\n <mat-icon>attach_file</mat-icon>\n {{ resolvedLabel() }}@if (isRequired()) {<span class=\"ngx-fi__required\" aria-hidden=\"true\">*</span>}\n </button>\n @if (hint()) {\n <span class=\"ngx-fi__hint\" [id]=\"id + '-hint'\">{{ hint() }}</span>\n }\n }\n\n @default {\n <!-- Zona de arrastre: es un bot\u00F3n (abre el selector) con la etiqueta como nombre accesible -->\n <div\n class=\"ngx-fi__zone\"\n [class.ngx-fi__zone--compact]=\"variant() === 'compact'\"\n role=\"button\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-describedby]=\"describedBy()\"\n (click)=\"openFilePicker()\"\n (keydown)=\"onZoneKeydown($event)\"\n (blur)=\"onZoneBlur()\">\n <mat-icon class=\"ngx-fi__zone-icon\" aria-hidden=\"true\">cloud_upload</mat-icon>\n\n <span class=\"ngx-fi__zone-text\">\n <span class=\"ngx-fi__label\">\n {{ resolvedLabel() }}@if (isRequired()) {<span class=\"ngx-fi__required\" aria-hidden=\"true\">*</span>}\n </span>\n @if (hint()) {\n <span class=\"ngx-fi__hint\" [id]=\"id + '-hint'\">{{ hint() }}</span>\n }\n @if (variant() === 'default') {\n <span class=\"ngx-fi__browse\">{{ texts().fileInput.browse }}</span>\n }\n </span>\n\n @if (showDropConfirm()) {\n <span class=\"ngx-fi__confirm\" aria-hidden=\"true\"><mat-icon>check_circle</mat-icon></span>\n }\n </div>\n }\n}\n\n@if (showError() && error()) {\n <span class=\"ngx-fi__error\" [id]=\"id + '-error'\">{{ error() }}</span>\n}\n\n@if (showPreview() && views().length) {\n <ul class=\"ngx-fi__list\">\n @for (view of views(); track view.item.id) {\n <li class=\"ngx-fi__item\" [class.ngx-fi__item--error]=\"view.error\">\n <span class=\"ngx-fi__thumb\" [attr.data-kind]=\"view.kind\">\n @if (view.item.preview) {\n <img [src]=\"view.item.preview\" alt=\"\" />\n } @else {\n <mat-icon aria-hidden=\"true\">{{ view.icon }}</mat-icon>\n }\n </span>\n\n <span class=\"ngx-fi__info\">\n <span class=\"ngx-fi__name\" [title]=\"view.item.name\">{{ view.item.name }}</span>\n <span class=\"ngx-fi__meta\">\n {{ view.size }}\n @if (view.error) {\n <span class=\"ngx-fi__item-error\"><mat-icon aria-hidden=\"true\">error</mat-icon>{{ view.error }}</span>\n }\n </span>\n </span>\n\n <button\n matIconButton\n type=\"button\"\n class=\"ngx-fi__remove\"\n [disabled]=\"isDisabled()\"\n [attr.aria-label]=\"texts().fileInput.remove(view.item.name)\"\n [matTooltip]=\"texts().fileInput.remove(view.item.name)\"\n (click)=\"removeFile(view.item)\">\n <mat-icon>close</mat-icon>\n </button>\n </li>\n }\n </ul>\n}\n\n<!-- Regi\u00F3n viva fija: a\u00F1adidos, quitados y rechazados -->\n<div class=\"ngx-fi__live\" aria-live=\"polite\">{{ announcement() }}</div>\n", styles: [":host{--ngx-file-input-image-color: var(--mat-sys-primary);--ngx-file-input-pdf-color: var(--mat-sys-error);--ngx-file-input-video-color: var(--mat-sys-tertiary);--ngx-file-input-audio-color: var(--mat-sys-secondary);--ngx-file-input-generic-color: var(--mat-sys-on-surface-variant);display:flex;flex-direction:column;gap:8px}:host(.ngx-fi--button){display:inline-flex;align-items:flex-start}.ngx-fi__zone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;min-height:180px;padding:32px 24px;border:2px dashed var(--mat-sys-outline-variant);border-radius:var(--mat-sys-corner-large, 16px);background:var(--mat-sys-surface-container-low);color:var(--mat-sys-on-surface);text-align:center;cursor:pointer;-webkit-user-select:none;user-select:none;overflow:hidden;transition:border-color .15s,background-color .15s}.ngx-fi__zone:hover{border-color:var(--mat-sys-outline);background:var(--mat-sys-surface-container)}.ngx-fi__zone:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:2px}.ngx-fi__zone--compact{flex-direction:row;justify-content:flex-start;gap:16px;min-height:0;padding:16px 20px;border-radius:var(--mat-sys-corner-medium, 12px);text-align:start}.ngx-fi__zone--compact .ngx-fi__zone-icon{width:32px;height:32px;font-size:32px}.ngx-fi__zone--compact .ngx-fi__zone-text{align-items:flex-start}.ngx-fi__zone-icon{flex:none;width:48px;height:48px;font-size:48px;color:var(--mat-sys-on-surface-variant);transition:transform .15s,color .15s}.ngx-fi__zone-text{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:0}.ngx-fi__label{font:var(--mat-sys-title-medium)}.ngx-fi__required{margin-inline-start:2px}.ngx-fi__hint{font:var(--mat-sys-body-small);color:var(--mat-sys-on-surface-variant)}.ngx-fi__browse{margin-top:4px;font:var(--mat-sys-label-large);color:var(--mat-sys-primary)}:host(.ngx-fi--dragover) .ngx-fi__zone{border-style:solid;border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,var(--mat-sys-surface-container-low))}:host(.ngx-fi--dragover) .ngx-fi__zone .ngx-fi__zone-icon{color:var(--mat-sys-primary);transform:scale(1.1)}:host(.ngx-fi--dragover) .ngx-fi__button{outline:2px dashed var(--mat-sys-primary);outline-offset:3px}:host(.ngx-fi--invalid) .ngx-fi__zone{border-color:var(--mat-sys-error)}:host(.ngx-fi--invalid) .ngx-fi__zone .ngx-fi__label,:host(.ngx-fi--invalid) .ngx-fi__zone .ngx-fi__zone-icon{color:var(--mat-sys-error)}:host(.ngx-fi--disabled) .ngx-fi__zone{cursor:default;border-color:color-mix(in srgb,var(--mat-sys-on-surface) 12%,transparent);background:transparent;color:color-mix(in srgb,var(--mat-sys-on-surface) 38%,transparent)}:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__zone-icon,:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__hint,:host(.ngx-fi--disabled) .ngx-fi__zone .ngx-fi__browse{color:inherit}.ngx-fi__confirm{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--mat-sys-primary) 12%,transparent);pointer-events:none;animation:ngx-fi-flash .45s ease-out forwards}.ngx-fi__confirm mat-icon{width:56px;height:56px;font-size:56px;color:var(--mat-sys-primary)}.ngx-fi__error{padding-inline:4px;font:var(--mat-sys-body-small);color:var(--mat-sys-error)}.ngx-fi__list{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;list-style:none}:host(.ngx-fi--button) .ngx-fi__list{align-self:stretch}.ngx-fi__item{display:flex;align-items:center;gap:12px;padding:8px 4px 8px 8px;border:1px solid var(--mat-sys-outline-variant);border-radius:var(--mat-sys-corner-medium, 12px);background:var(--mat-sys-surface-container-lowest);animation:ngx-fi-enter .2s ease-out both}.ngx-fi__item--error{border-color:color-mix(in srgb,var(--mat-sys-error) 40%,transparent);background:color-mix(in srgb,var(--mat-sys-error) 6%,var(--mat-sys-surface-container-lowest))}.ngx-fi__thumb{--ngx-fi-kind-color: var(--ngx-file-input-generic-color);flex:none;display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--mat-sys-corner-small, 8px);background:color-mix(in srgb,var(--ngx-fi-kind-color) 12%,var(--mat-sys-surface));overflow:hidden}.ngx-fi__thumb[data-kind=image]{--ngx-fi-kind-color: var(--ngx-file-input-image-color)}.ngx-fi__thumb[data-kind=pdf]{--ngx-fi-kind-color: var(--ngx-file-input-pdf-color)}.ngx-fi__thumb[data-kind=video]{--ngx-fi-kind-color: var(--ngx-file-input-video-color)}.ngx-fi__thumb[data-kind=audio]{--ngx-fi-kind-color: var(--ngx-file-input-audio-color)}.ngx-fi__thumb mat-icon{color:var(--ngx-fi-kind-color)}.ngx-fi__thumb img{width:100%;height:100%;object-fit:cover}.ngx-fi__info{display:flex;flex:1;flex-direction:column;gap:2px;min-width:0}.ngx-fi__name{overflow:hidden;font:var(--mat-sys-body-medium);color:var(--mat-sys-on-surface);text-overflow:ellipsis;white-space:nowrap}.ngx-fi__meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;font:var(--mat-sys-body-small);color:var(--mat-sys-on-surface-variant)}.ngx-fi__item-error{display:inline-flex;align-items:center;gap:4px;color:var(--mat-sys-error)}.ngx-fi__item-error mat-icon{width:16px;height:16px;font-size:16px}.ngx-fi__remove{flex:none}.ngx-fi__live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}@keyframes ngx-fi-enter{0%{opacity:0;transform:translateY(-4px)}}@keyframes ngx-fi-flash{0%{opacity:1}to{opacity:0}}@media (prefers-reduced-motion: reduce){.ngx-fi__item,.ngx-fi__confirm{animation:none}.ngx-fi__zone,.ngx-fi__zone-icon{transition:none}}\n"] }]
4967
+ }], ctorParameters: () => [], propDecorators: { _nativeInput: [{ type: i0.ViewChild, args: ['nativeInput', { isSignal: true }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], validationError: [{ type: i0.Output, args: ["validationError"] }] } });
4959
4968
 
4960
4969
  /**
4961
4970
  * Generated bundle index. Do not edit.