@solcre-org/core-ui 2.20.31 → 2.20.33

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.
@@ -16,6 +16,10 @@
16
16
  object-fit: cover;
17
17
  }
18
18
 
19
+ .c-img-preview--no-hover {
20
+ cursor: default;
21
+ }
22
+
19
23
  .c-img-preview::before {
20
24
  content: "";
21
25
  position: absolute;
@@ -171,6 +175,11 @@
171
175
  transition: opacity var(--trs-duration-400) ease-out;
172
176
  }
173
177
 
178
+ .c-img-preview--no-hover:hover::after,
179
+ .c-img-preview--no-hover:hover::before {
180
+ opacity: 0 !important;
181
+ }
182
+
174
183
  }
175
184
 
176
185
 
@@ -1779,13 +1779,13 @@ class SelectFieldComponent extends BaseFieldComponent {
1779
1779
  else {
1780
1780
  current.push(option.value);
1781
1781
  }
1782
- this.formControl().setValue(current);
1782
+ this.formControl().setValue(current, { emitEvent: false });
1783
1783
  this.formControl().markAsDirty();
1784
1784
  this.onValueChange(current);
1785
1785
  this.searchTerm.set('');
1786
1786
  }
1787
1787
  else {
1788
- this.formControl().setValue(option.value);
1788
+ this.formControl().setValue(option.value, { emitEvent: false });
1789
1789
  this.formControl().markAsDirty();
1790
1790
  this.onValueChange(option.value);
1791
1791
  this.closeDropdown();
@@ -1797,7 +1797,7 @@ class SelectFieldComponent extends BaseFieldComponent {
1797
1797
  if (this.field().multiple) {
1798
1798
  const current = Array.isArray(this.computedValue()) ? [...this.computedValue()] : [];
1799
1799
  const filtered = current.filter((v) => String(v) !== String(item.value));
1800
- this.formControl().setValue(filtered);
1800
+ this.formControl().setValue(filtered, { emitEvent: false });
1801
1801
  this.formControl().markAsDirty();
1802
1802
  this.onValueChange(filtered);
1803
1803
  }
@@ -1806,7 +1806,7 @@ class SelectFieldComponent extends BaseFieldComponent {
1806
1806
  event.preventDefault();
1807
1807
  event.stopPropagation();
1808
1808
  const emptyValue = this.field().multiple ? [] : null;
1809
- this.formControl().setValue(emptyValue);
1809
+ this.formControl().setValue(emptyValue, { emitEvent: false });
1810
1810
  this.formControl().markAsDirty();
1811
1811
  this.hasValue.set(false);
1812
1812
  this.onValueChange(emptyValue);
@@ -1853,7 +1853,7 @@ class SelectFieldComponent extends BaseFieldComponent {
1853
1853
  if (current.length > 0) {
1854
1854
  event.preventDefault();
1855
1855
  current.pop();
1856
- this.formControl().setValue(current);
1856
+ this.formControl().setValue(current, { emitEvent: false });
1857
1857
  this.formControl().markAsDirty();
1858
1858
  this.onValueChange(current);
1859
1859
  }
@@ -2897,6 +2897,8 @@ class ImagePreviewComponent {
2897
2897
  isRelative = input(false);
2898
2898
  showSkeleton = input(true);
2899
2899
  skeletonAnimation = input(SkeletonAnimation.SHIMMER);
2900
+ disableModal = input(false);
2901
+ disableHover = input(false);
2900
2902
  isImageLoading = signal(true);
2901
2903
  hasImageError = signal(false);
2902
2904
  SkeletonType = SkeletonType;
@@ -2909,6 +2911,8 @@ class ImagePreviewComponent {
2909
2911
  this.hasImageError.set(true);
2910
2912
  }
2911
2913
  onImageClick() {
2914
+ if (this.disableModal())
2915
+ return;
2912
2916
  if (!this.isImageLoading() && !this.hasImageError()) {
2913
2917
  this.imageModalService.openImageModal({
2914
2918
  src: this.src(),
@@ -2918,16 +2922,16 @@ class ImagePreviewComponent {
2918
2922
  }
2919
2923
  }
2920
2924
  onSkeletonClick() {
2921
- if (!this.isImageLoading()) {
2925
+ if (!this.isImageLoading() && !this.disableModal()) {
2922
2926
  this.onImageClick();
2923
2927
  }
2924
2928
  }
2925
2929
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ImagePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2926
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ImagePreviewComponent, isStandalone: true, selector: "core-image-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, objectFit: { classPropertyName: "objectFit", publicName: "objectFit", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, cursor: { classPropertyName: "cursor", publicName: "cursor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, isRelative: { classPropertyName: "isRelative", publicName: "isRelative", isSignal: true, isRequired: false, transformFunction: null }, showSkeleton: { classPropertyName: "showSkeleton", publicName: "showSkeleton", isSignal: true, isRequired: false, transformFunction: null }, skeletonAnimation: { classPropertyName: "skeletonAnimation", publicName: "skeletonAnimation", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: GenericSkeletonComponent, selector: "core-generic-skeleton", inputs: ["config", "items", "type", "size", "width", "height", "animated", "animation", "lines", "customClass", "ariaLabel"] }] });
2930
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ImagePreviewComponent, isStandalone: true, selector: "core-image-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, objectFit: { classPropertyName: "objectFit", publicName: "objectFit", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, cursor: { classPropertyName: "cursor", publicName: "cursor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, isRelative: { classPropertyName: "isRelative", publicName: "isRelative", isSignal: true, isRequired: false, transformFunction: null }, showSkeleton: { classPropertyName: "showSkeleton", publicName: "showSkeleton", isSignal: true, isRequired: false, transformFunction: null }, skeletonAnimation: { classPropertyName: "skeletonAnimation", publicName: "skeletonAnimation", isSignal: true, isRequired: false, transformFunction: null }, disableModal: { classPropertyName: "disableModal", publicName: "disableModal", isSignal: true, isRequired: false, transformFunction: null }, disableHover: { classPropertyName: "disableHover", publicName: "disableHover", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [class.c-img-preview--no-hover]=\"disableHover()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"disableModal() ? 'default' : cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>", styles: [".c-img-preview{overflow:hidden;display:inline-block;position:relative}.c-img-preview img{display:block;width:100%;height:100%;transition:transform .2s ease,filter .2s ease}.c-img-preview:not(.c-img-preview--no-hover) img:hover{transform:scale(1.05);filter:brightness(.9)}.c-img-preview--no-hover img{cursor:default!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: GenericSkeletonComponent, selector: "core-generic-skeleton", inputs: ["config", "items", "type", "size", "width", "height", "animated", "animation", "lines", "customClass", "ariaLabel"] }] });
2927
2931
  }
2928
2932
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ImagePreviewComponent, decorators: [{
2929
2933
  type: Component,
2930
- args: [{ selector: 'core-image-preview', standalone: true, imports: [CommonModule, GenericSkeletonComponent], template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>" }]
2934
+ args: [{ selector: 'core-image-preview', standalone: true, imports: [CommonModule, GenericSkeletonComponent], template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [class.c-img-preview--no-hover]=\"disableHover()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"disableModal() ? 'default' : cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>", styles: [".c-img-preview{overflow:hidden;display:inline-block;position:relative}.c-img-preview img{display:block;width:100%;height:100%;transition:transform .2s ease,filter .2s ease}.c-img-preview:not(.c-img-preview--no-hover) img:hover{transform:scale(1.05);filter:brightness(.9)}.c-img-preview--no-hover img{cursor:default!important}\n"] }]
2931
2935
  }] });
2932
2936
 
2933
2937
  class FileFieldComponent extends BaseFieldComponent {
@@ -3414,7 +3418,7 @@ class FileFieldComponent extends BaseFieldComponent {
3414
3418
  this.onBlur();
3415
3419
  }
3416
3420
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: FileFieldComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3417
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: FileFieldComponent, isStandalone: true, selector: "core-file-field", usesInheritance: true, hostDirectives: [{ directive: CoreHostDirective }], ngImport: i0, template: "<label class=\"c-entry-item\" [class.c-entry-item--inline]=\"field().inline\">\n @if (field().label) {\n <span class=\"c-entry-text\">\n {{ field().label | translate }}\n @if (hasRequiredValidators()) {\n <span class=\"c-required\">*</span>\n }\n </span>\n }\n <div class=\"c-entry-file\" [class.has-error]=\"hasError()\">\n @if(fieldConfig().multiple || isEditMode() || allFiles().length === 0) {\n <label class=\"c-entry-file__label\">\n <span class=\"icon-upload c-entry-file__icon\"></span>\n <input\n type=\"file\"\n class=\"c-entry-file__input\"\n (change)=\"onFileSelected($event)\"\n [accept]=\"fieldConfig().accept\"\n [multiple]=\"fieldConfig().multiple\"\n (blur)=\"onBlurInput()\"\n [readonly]=\"isReadonly()\"\n >\n {{ fieldConfig().placeholder ?? fieldConfig().label | translate }}\n @if(fieldConfig().acceptString) {\n <br>\n <span class=\"c-entry-file__filename\" id=\"file-name\">{{ fieldConfig().acceptString }}</span>\n }\n </label>\n }\n\n @if (allFiles().length > 0) {\n <div class=\"c-attachments\">\n <p class=\"c-entry-text\">{{ 'files.attachedFiles' | translate }}</p>\n \n <ul class=\"c-attachments__list\">\n @for (file of allFiles(); track $index; let i = $index) {\n <li class=\"c-attachments__item\">\n <div class=\"c-bulleted-text\">\n @if(!isServerFile(file)) {\n <time>{{ getCurrentDate() }}</time>\n <span>{{ getCurrentUser() }}</span>\n }\n </div>\n \n <div class=\"c-attachments__holder\">\n @if(!isServerFile(file) && file.type.startsWith('image/') && fieldConfig().showPreview !== false) {\n <div class=\"c-attachments__content\">\n <div class=\"c-attachments__pic\">\n @if(isPreviewFile(file) && getOriginalUrl(file)) {\n <core-image-preview\n [src]=\"getOriginalUrl(file)!\"\n [alt]=\"file.name\"\n ></core-image-preview>\n } @else if(getPreviewUrl(file)) {\n <core-image-preview\n [src]=\"getPreviewUrl(file)!\"\n [alt]=\"file.name\"\n ></core-image-preview>\n }\n </div>\n <div class=\"c-attachments__text\">\n <span class=\"c-attachments__name-file\">{{ file.name }}</span>\n <span class=\"c-attachments__file-size\">({{ formatFileSize(file.size) }})</span>\n </div>\n </div>\n } @else {\n <a class=\"c-attachments__content\">\n @if(isServerFile(file)) {\n <span class=\"icon-file\"></span>\n {{ file.filename }}\n } @else {\n <span [class]=\"getFileIcon(file)\"></span>\n <div class=\"c-attachments__text\">\n <span class=\"c-attachments__name-file\">{{ file.name }}</span>\n <span class=\"c-attachments__file-size\">({{ formatFileSize(file.size) }})</span>\n </div>\n }\n </a>\n }\n\n <div class=\"c-attachments__actions u-flex u-flex--wrap\">\n <button \n type=\"button\" \n class=\"c-link context:error\" \n (click)=\"removeFile(i)\"\n [title]=\"'files.remove' | translate\"\n >\n <span class=\"icon-delete\"></span>\n {{ 'files.remove' | translate }}\n </button>\n\n @if(fieldConfig().customActions) {\n @for(action of fieldConfig().customActions; track action.id) {\n <button \n type=\"button\"\n class=\"c-link c-link--underlined\" \n (click)=\"action.action(file)\"\n [title]=\"action.label | translate\"\n >\n {{ action.label | translate }}\n <span [ngClass]=\"action.icon | coreIconCompat\"></span>\n </button>\n }\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n <core-field-errors [errors]=\"errors()\" />\n @if(displayErrorMessage()) {\n <span class=\"c-entry-error\">{{ displayErrorMessage()!.key | translate:displayErrorMessage()!.params }}</span>\n }\n</label>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FieldErrorsComponent, selector: "core-field-errors", inputs: ["errors"] }, { kind: "pipe", type: IconCompatPipe, name: "coreIconCompat" }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation"] }] });
3421
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: FileFieldComponent, isStandalone: true, selector: "core-file-field", usesInheritance: true, hostDirectives: [{ directive: CoreHostDirective }], ngImport: i0, template: "<label class=\"c-entry-item\" [class.c-entry-item--inline]=\"field().inline\">\n @if (field().label) {\n <span class=\"c-entry-text\">\n {{ field().label | translate }}\n @if (hasRequiredValidators()) {\n <span class=\"c-required\">*</span>\n }\n </span>\n }\n <div class=\"c-entry-file\" [class.has-error]=\"hasError()\">\n @if(fieldConfig().multiple || isEditMode() || allFiles().length === 0) {\n <label class=\"c-entry-file__label\">\n <span class=\"icon-upload c-entry-file__icon\"></span>\n <input\n type=\"file\"\n class=\"c-entry-file__input\"\n (change)=\"onFileSelected($event)\"\n [accept]=\"fieldConfig().accept\"\n [multiple]=\"fieldConfig().multiple\"\n (blur)=\"onBlurInput()\"\n [readonly]=\"isReadonly()\"\n >\n {{ fieldConfig().placeholder ?? fieldConfig().label | translate }}\n @if(fieldConfig().acceptString) {\n <br>\n <span class=\"c-entry-file__filename\" id=\"file-name\">{{ fieldConfig().acceptString }}</span>\n }\n </label>\n }\n\n @if (allFiles().length > 0) {\n <div class=\"c-attachments\">\n <p class=\"c-entry-text\">{{ 'files.attachedFiles' | translate }}</p>\n \n <ul class=\"c-attachments__list\">\n @for (file of allFiles(); track $index; let i = $index) {\n <li class=\"c-attachments__item\">\n <div class=\"c-bulleted-text\">\n @if(!isServerFile(file)) {\n <time>{{ getCurrentDate() }}</time>\n <span>{{ getCurrentUser() }}</span>\n }\n </div>\n \n <div class=\"c-attachments__holder\">\n @if(!isServerFile(file) && file.type.startsWith('image/') && fieldConfig().showPreview !== false) {\n <div class=\"c-attachments__content\">\n <div class=\"c-attachments__pic\">\n @if(isPreviewFile(file) && getOriginalUrl(file)) {\n <core-image-preview\n [src]=\"getOriginalUrl(file)!\"\n [alt]=\"file.name\"\n ></core-image-preview>\n } @else if(getPreviewUrl(file)) {\n <core-image-preview\n [src]=\"getPreviewUrl(file)!\"\n [alt]=\"file.name\"\n ></core-image-preview>\n }\n </div>\n <div class=\"c-attachments__text\">\n <span class=\"c-attachments__name-file\">{{ file.name }}</span>\n <span class=\"c-attachments__file-size\">({{ formatFileSize(file.size) }})</span>\n </div>\n </div>\n } @else {\n <a class=\"c-attachments__content\">\n @if(isServerFile(file)) {\n <span class=\"icon-file\"></span>\n {{ file.filename }}\n } @else {\n <span [class]=\"getFileIcon(file)\"></span>\n <div class=\"c-attachments__text\">\n <span class=\"c-attachments__name-file\">{{ file.name }}</span>\n <span class=\"c-attachments__file-size\">({{ formatFileSize(file.size) }})</span>\n </div>\n }\n </a>\n }\n\n <div class=\"c-attachments__actions u-flex u-flex--wrap\">\n <button \n type=\"button\" \n class=\"c-link context:error\" \n (click)=\"removeFile(i)\"\n [title]=\"'files.remove' | translate\"\n >\n <span class=\"icon-delete\"></span>\n {{ 'files.remove' | translate }}\n </button>\n\n @if(fieldConfig().customActions) {\n @for(action of fieldConfig().customActions; track action.id) {\n <button \n type=\"button\"\n class=\"c-link c-link--underlined\" \n (click)=\"action.action(file)\"\n [title]=\"action.label | translate\"\n >\n {{ action.label | translate }}\n <span [ngClass]=\"action.icon | coreIconCompat\"></span>\n </button>\n }\n }\n </div>\n </div>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n <core-field-errors [errors]=\"errors()\" />\n @if(displayErrorMessage()) {\n <span class=\"c-entry-error\">{{ displayErrorMessage()!.key | translate:displayErrorMessage()!.params }}</span>\n }\n</label>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FieldErrorsComponent, selector: "core-field-errors", inputs: ["errors"] }, { kind: "pipe", type: IconCompatPipe, name: "coreIconCompat" }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation", "disableModal", "disableHover"] }] });
3418
3422
  }
