valtech-components 4.0.1038 → 4.0.1040

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.
@@ -70,7 +70,7 @@ import fixWebmDuration from 'fix-webm-duration';
70
70
  * Current version of valtech-components.
71
71
  * This is automatically updated during the publish process.
72
72
  */
73
- const VERSION = '4.0.1038';
73
+ const VERSION = '4.0.1040';
74
74
 
75
75
  function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
76
76
  if (rule == null)
@@ -1876,13 +1876,18 @@ class I18nService {
1876
1876
  console.warn(`[i18n] Language '${lang}' not in supported languages`);
1877
1877
  return;
1878
1878
  }
1879
- if (lang === this._lang()) {
1880
- return;
1881
- }
1882
- // Persistir en localStorage (browser-only)
1879
+ // Aunque el signal ya tenga este valor, re-persistimos. Esto corrige
1880
+ // estados viejos donde la UI quedó en el idioma correcto por snapshot, pero
1881
+ // localStorage conservó otro valor y el siguiente bootstrap volvió al viejo.
1883
1882
  if (this.isBrowser) {
1884
1883
  localStorage.setItem(LANG_STORAGE_KEY$1, lang);
1885
1884
  }
1885
+ if (lang === this._lang()) {
1886
+ if (forceReload && this.isBrowser) {
1887
+ window.location.reload();
1888
+ }
1889
+ return;
1890
+ }
1886
1891
  // Actualizar signal
1887
1892
  this._lang.set(lang);
1888
1893
  // Fallback: recargar si se solicita
@@ -5366,6 +5371,260 @@ function query() {
5366
5371
  return new QueryBuilder();
5367
5372
  }
5368
5373
 
