valtech-components 4.0.1039 → 4.0.1041

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.1039';
73
+ const VERSION = '4.0.1041';
74
74
 
75
75
  function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
76
76
  if (rule == null)
@@ -5371,6 +5371,260 @@ function query() {
5371
5371
  return new QueryBuilder();
5372
5372
  }
5373
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
+
5374
5628
  /**
5375
5629
  * Storage Service
5376
5630
  *
@@ -5409,6 +5663,7 @@ class StorageService {
5409
5663
  constructor(storage) {
5410
5664
  this.storage = storage;
5411
5665
  this.config = inject(VALTECH_FIREBASE_CONFIG, { optional: true });
5666
+ this.imageService = inject(ImageService);
5412
5667
  }
5413
5668
  /**
5414
5669
  * Prefija el path de storage con el appId si está configurado.
@@ -5654,6 +5909,52 @@ class StorageService {
5654
5909
  }
5655
5910
  return sdkResult;
5656
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
+ }
5657
5958
  /**
5658
5959
  * Sube un archivo desde una Data URL (base64).
5659
5960
  *
@@ -22698,6 +22999,7 @@ class SearchSelectorComponent {
22698
22999
  * Resolved props after merging preset + explicit props.
22699
23000
  */
22700
23001
  this.resolvedProps = {};
23002
+ addIcons({ chevronDownOutline });
22701
23003
  }