3419
3423
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: FileFieldComponent, decorators: [{
3420
3424
  type: Component,
@@ -17755,12 +17759,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
17755
17759
  // Este archivo es generado automáticamente por scripts/update-version.js
17756
17760
  // No edites manualmente este archivo
17757
17761
  const VERSION = {
17758
- full: '2.20.31',
17762
+ full: '2.20.33',
17759
17763
  major: 2,
17760
17764
  minor: 20,
17761
- patch: 31,
17762
- timestamp: '2026-03-03T16:38:38.438Z',
17763
- buildDate: '3/3/2026'
17765
+ patch: 33,
17766
+ timestamp: '2026-03-05T10:14:39.998Z',
17767
+ buildDate: '5/3/2026'
17764
17768
  };
17765
17769
 
17766
17770
  class MainNavComponent {
@@ -19815,7 +19819,7 @@ class CarouselComponent {
19815
19819
  };
19816
19820
  }
19817
19821
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19818
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: CarouselComponent, isStandalone: true, selector: "core-carousel", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "onResize()", "keydown": "onKeyDown($event)" } }, viewQueries: [{ propertyName: "carouselHolder", first: true, predicate: ["carouselHolder"], descendants: true, isSignal: true }, { propertyName: "carouselViewport", first: true, predicate: ["carouselViewport"], descendants: true, isSignal: true }], ngImport: i0, template: "<div \n [ngClass]=\"carouselClasses()\"\n [attr.aria-label]=\"ariaLabel()\"\n tabindex=\"0\"\n #carouselViewport\n [class.is-multiple]=\"perView() > 1\">\n \n <div class=\"c-img-carousel__viewport\">\n <div class=\"c-img-carousel__holder js-img-carousel-holder\" #carouselHolder>\n <div \n *ngFor=\"let image of images(); let i = index\"\n class=\"c-img-carousel__slide js-img-carousel-slide\">\n <div class=\"c-img-carousel__slide-inner\">\n <core-image-preview\n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image ' + (i + 1)\"\n [title]=\"image.title || image.alt || 'Image ' + (i + 1)\">\n </core-image-preview>\n </div>\n </div>\n </div>\n \n @if (!config().arrowsOutside) {\n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--prev icon-arrow-left\"\n type=\"button\"\n (click)=\"goToPrevSlide()\"\n [attr.aria-label]=\"'Anterior'\"\n data-control=\"prevBtn\">\n </button>\n \n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--next icon-arrow-right\"\n type=\"button\"\n (click)=\"goToNextSlide()\"\n [attr.aria-label]=\"'Siguiente'\"\n data-control=\"nextBtn\">\n </button>\n }\n \n <div \n *ngIf=\"showDots()\"\n class=\"c-img-carousel__dots js-img-carousel-nav\"\n aria-label=\"Navegaci\u00F3n\">\n <button\n *ngFor=\"let page of dots(); let i = index\"\n class=\"c-img-carousel__dot js-img-carousel-dot\"\n type=\"button\"\n (click)=\"goToPage(i)\"\n [class.c-img-carousel__dot--active]=\"currentPage() === i\"\n [attr.aria-label]=\"'Ir a p\u00E1gina ' + (i + 1)\"\n [attr.aria-current]=\"currentPage() === i ? 'true' : 'false'\">\n </button>\n </div>\n </div>\n @if(config().arrowsOutside) {\n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--prev icon-arrow-left\"\n type=\"button\"\n (click)=\"goToPrevSlide()\"\n [attr.aria-label]=\"'Anterior'\"\n data-control=\"prevBtn\">\n </button>\n \n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--next icon-arrow-right\"\n type=\"button\"\n (click)=\"goToNextSlide()\"\n [attr.aria-label]=\"'Siguiente'\"\n data-control=\"nextBtn\">\n </button>\n }\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19822
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: CarouselComponent, isStandalone: true, selector: "core-carousel", inputs: { images: { classPropertyName: "images", publicName: "images", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "onResize()", "keydown": "onKeyDown($event)" } }, viewQueries: [{ propertyName: "carouselHolder", first: true, predicate: ["carouselHolder"], descendants: true, isSignal: true }, { propertyName: "carouselViewport", first: true, predicate: ["carouselViewport"], descendants: true, isSignal: true }], ngImport: i0, template: "<div \n [ngClass]=\"carouselClasses()\"\n [attr.aria-label]=\"ariaLabel()\"\n tabindex=\"0\"\n #carouselViewport\n [class.is-multiple]=\"perView() > 1\">\n \n <div class=\"c-img-carousel__viewport\">\n <div class=\"c-img-carousel__holder js-img-carousel-holder\" #carouselHolder>\n <div \n *ngFor=\"let image of images(); let i = index\"\n class=\"c-img-carousel__slide js-img-carousel-slide\">\n <div class=\"c-img-carousel__slide-inner\">\n <core-image-preview\n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image ' + (i + 1)\"\n [title]=\"image.title || image.alt || 'Image ' + (i + 1)\">\n </core-image-preview>\n </div>\n </div>\n </div>\n \n @if (!config().arrowsOutside) {\n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--prev icon-arrow-left\"\n type=\"button\"\n (click)=\"goToPrevSlide()\"\n [attr.aria-label]=\"'Anterior'\"\n data-control=\"prevBtn\">\n </button>\n \n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--next icon-arrow-right\"\n type=\"button\"\n (click)=\"goToNextSlide()\"\n [attr.aria-label]=\"'Siguiente'\"\n data-control=\"nextBtn\">\n </button>\n }\n \n <div \n *ngIf=\"showDots()\"\n class=\"c-img-carousel__dots js-img-carousel-nav\"\n aria-label=\"Navegaci\u00F3n\">\n <button\n *ngFor=\"let page of dots(); let i = index\"\n class=\"c-img-carousel__dot js-img-carousel-dot\"\n type=\"button\"\n (click)=\"goToPage(i)\"\n [class.c-img-carousel__dot--active]=\"currentPage() === i\"\n [attr.aria-label]=\"'Ir a p\u00E1gina ' + (i + 1)\"\n [attr.aria-current]=\"currentPage() === i ? 'true' : 'false'\">\n </button>\n </div>\n </div>\n @if(config().arrowsOutside) {\n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--prev icon-arrow-left\"\n type=\"button\"\n (click)=\"goToPrevSlide()\"\n [attr.aria-label]=\"'Anterior'\"\n data-control=\"prevBtn\">\n </button>\n \n <button \n *ngIf=\"showArrows()\"\n class=\"c-img-carousel__btn c-img-carousel__btn--next icon-arrow-right\"\n type=\"button\"\n (click)=\"goToNextSlide()\"\n [attr.aria-label]=\"'Siguiente'\"\n data-control=\"nextBtn\">\n </button>\n }\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation", "disableModal", "disableHover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19819
19823
  }