5374
+ /**
5375
+ * Default values for image processing
5376
+ */
5377
+ const IMAGE_DEFAULTS = {
5378
+ maxWidth: 800,
5379
+ maxHeight: 800,
5380
+ quality: 0.8,
5381
+ mimeType: 'image/jpeg',
5382
+ maxSize: 10 * 1024 * 1024, // 10MB
5383
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
5384
+ thumbnailSize: 150,
5385
+ };
5386
+
5387
+ /**
5388
+ * ImageService
5389
+ *
5390
+ * Service for image processing including compression, thumbnails, cropping and validation.
5391
+ * Uses HTML Canvas for all operations - no external dependencies.
5392
+ *
5393
+ * @example
5394
+ * ```typescript
5395
+ * const imageService = inject(ImageService);
5396
+ *
5397
+ * // Compress an image
5398
+ * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
5399
+ *
5400
+ * // Generate thumbnail
5401
+ * const thumb = await imageService.thumbnail(file, 150);
5402
+ *
5403
+ * // Validate before processing
5404
+ * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
5405
+ * if (!validation.valid) {
5406
+ * console.error(validation.message);
5407
+ * }
5408
+ * ```
5409
+ */
5410
+ class ImageService {
5411
+ /**
5412
+ * Compress an image maintaining aspect ratio
5413
+ * @param file - File or Blob to compress
5414
+ * @param options - Compression options
5415
+ * @returns Promise with processed image data
5416
+ */
5417
+ async compress(file, options) {
5418
+ const opts = {
5419
+ maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
5420
+ maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
5421
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
5422
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
5423
+ };
5424
+ const img = await this.loadImage(file);
5425
+ const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
5426
+ const canvas = document.createElement('canvas');
5427
+ canvas.width = width;
5428
+ canvas.height = height;
5429
+ const ctx = canvas.getContext('2d');
5430
+ ctx.drawImage(img, 0, 0, width, height);
5431
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
5432
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
5433
+ return {
5434
+ blob,
5435
+ dataUrl,
5436
+ width,
5437
+ height,
5438
+ size: blob.size,
5439
+ };
5440
+ }
5441
+ /**
5442
+ * Generate a square thumbnail from an image
5443
+ * @param file - File or Blob to process
5444
+ * @param size - Thumbnail size in pixels (default: 150)
5445
+ * @returns Promise with processed thumbnail
5446
+ */
5447
+ async thumbnail(file, size) {
5448
+ const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
5449
+ const img = await this.loadImage(file);
5450
+ // Calculate square crop from center
5451
+ const minDim = Math.min(img.width, img.height);
5452
+ const cropX = (img.width - minDim) / 2;
5453
+ const cropY = (img.height - minDim) / 2;
5454
+ const canvas = document.createElement('canvas');
5455
+ canvas.width = thumbSize;
5456
+ canvas.height = thumbSize;
5457
+ const ctx = canvas.getContext('2d');
5458
+ ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
5459
+ const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
5460
+ );
5461
+ const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
5462
+ return {
5463
+ blob,
5464
+ dataUrl,
5465
+ width: thumbSize,
5466
+ height: thumbSize,
5467
+ size: blob.size,
5468
+ };
5469
+ }
5470
+ /**
5471
+ * Crop an image with specific coordinates
5472
+ * @param file - File or Blob to crop
5473
+ * @param cropData - Crop coordinates and dimensions
5474
+ * @param options - Optional compression options for output
5475
+ * @returns Promise with cropped image
5476
+ */
5477
+ async crop(file, cropData, options) {
5478
+ const img = await this.loadImage(file);
5479
+ const opts = {
5480
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
5481
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
5482
+ };
5483
+ const canvas = document.createElement('canvas');
5484
+ canvas.width = cropData.width;
5485
+ canvas.height = cropData.height;
5486
+ const ctx = canvas.getContext('2d');
5487
+ ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
5488
+ // Apply max dimensions if specified
5489
+ if (options?.maxWidth || options?.maxHeight) {
5490
+ return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
5491
+ }
5492
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
5493
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
5494
+ return {
5495
+ blob,
5496
+ dataUrl,
5497
+ width: cropData.width,
5498
+ height: cropData.height,
5499
+ size: blob.size,
5500
+ };
5501
+ }
5502
+ /**
5503
+ * Validate an image file before processing
5504
+ * @param file - File to validate
5505
+ * @param options - Validation options
5506
+ * @returns Validation result with error details if invalid
5507
+ */
5508
+ validate(file, options) {
5509
+ const opts = {
5510
+ maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
5511
+ allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
5512
+ };
5513
+ // Check file type
5514
+ if (!opts.allowedTypes.includes(file.type)) {
5515
+ return {
5516
+ valid: false,
5517
+ error: 'invalidType',
5518
+ message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
5519
+ };
5520
+ }
5521
+ // Check file size
5522
+ if (file.size > opts.maxSize) {
5523
+ const maxMB = Math.round(opts.maxSize / (1024 * 1024));
5524
+ return {
5525
+ valid: false,
5526
+ error: 'fileTooLarge',
5527
+ message: `La imagen es muy grande. Máximo ${maxMB}MB`,
5528
+ };
5529
+ }
5530
+ return { valid: true };
5531
+ }
5532
+ /**
5533
+ * Validate image dimensions (async - requires loading image)
5534
+ * @param file - File to validate
5535
+ * @param options - Validation options with minWidth/minHeight
5536
+ * @returns Promise with validation result
5537
+ */
5538
+ async validateDimensions(file, options) {
5539
+ const img = await this.loadImage(file);
5540
+ if (options.minWidth && img.width < options.minWidth) {
5541
+ return {
5542
+ valid: false,
5543
+ error: 'imageTooSmall',
5544
+ message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
5545
+ };
5546
+ }
5547
+ if (options.minHeight && img.height < options.minHeight) {
5548
+ return {
5549
+ valid: false,
5550
+ error: 'imageTooSmall',
5551
+ message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
5552
+ };
5553
+ }
5554
+ return { valid: true };
5555
+ }
5556
+ /**
5557
+ * Convert a Blob/File to a data URL
5558
+ */
5559
+ async toDataUrl(file) {
5560
+ return new Promise((resolve, reject) => {
5561
+ const reader = new FileReader();
5562
+ reader.onload = () => resolve(reader.result);
5563
+ reader.onerror = reject;
5564
+ reader.readAsDataURL(file);
5565
+ });
5566
+ }
5567
+ /**
5568
+ * Convert a data URL to a Blob
5569
+ */
5570
+ dataUrlToBlob(dataUrl) {
5571
+ const arr = dataUrl.split(',');
5572
+ const mime = arr[0].match(/:(.*?);/)[1];
5573
+ const bstr = atob(arr[1]);
5574
+ let n = bstr.length;
5575
+ const u8arr = new Uint8Array(n);
5576
+ while (n--) {
5577
+ u8arr[n] = bstr.charCodeAt(n);
5578
+ }
5579
+ return new Blob([u8arr], { type: mime });
5580
+ }
5581
+ // ============== Private Helpers ==============
5582
+ loadImage(file) {
5583
+ return new Promise((resolve, reject) => {
5584
+ const img = new Image();
5585
+ img.onload = () => {
5586
+ URL.revokeObjectURL(img.src);
5587
+ resolve(img);
5588
+ };
5589
+ img.onerror = reject;
5590
+ img.src = URL.createObjectURL(file);
5591
+ });
5592
+ }
5593
+ calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
5594
+ let width = originalWidth;
5595
+ let height = originalHeight;
5596
+ // Scale down if necessary, maintaining aspect ratio
5597
+ if (width > maxWidth) {
5598
+ height = (height * maxWidth) / width;
5599
+ width = maxWidth;
5600
+ }
5601
+ if (height > maxHeight) {
5602
+ width = (width * maxHeight) / height;
5603
+ height = maxHeight;
5604
+ }
5605
+ return {
5606
+ width: Math.round(width),
5607
+ height: Math.round(height),
5608
+ };
5609
+ }
5610
+ canvasToBlob(canvas, mimeType, quality) {
5611
+ return new Promise((resolve, reject) => {
5612
+ canvas.toBlob((blob) => {
5613
+ if (blob)
5614
+ resolve(blob);
5615
+ else
5616
+ reject(new Error('Failed to create blob from canvas'));
5617
+ }, mimeType, quality);
5618
+ });
5619
+ }
5620
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5621
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
5622
+ }
5623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
5624
+ type: Injectable,
5625
+ args: [{ providedIn: 'root' }]
5626
+ }] });
5627
+
5369
5628
  /**
5370
5629
  * Storage Service
5371
5630
  *
@@ -5404,6 +5663,7 @@ class StorageService {
5404
5663
  constructor(storage) {
5405
5664
  this.storage = storage;
5406
5665
  this.config = inject(VALTECH_FIREBASE_CONFIG, { optional: true });
5666
+ this.imageService = inject(ImageService);
5407
5667
  }
5408
5668
  /**
5409
5669
  * Prefija el path de storage con el appId si está configurado.
@@ -5649,6 +5909,52 @@ class StorageService {
5649
5909
  }
5650
5910
  return sdkResult;
5651
5911
  }
5912
+ /**
5913
+ * Procesa y sube una imagen.
5914
+ *
5915
+ * A diferencia de `uploadAndGetUrl`, este método es específico para imágenes:
5916
+ * comprime por defecto con `ImageService` y puede generar un thumbnail en un
5917
+ * path separado. Para archivos que deben conservarse byte a byte, usar
5918
+ * `uploadAndGetUrl`.
5919
+ */
5920
+ async uploadImageAndGetUrl(path, file, options) {
5921
+ const { compress = true, compression, thumbnailPath, thumbnailSize, thumbnailMetadata, ...metadata } = options ?? {};
5922
+ const processed = compress
5923
+ ? await this.imageService.compress(file, compression)
5924
+ : {
5925
+ blob: await this.materialize(file),
5926
+ width: 0,
5927
+ height: 0,
5928
+ size: file.size,
5929
+ };
5930
+ const imageContentType = compress
5931
+ ? (compression?.mimeType ?? IMAGE_DEFAULTS.mimeType)
5932
+ : (metadata.contentType ?? file.type ?? 'application/octet-stream');
5933
+ const result = await this.uploadAndGetUrl(path, processed.blob, {
5934
+ ...metadata,
5935
+ contentType: imageContentType,
5936
+ });
5937
+ let thumbnail;
5938
+ if (thumbnailPath) {
5939
+ const thumb = await this.imageService.thumbnail(processed.blob, thumbnailSize);
5940
+ thumbnail = await this.uploadAndGetUrl(thumbnailPath, thumb.blob, {
5941
+ ...metadata,
5942
+ ...thumbnailMetadata,
5943
+ contentType: thumbnailMetadata?.contentType ?? IMAGE_DEFAULTS.mimeType,
5944
+ });
5945
+ }
5946
+ return {
5947
+ ...result,
5948
+ thumbnail,
5949
+ processed: {
5950
+ originalSize: file.size,
5951
+ size: processed.blob.size,
5952
+ width: processed.width,
5953
+ height: processed.height,
5954
+ compressed: compress,
5955
+ },
5956
+ };
5957
+ }
5652
5958
  /**
5653
5959
  * Sube un archivo desde una Data URL (base64).
5654
5960
  *
@@ -10651,7 +10957,11 @@ class AuthService {
10651
10957
  handle: profile.handle ?? null,
10652
10958
  avatarUrl: profile.avatarUrl ?? null,
10653
10959
  phone: profile.phone ?? null,
10654
- })), catchError(error => this.handleAuthError(error)));
10960
+ })), tap(profile => {
10961
+ if (this.i18nService && profile.language) {
10962
+ this.i18nService.setLanguage(profile.language);
10963
+ }
10964
+ }), catchError(error => this.handleAuthError(error)));
10655
10965
  }
10656
10966
  /**
10657
10967
  * Actualiza el perfil del usuario. Sincroniza state con los nuevos values
@@ -22757,6 +23067,7 @@ class SearchSelectorComponent {
22757
23067
  [interfaceOptions]="customPopoverOptions"
22758
23068
  [interface]="resolvedProps.selectInterface || 'popover'"
22759
23069
  [placeholder]="resolvedProps.placeholder"
23070
+ toggleIcon="chevron-down-outline"
22760
23071
  [cancelText]="cancelText"
22761
23072
  [okText]="okText"
22762
23073
  >
@@ -22782,7 +23093,7 @@ class SearchSelectorComponent {
22782
23093
  </ion-select-option>
22783
23094
  }
22784
23095
  </ion-select>
22785
- `, isInline: true, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.625rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$8.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$8.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }] }); }
23096
+ `, isInline: true, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.875rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$8.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$8.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }] }); }
22786
23097
  }
22787
23098
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SearchSelectorComponent, decorators: [{
22788
23099
  type: Component,
@@ -22793,6 +23104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
22793
23104
  [interfaceOptions]="customPopoverOptions"
22794
23105
  [interface]="resolvedProps.selectInterface || 'popover'"
22795
23106
  [placeholder]="resolvedProps.placeholder"
23107
+ toggleIcon="chevron-down-outline"
22796
23108
  [cancelText]="cancelText"
22797
23109
  [okText]="okText"
22798
23110
  >
@@ -22818,7 +23130,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
22818
23130
  </ion-select-option>
22819
23131
  }
22820
23132
  </ion-select>
22821
- `, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.625rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"] }]
23133
+ `, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.875rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"] }]
22822
23134
  }], propDecorators: { preset: [{
22823
23135
  type: Input
22824
23136
  }], props: [{
@@ -37930,260 +38242,6 @@ const DEFAULT_FEEDBACK_TYPE_OPTIONS = [
37930
38242
  */