22702
23004
  ngOnInit() {
22703
23005
  this.resolveProps();
@@ -22766,6 +23068,7 @@ class SearchSelectorComponent {
22766
23068
  [interfaceOptions]="customPopoverOptions"
22767
23069
  [interface]="resolvedProps.selectInterface || 'popover'"
22768
23070
  [placeholder]="resolvedProps.placeholder"
23071
+ toggleIcon="chevron-down-outline"
22769
23072
  [cancelText]="cancelText"
22770
23073
  [okText]="okText"
22771
23074
  >
@@ -22791,7 +23094,7 @@ class SearchSelectorComponent {
22791
23094
  </ion-select-option>
22792
23095
  }
22793
23096
  </ion-select>
22794
- `, 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"] }] }); }
23097
+ `, 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"] }] }); }
22795
23098
  }
22796
23099
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SearchSelectorComponent, decorators: [{
22797
23100
  type: Component,
@@ -22802,6 +23105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
22802
23105
  [interfaceOptions]="customPopoverOptions"
22803
23106
  [interface]="resolvedProps.selectInterface || 'popover'"
22804
23107
  [placeholder]="resolvedProps.placeholder"
23108
+ toggleIcon="chevron-down-outline"
22805
23109
  [cancelText]="cancelText"
22806
23110
  [okText]="okText"
22807
23111
  >
@@ -22827,8 +23131,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
22827
23131
  </ion-select-option>
22828
23132
  }
22829
23133
  </ion-select>
22830
- `, 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"] }]
22831
- }], propDecorators: { preset: [{
23134
+ `, 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"] }]
23135
+ }], ctorParameters: () => [], propDecorators: { preset: [{
22832
23136
  type: Input
22833
23137
  }], props: [{
22834
23138
  type: Input
@@ -37939,260 +38243,6 @@ const DEFAULT_FEEDBACK_TYPE_OPTIONS = [
37939
38243
  */
37940
38244
  // Configuration
37941
38245
 
37942
- /**
37943
- * Default values for image processing
37944
- */
37945
- const IMAGE_DEFAULTS = {
37946
- maxWidth: 800,
37947
- maxHeight: 800,
37948
- quality: 0.8,
37949
- mimeType: 'image/jpeg',
37950
- maxSize: 10 * 1024 * 1024, // 10MB
37951
- allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
37952
- thumbnailSize: 150,
37953
- };
37954
-
37955
- /**
37956
- * ImageService
37957
- *
37958
- * Service for image processing including compression, thumbnails, cropping and validation.
37959
- * Uses HTML Canvas for all operations - no external dependencies.
37960
- *
37961
- * @example
37962
- * ```typescript
37963
- * const imageService = inject(ImageService);
37964
- *
37965
- * // Compress an image
37966
- * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
37967
- *
37968
- * // Generate thumbnail
37969
- * const thumb = await imageService.thumbnail(file, 150);
37970
- *
37971
- * // Validate before processing
37972
- * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
37973
- * if (!validation.valid) {
37974
- * console.error(validation.message);
37975
- * }
37976
- * ```
37977
- */
37978
- class ImageService {
37979
- /**
37980
- * Compress an image maintaining aspect ratio
37981
- * @param file - File or Blob to compress
37982
- * @param options - Compression options
37983
- * @returns Promise with processed image data
37984
- */
37985
- async compress(file, options) {
37986
- const opts = {
37987
- maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
37988
- maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
37989
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
37990
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
37991
- };
37992
- const img = await this.loadImage(file);
37993
- const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
37994
- const canvas = document.createElement('canvas');
37995
- canvas.width = width;
37996
- canvas.height = height;
37997
- const ctx = canvas.getContext('2d');
37998
- ctx.drawImage(img, 0, 0, width, height);
37999
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
38000
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
38001
- return {
38002
- blob,
38003
- dataUrl,
38004
- width,
38005
- height,
38006
- size: blob.size,
38007
- };
38008
- }
38009
- /**
38010
- * Generate a square thumbnail from an image
38011
- * @param file - File or Blob to process
38012
- * @param size - Thumbnail size in pixels (default: 150)
38013
- * @returns Promise with processed thumbnail
38014
- */
38015
- async thumbnail(file, size) {
38016
- const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
38017
- const img = await this.loadImage(file);
38018
- // Calculate square crop from center
38019
- const minDim = Math.min(img.width, img.height);
38020
- const cropX = (img.width - minDim) / 2;
38021
- const cropY = (img.height - minDim) / 2;
38022
- const canvas = document.createElement('canvas');
38023
- canvas.width = thumbSize;
38024
- canvas.height = thumbSize;
38025
- const ctx = canvas.getContext('2d');
38026
- ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
38027
- const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
38028
- );
38029
- const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
38030
- return {
38031
- blob,
38032
- dataUrl,
38033
- width: thumbSize,
38034
- height: thumbSize,
38035
- size: blob.size,
38036
- };
38037
- }
38038
- /**
38039
- * Crop an image with specific coordinates
38040
- * @param file - File or Blob to crop
38041
- * @param cropData - Crop coordinates and dimensions
38042
- * @param options - Optional compression options for output
38043
- * @returns Promise with cropped image
38044
- */
38045
- async crop(file, cropData, options) {
38046
- const img = await this.loadImage(file);
38047
- const opts = {
38048
- quality: options?.quality ?? IMAGE_DEFAULTS.quality,
38049
- mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
38050
- };
38051
- const canvas = document.createElement('canvas');
38052
- canvas.width = cropData.width;
38053
- canvas.height = cropData.height;
38054
- const ctx = canvas.getContext('2d');
38055
- ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
38056
- // Apply max dimensions if specified
38057
- if (options?.maxWidth || options?.maxHeight) {
38058
- return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
38059
- }
38060
- const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
38061
- const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
38062
- return {
38063
- blob,
38064
- dataUrl,
38065
- width: cropData.width,
38066
- height: cropData.height,
38067
- size: blob.size,
38068
- };
38069
- }
38070
- /**
38071
- * Validate an image file before processing
38072
- * @param file - File to validate
38073
- * @param options - Validation options
38074
- * @returns Validation result with error details if invalid
38075
- */
38076
- validate(file, options) {
38077
- const opts = {
38078
- maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
38079
- allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
38080
- };
38081
- // Check file type
38082
- if (!opts.allowedTypes.includes(file.type)) {
38083
- return {
38084
- valid: false,
38085
- error: 'invalidType',
38086
- message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
38087
- };
38088
- }
38089
- // Check file size
38090
- if (file.size > opts.maxSize) {
38091
- const maxMB = Math.round(opts.maxSize / (1024 * 1024));
38092
- return {
38093
- valid: false,
38094
- error: 'fileTooLarge',
38095
- message: `La imagen es muy grande. Máximo ${maxMB}MB`,
38096
- };
38097
- }
38098
- return { valid: true };
38099
- }
38100
- /**
38101
- * Validate image dimensions (async - requires loading image)
38102
- * @param file - File to validate
38103
- * @param options - Validation options with minWidth/minHeight
38104
- * @returns Promise with validation result
38105
- */
38106
- async validateDimensions(file, options) {
38107
- const img = await this.loadImage(file);
38108
- if (options.minWidth && img.width < options.minWidth) {
38109
- return {
38110
- valid: false,
38111
- error: 'imageTooSmall',
38112
- message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
38113
- };
38114
- }
38115
- if (options.minHeight && img.height < options.minHeight) {
38116
- return {
38117
- valid: false,
38118
- error: 'imageTooSmall',
38119
- message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
38120
- };
38121
- }
38122
- return { valid: true };
38123
- }
38124
- /**
38125
- * Convert a Blob/File to a data URL
38126
- */
38127
- async toDataUrl(file) {
38128
- return new Promise((resolve, reject) => {
38129
- const reader = new FileReader();
38130
- reader.onload = () => resolve(reader.result);
38131
- reader.onerror = reject;
38132
- reader.readAsDataURL(file);
38133
- });
38134
- }
38135
- /**
38136
- * Convert a data URL to a Blob
38137
- */
38138
- dataUrlToBlob(dataUrl) {
38139
- const arr = dataUrl.split(',');
38140
- const mime = arr[0].match(/:(.*?);/)[1];
38141
- const bstr = atob(arr[1]);
38142
- let n = bstr.length;
38143
- const u8arr = new Uint8Array(n);
38144
- while (n--) {
38145
- u8arr[n] = bstr.charCodeAt(n);
38146
- }
38147
- return new Blob([u8arr], { type: mime });
38148
- }
38149
- // ============== Private Helpers ==============
38150
- loadImage(file) {
38151
- return new Promise((resolve, reject) => {
38152
- const img = new Image();
38153
- img.onload = () => {
38154
- URL.revokeObjectURL(img.src);
38155
- resolve(img);
38156
- };
38157
- img.onerror = reject;
38158
- img.src = URL.createObjectURL(file);
38159
- });
38160
- }
38161
- calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
38162
- let width = originalWidth;
38163
- let height = originalHeight;
38164
- // Scale down if necessary, maintaining aspect ratio
38165
- if (width > maxWidth) {
38166
- height = (height * maxWidth) / width;
38167
- width = maxWidth;
38168
- }
38169
- if (height > maxHeight) {
38170
- width = (width * maxHeight) / height;
38171
- height = maxHeight;
38172
- }
38173
- return {
38174
- width: Math.round(width),
38175
- height: Math.round(height),
38176
- };
38177
- }
38178
- canvasToBlob(canvas, mimeType, quality) {
38179
- return new Promise((resolve, reject) => {
38180
- canvas.toBlob((blob) => {
38181
- if (blob)
38182
- resolve(blob);
38183
- else
38184
- reject(new Error('Failed to create blob from canvas'));
38185
- }, mimeType, quality);
38186
- });
38187
- }
38188
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
38189
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
38190
- }
38191
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
38192
- type: Injectable,
38193
- args: [{ providedIn: 'root' }]
38194
- }] });
38195
-
38196
38246
  class AttachmentUploaderComponent {
38197
38247
  get readyUrls() {
38198
38248
  return this.attachments()