19820
19824
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CarouselComponent, decorators: [{
19821
19825
  type: Component,
@@ -21137,7 +21141,7 @@ class GenericChatComponent {
21137
21141
  return this.isSending();
21138
21142
  }
21139
21143
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: GenericChatComponent, deps: [{ token: GenericChatService }], target: i0.ɵɵFactoryTarget.Component });
21140
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: GenericChatComponent, isStandalone: true, selector: "core-generic-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messagesSent: "messagesSent", imageClicked: "imageClicked", fileClicked: "fileClicked", fileSelected: "fileSelected", filesSelected: "filesSelected", typingStatusChanged: "typingStatusChanged", modalClosed: "modalClosed", attachmentAction: "attachmentAction", sendingStatusChanged: "sendingStatusChanged", loadMoreMessages: "loadMoreMessages" }, host: { listeners: { "document:keydown": "handleKeyboardEvent($event)" } }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }, { propertyName: "modalMessagesContainer", first: true, predicate: ["modalMessagesContainer"], descendants: true, isSignal: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "scrollSentinelElement", first: true, predicate: ["scrollSentinel"], descendants: true, read: ElementRef }], ngImport: i0, template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file) || ''\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: GenericButtonComponent, selector: "core-generic-button", inputs: ["config", "data"], outputs: ["buttonClick"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation"] }, { kind: "component", type: FilePreviewComponent, selector: "core-file-preview", inputs: ["fileModels", "files", "config"], outputs: ["actionTriggered"] }] });
21144
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: GenericChatComponent, isStandalone: true, selector: "core-generic-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messagesSent: "messagesSent", imageClicked: "imageClicked", fileClicked: "fileClicked", fileSelected: "fileSelected", filesSelected: "filesSelected", typingStatusChanged: "typingStatusChanged", modalClosed: "modalClosed", attachmentAction: "attachmentAction", sendingStatusChanged: "sendingStatusChanged", loadMoreMessages: "loadMoreMessages" }, host: { listeners: { "document:keydown": "handleKeyboardEvent($event)" } }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }, { propertyName: "modalMessagesContainer", first: true, predicate: ["modalMessagesContainer"], descendants: true, isSignal: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "scrollSentinelElement", first: true, predicate: ["scrollSentinel"], descendants: true, read: ElementRef }], ngImport: i0, template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file) || ''\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: GenericButtonComponent, selector: "core-generic-button", inputs: ["config", "data"], outputs: ["buttonClick"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation", "disableModal", "disableHover"] }, { kind: "component", type: FilePreviewComponent, selector: "core-file-preview", inputs: ["fileModels", "files", "config"], outputs: ["actionTriggered"] }] });
21141
21145
  }
21142
21146
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: GenericChatComponent, decorators: [{
21143
21147
  type: Component,