37931
38243
  // Configuration
37932
38244
 
37933
- /**
37934
- * Default values for image processing
37935
- */
37936
- const IMAGE_DEFAULTS = {
37937
- maxWidth: 800,
37938
- maxHeight: 800,
37939
- quality: 0.8,
37940
- mimeType: 'image/jpeg',
37941
- maxSize: 10 * 1024 * 1024, // 10MB
37942
- allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
37943
- thumbnailSize: 150,
37944
- };
37945
-
37946
- /**
37947
- * ImageService
37948
- *
37949
- * Service for image processing including compression, thumbnails, cropping and validation.
37950
- * Uses HTML Canvas for all operations - no external dependencies.
37951
- *
37952
- * @example
37953
- * ```typescript
37954
- * const imageService = inject(ImageService);
37955
- *
37956
- * // Compress an image
37957
- * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
37958
- *
37959
- * // Generate thumbnail
37960
- * const thumb = await imageService.thumbnail(file, 150);
37961
- *
37962
- * // Validate before processing
37963
- * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
37964
- * if (!validation.valid) {
37965
- * console.error(validation.message);
37966
- * }
37967
- * ```
37968
- */
37969
- class ImageService {
37970
- /**
37971
- * Compress an image maintaining aspect ratio
37972
- * @param file - File or Blob to compress
37973
- * @param options - Compression options
37974
- * @returns Promise with processed image data
37975
- */
37976
- async compress(file, options) {
37977
- const opts = {
37978
- maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
37979
- maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
37980
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
37981
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
37982
- };
37983
- const img = await this.loadImage(file);
37984
- const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
37985
- const canvas = document.createElement('canvas');
37986
- canvas.width = width;
37987
- canvas.height = height;
37988
- const ctx = canvas.getContext('2d');
37989
- ctx.drawImage(img, 0, 0, width, height);
37990
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
37991
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
37992
- return {
37993
- blob,
37994
- dataUrl,
37995
- width,
37996
- height,
37997
- size: blob.size,
37998
- };
37999
- }
38000
- /**
38001
- * Generate a square thumbnail from an image
38002
- * @param file - File or Blob to process
38003
- * @param size - Thumbnail size in pixels (default: 150)
38004
- * @returns Promise with processed thumbnail
38005
- */
38006
- async thumbnail(file, size) {
38007
- const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
38008
- const img = await this.loadImage(file);
38009
- // Calculate square crop from center
38010
- const minDim = Math.min(img.width, img.height);
38011
- const cropX = (img.width - minDim) / 2;
38012
- const cropY = (img.height - minDim) / 2;
38013
- const canvas = document.createElement('canvas');
38014
- canvas.width = thumbSize;
38015
- canvas.height = thumbSize;
38016
- const ctx = canvas.getContext('2d');
38017
- ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
38018
- const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
38019
- );
38020
- const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
38021
- return {
38022
- blob,
38023
- dataUrl,
38024
- width: thumbSize,
38025
- height: thumbSize,
38026
- size: blob.size,
38027
- };
38028
- }
38029
- /**
38030
- * Crop an image with specific coordinates
38031
- * @param file - File or Blob to crop
38032
- * @param cropData - Crop coordinates and dimensions
38033
- * @param options - Optional compression options for output
38034
- * @returns Promise with cropped image
38035
- */
38036
- async crop(file, cropData, options) {
38037
- const img = await this.loadImage(file);
38038
- const opts = {
38039
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
38040
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
38041
- };
38042
- const canvas = document.createElement('canvas');
38043
- canvas.width = cropData.width;
38044
- canvas.height = cropData.height;
38045
- const ctx = canvas.getContext('2d');
38046
- ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
38047
- // Apply max dimensions if specified
38048
- if (options?.maxWidth || options?.maxHeight) {
38049
- return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
38050
- }
38051
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
38052
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
38053
- return {
38054
- blob,
38055
- dataUrl,
38056
- width: cropData.width,
38057
- height: cropData.height,
38058
- size: blob.size,
38059
- };
38060
- }
38061
- /**
38062
- * Validate an image file before processing
38063
- * @param file - File to validate
38064
- * @param options - Validation options
38065
- * @returns Validation result with error details if invalid
38066
- */
38067
- validate(file, options) {
38068
- const opts = {
38069
- maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
38070
- allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
38071
- };
38072
- // Check file type
38073
- if (!opts.allowedTypes.includes(file.type)) {
38074
- return {
38075
- valid: false,
38076
- error: 'invalidType',
38077
- message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
38078
- };
38079
- }
38080
- // Check file size
38081
- if (file.size > opts.maxSize) {
38082
- const maxMB = Math.round(opts.maxSize / (1024 * 1024));
38083
- return {
38084
- valid: false,
38085
- error: 'fileTooLarge',
38086
- message: `La imagen es muy grande. Máximo ${maxMB}MB`,
38087
- };
38088
- }
38089
- return { valid: true };
38090
- }
38091
- /**
38092
- * Validate image dimensions (async - requires loading image)
38093
- * @param file - File to validate
38094
- * @param options - Validation options with minWidth/minHeight
38095
- * @returns Promise with validation result
38096
- */
38097
- async validateDimensions(file, options) {
38098
- const img = await this.loadImage(file);
38099
- if (options.minWidth && img.width < options.minWidth) {
38100
- return {
38101
- valid: false,
38102
- error: 'imageTooSmall',
38103
- message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
38104
- };
38105
- }
38106
- if (options.minHeight && img.height < options.minHeight) {
38107
- return {
38108
- valid: false,
38109
- error: 'imageTooSmall',
38110
- message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
38111
- };
38112
- }
38113
- return { valid: true };
38114
- }
38115
- /**
38116
- * Convert a Blob/File to a data URL
38117
- */
38118
- async toDataUrl(file) {
38119
- return new Promise((resolve, reject) => {
38120
- const reader = new FileReader();
38121
- reader.onload = () => resolve(reader.result);
38122
- reader.onerror = reject;
38123
- reader.readAsDataURL(file);
38124
- });
38125
- }
38126
- /**
38127
- * Convert a data URL to a Blob
38128
- */
38129
- dataUrlToBlob(dataUrl) {
38130
- const arr = dataUrl.split(',');
38131
- const mime = arr[0].match(/:(.*?);/)[1];
38132
- const bstr = atob(arr[1]);
38133
- let n = bstr.length;
38134
- const u8arr = new Uint8Array(n);
38135
- while (n--) {
38136
- u8arr[n] = bstr.charCodeAt(n);
38137
- }
38138
- return new Blob([u8arr], { type: mime });
38139
- }
38140
- // ============== Private Helpers ==============
38141
- loadImage(file) {
38142
- return new Promise((resolve, reject) => {
38143
- const img = new Image();
38144
- img.onload = () => {
38145
- URL.revokeObjectURL(img.src);
38146
- resolve(img);
38147
- };
38148
- img.onerror = reject;
38149
- img.src = URL.createObjectURL(file);
38150
- });
38151
- }
38152
- calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
38153
- let width = originalWidth;
38154
- let height = originalHeight;
38155
- // Scale down if necessary, maintaining aspect ratio
38156
- if (width > maxWidth) {
38157
- height = (height * maxWidth) / width;
38158
- width = maxWidth;
38159
- }
38160
- if (height > maxHeight) {
38161
- width = (width * maxHeight) / height;
38162
- height = maxHeight;
38163
- }
38164
- return {
38165
- width: Math.round(width),
38166
- height: Math.round(height),
38167
- };
38168
- }
38169
- canvasToBlob(canvas, mimeType, quality) {
38170
- return new Promise((resolve, reject) => {
38171
- canvas.toBlob((blob) => {
38172
- if (blob)
38173
- resolve(blob);
38174
- else
38175
- reject(new Error('Failed to create blob from canvas'));
38176
- }, mimeType, quality);
38177
- });
38178
- }
38179
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
38180
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
38181
- }
38182
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
38183
- type: Injectable,
38184
- args: [{ providedIn: 'root' }]
38185
- }] });
38186
-
38187
38245
  class AttachmentUploaderComponent {
38188
38246
  get readyUrls() {
38189
38247
  return this.attachments()
@@ -59765,9 +59823,13 @@ class PreferencesService {
59765
59823
  .docChanges(`users/${userId}/preferences`, 'main')
59766
59824
  .subscribe(doc => {
59767
59825
  if (!doc) {
59768
- // Doc no existe aún (user nuevo, sin primer sync). Mantenemos
59769
- // defaults locales pero NO marcamos synced — los side-effects no
59770
- // pisan el theme/lang locales con valores arbitrarios.
59826
+ // Doc no existe aún para esta app. Antes dejábamos la app con
59827
+ // localStorage/navigator, aunque el usuario ya tuviera idioma
59828
+ // canónico en auth. Resultado: cuenta en español, app nueva en inglés
59829
+ // hasta cambiar manualmente a inglés y volver a español. Hidratar
59830
+ // desde perfil canónico corrige ese primer arranque y además crea el
59831
+ // mirror de esta app.
59832
+ this.hydrateMissingPreferencesFromProfile(userId);
59771
59833
  return;
59772
59834
  }
59773
59835
  // Ignorar emisiones del listener mientras hay un update() en vuelo: un
@@ -59792,8 +59854,25 @@ class PreferencesService {
59792
59854
  this.subscription?.unsubscribe();
59793
59855
  this.subscription = undefined;
59794
59856
  this.currentUserId = undefined;
59857
+ this.missingDocHydrationUserId = undefined;
59795
59858
  this._synced.set(false);
59796
59859
  }
59860
+ hydrateMissingPreferencesFromProfile(userId) {
59861
+ if (this.missingDocHydrationUserId === userId)
59862
+ return;
59863
+ this.missingDocHydrationUserId = userId;
59864
+ void firstValueFrom(this.auth.getProfile())
59865
+ .then(profile => {
59866
+ const language = profile.language;
59867
+ if (language === 'es' || language === 'en') {
59868
+ this._language.set(language);
59869
+ this.i18n?.setLanguage(language);
59870
+ this._synced.set(true);
59871
+ void this.update({ language }).catch(() => undefined);
59872
+ }
59873
+ })
59874
+ .catch(() => undefined);
59875
+ }
59797
59876
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PreferencesService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: FirestoreService }, { token: AuthService }, { token: i1$3.HttpClient }, { token: FirebaseService, optional: true }, { token: ThemeService, optional: true }, { token: FontSizeService, optional: true }, { token: I18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
59798
59877
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PreferencesService, providedIn: 'root' }); }
59799
59878
  }