valtech-components 2.0.681 → 2.0.683

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/esm2022/lib/components/molecules/image-crop/image-crop.component.mjs +174 -0
  2. package/esm2022/lib/components/molecules/image-crop/index.mjs +2 -0
  3. package/esm2022/lib/components/organisms/avatar-upload/avatar-upload.component.mjs +345 -0
  4. package/esm2022/lib/components/organisms/avatar-upload/types.mjs +15 -0
  5. package/esm2022/lib/components/templates/docs-page/docs-page.component.mjs +35 -4
  6. package/esm2022/lib/components/templates/docs-page/types.mjs +1 -1
  7. package/esm2022/lib/services/auth/auth.service.mjs +11 -2
  8. package/esm2022/lib/services/auth/types.mjs +1 -1
  9. package/esm2022/lib/services/feedback/feedback.service.mjs +24 -21
  10. package/esm2022/lib/services/image/image.service.mjs +244 -0
  11. package/esm2022/lib/services/image/index.mjs +3 -0
  12. package/esm2022/lib/services/image/types.mjs +13 -0
  13. package/esm2022/lib/version.mjs +2 -2
  14. package/esm2022/public-api.mjs +7 -1
  15. package/fesm2022/valtech-components.mjs +838 -27
  16. package/fesm2022/valtech-components.mjs.map +1 -1
  17. package/lib/components/molecules/image-crop/image-crop.component.d.ts +59 -0
  18. package/lib/components/molecules/image-crop/index.d.ts +1 -0
  19. package/lib/components/organisms/avatar-upload/avatar-upload.component.d.ts +82 -0
  20. package/lib/components/organisms/avatar-upload/types.d.ts +62 -0
  21. package/lib/components/templates/docs-page/docs-page.component.d.ts +3 -0
  22. package/lib/components/templates/docs-page/types.d.ts +39 -0
  23. package/lib/services/auth/auth.service.d.ts +6 -1
  24. package/lib/services/auth/types.d.ts +18 -0
  25. package/lib/services/image/image.service.d.ts +76 -0
  26. package/lib/services/image/index.d.ts +2 -0
  27. package/lib/services/image/types.d.ts +74 -0
  28. package/lib/version.d.ts +1 -1
  29. package/package.json +2 -1
  30. package/public-api.d.ts +4 -0
  31. package/src/lib/services/firebase/firebase-messaging-sw.js +134 -0
@@ -5,7 +5,7 @@ import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal,
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgStyle, NgFor, isPlatformBrowser, NgClass } from '@angular/common';
7
7
  import { addIcons } from 'ionicons';
8
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
8
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, cameraOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
9
9
  import * as i1$1 from '@angular/router';
10
10
  import { Router, NavigationEnd, RouterLink, RouterOutlet, RouterModule } from '@angular/router';
11
11
  import { Browser } from '@capacitor/browser';
@@ -39,6 +39,7 @@ import * as i1$7 from '@angular/fire/storage';
39
39
  import { provideStorage, getStorage, connectStorageEmulator, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
40
40
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
41
41
  import { filter, map as map$1, catchError as catchError$1, tap, switchMap as switchMap$1, finalize, take, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
42
+ import { ImageCropperComponent } from 'ngx-image-cropper';
42
43
  import * as i1$8 from '@angular/common/http';
43
44
  import { provideHttpClient, withInterceptors, HttpClient } from '@angular/common/http';
44
45
  import { Capacitor } from '@capacitor/core';
@@ -49,7 +50,7 @@ import 'prismjs/components/prism-json';
49
50
  * Current version of valtech-components.
50
51
  * This is automatically updated during the publish process.
51
52
  */
52
- const VERSION = '2.0.681';
53
+ const VERSION = '2.0.683';
53
54
 
54
55
  /**
55
56
  * Servicio para gestionar presets de componentes.
@@ -21196,6 +21197,174 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
21196
21197
  type: Input
21197
21198
  }] } });
21198
21199
 
21200
+ /**
21201
+ * ImageCropComponent
21202
+ *
21203
+ * A modal-ready component for cropping images with a specified aspect ratio.
21204
+ * Uses ngx-image-cropper internally and provides a simple interface.
21205
+ *
21206
+ * @example Inside an ion-modal
21207
+ * ```html
21208
+ * <ion-modal [isOpen]="showCropModal">
21209
+ * <ng-template>
21210
+ * <val-image-crop
21211
+ * [image]="selectedFile"
21212
+ * [aspectRatio]="1"
21213
+ * [roundCropper]="true"
21214
+ * (cropComplete)="onCropComplete($event)"
21215
+ * (cancel)="showCropModal = false"
21216
+ * />
21217
+ * </ng-template>
21218
+ * </ion-modal>
21219
+ * ```
21220
+ */
21221
+ class ImageCropComponent {
21222
+ constructor() {
21223
+ this.i18n = inject(I18nService);
21224
+ /** Image file to crop */
21225
+ this.image = input.required();
21226
+ /** Aspect ratio (1 for square, 16/9 for widescreen, etc.) */
21227
+ this.aspectRatio = input(1);
21228
+ /** Use round cropper (for avatars) */
21229
+ this.roundCropper = input(true);
21230
+ /** Resize output to specific width (0 = no resize) */
21231
+ this.resizeToWidth = input(0);
21232
+ /** i18n namespace for labels */
21233
+ this.i18nNamespace = input('ImageCrop');
21234
+ /** Emitted when crop is confirmed with the cropped blob */
21235
+ this.cropComplete = new EventEmitter();
21236
+ /** Emitted when user cancels the crop */
21237
+ this.cancel = new EventEmitter();
21238
+ /** Emitted when image fails to load */
21239
+ this.loadFailed = new EventEmitter();
21240
+ /** Internal signal for cropped blob */
21241
+ this.croppedBlob = signal(null);
21242
+ /** Computed text for cancel button */
21243
+ this.cancelText = computed(() => {
21244
+ this.i18n.lang(); // Track language changes
21245
+ return this.i18n.t('cancel', 'Common') || 'Cancelar';
21246
+ });
21247
+ /** Computed text for confirm button */
21248
+ this.confirmText = computed(() => {
21249
+ this.i18n.lang();
21250
+ return this.i18n.t('confirm', 'Common') || 'Confirmar';
21251
+ });
21252
+ /** Computed text for title */
21253
+ this.titleText = computed(() => {
21254
+ this.i18n.lang();
21255
+ return this.i18n.t('cropImage', this.i18nNamespace()) || 'Recortar imagen';
21256
+ });
21257
+ }
21258
+ /** Handle crop event from ngx-image-cropper */
21259
+ onImageCropped(event) {
21260
+ if (event.blob) {
21261
+ this.croppedBlob.set(event.blob);
21262
+ }
21263
+ }
21264
+ /** Confirm and emit the cropped blob */
21265
+ confirmCrop() {
21266
+ const blob = this.croppedBlob();
21267
+ if (blob) {
21268
+ this.cropComplete.emit(blob);
21269
+ }
21270
+ }
21271
+ /** Handle load failure */
21272
+ onLoadFailed() {
21273
+ this.loadFailed.emit();
21274
+ }
21275
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageCropComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
21276
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: ImageCropComponent, isStandalone: true, selector: "val-image-crop", inputs: { image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: true, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, roundCropper: { classPropertyName: "roundCropper", publicName: "roundCropper", isSignal: true, isRequired: false, transformFunction: null }, resizeToWidth: { classPropertyName: "resizeToWidth", publicName: "resizeToWidth", isSignal: true, isRequired: false, transformFunction: null }, i18nNamespace: { classPropertyName: "i18nNamespace", publicName: "i18nNamespace", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropComplete: "cropComplete", cancel: "cancel", loadFailed: "loadFailed" }, ngImport: i0, template: `
21277
+ <ion-header>
21278
+ <ion-toolbar>
21279
+ <ion-buttons slot="start">
21280
+ <ion-button (click)="cancel.emit()" color="medium">
21281
+ {{ cancelText() }}
21282
+ </ion-button>
21283
+ </ion-buttons>
21284
+ <ion-title>{{ titleText() }}</ion-title>
21285
+ <ion-buttons slot="end">
21286
+ <ion-button
21287
+ (click)="confirmCrop()"
21288
+ color="primary"
21289
+ [strong]="true"
21290
+ [disabled]="!croppedBlob()"
21291
+ >
21292
+ {{ confirmText() }}
21293
+ </ion-button>
21294
+ </ion-buttons>
21295
+ </ion-toolbar>
21296
+ </ion-header>
21297
+
21298
+ <ion-content class="image-crop-content">
21299
+ <image-cropper
21300
+ [imageFile]="image()"
21301
+ [aspectRatio]="aspectRatio()"
21302
+ [maintainAspectRatio]="true"
21303
+ [roundCropper]="roundCropper()"
21304
+ [resizeToWidth]="resizeToWidth()"
21305
+ format="jpeg"
21306
+ outputType="blob"
21307
+ (imageCropped)="onImageCropped($event)"
21308
+ (loadImageFailed)="onLoadFailed()"
21309
+ />
21310
+ </ion-content>
21311
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%}.image-crop-content{--background: var(--ion-color-dark)}.image-crop-content::part(scroll){display:flex;flex-direction:column}image-cropper{--cropper-outline-color: rgba(255, 255, 255, .3);--cropper-background-color: var(--ion-color-dark);flex:1;height:100%;max-height:calc(100vh - 56px)}::ng-deep .ngx-ic-component{height:100%!important}::ng-deep .ngx-ic-source-image{max-height:100%!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: ImageCropperComponent, selector: "image-cropper", inputs: ["imageChangedEvent", "imageURL", "imageBase64", "imageFile", "imageAltText", "options", "cropperFrameAriaLabel", "output", "format", "autoCrop", "cropper", "transform", "maintainAspectRatio", "aspectRatio", "resetCropOnAspectRatioChange", "resizeToWidth", "resizeToHeight", "cropperMinWidth", "cropperMinHeight", "cropperMaxHeight", "cropperMaxWidth", "cropperStaticWidth", "cropperStaticHeight", "canvasRotation", "initialStepSize", "roundCropper", "onlyScaleDown", "imageQuality", "backgroundColor", "containWithinAspectRatio", "hideResizeSquares", "allowMoveImage", "checkImageType", "alignImage", "disabled", "hidden"], outputs: ["imageCropped", "startCropImage", "imageLoaded", "cropperReady", "loadImageFailed", "transformChange", "cropperChange"] }] }); }
21312
+ }
21313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageCropComponent, decorators: [{
21314
+ type: Component,
21315
+ args: [{ selector: 'val-image-crop', standalone: true, imports: [
21316
+ CommonModule,
21317
+ IonHeader,
21318
+ IonToolbar,
21319
+ IonTitle,
21320
+ IonButtons,
21321
+ IonButton,
21322
+ IonContent,
21323
+ ImageCropperComponent,
21324
+ ], template: `
21325
+ <ion-header>
21326
+ <ion-toolbar>
21327
+ <ion-buttons slot="start">
21328
+ <ion-button (click)="cancel.emit()" color="medium">
21329
+ {{ cancelText() }}
21330
+ </ion-button>
21331
+ </ion-buttons>
21332
+ <ion-title>{{ titleText() }}</ion-title>
21333
+ <ion-buttons slot="end">
21334
+ <ion-button
21335
+ (click)="confirmCrop()"
21336
+ color="primary"
21337
+ [strong]="true"
21338
+ [disabled]="!croppedBlob()"
21339
+ >
21340
+ {{ confirmText() }}
21341
+ </ion-button>
21342
+ </ion-buttons>
21343
+ </ion-toolbar>
21344
+ </ion-header>
21345
+
21346
+ <ion-content class="image-crop-content">
21347
+ <image-cropper
21348
+ [imageFile]="image()"
21349
+ [aspectRatio]="aspectRatio()"
21350
+ [maintainAspectRatio]="true"
21351
+ [roundCropper]="roundCropper()"
21352
+ [resizeToWidth]="resizeToWidth()"
21353
+ format="jpeg"
21354
+ outputType="blob"
21355
+ (imageCropped)="onImageCropped($event)"
21356
+ (loadImageFailed)="onLoadFailed()"
21357
+ />
21358
+ </ion-content>
21359
+ `, styles: [":host{display:flex;flex-direction:column;height:100%}.image-crop-content{--background: var(--ion-color-dark)}.image-crop-content::part(scroll){display:flex;flex-direction:column}image-cropper{--cropper-outline-color: rgba(255, 255, 255, .3);--cropper-background-color: var(--ion-color-dark);flex:1;height:100%;max-height:calc(100vh - 56px)}::ng-deep .ngx-ic-component{height:100%!important}::ng-deep .ngx-ic-source-image{max-height:100%!important}\n"] }]
21360
+ }], propDecorators: { cropComplete: [{
21361
+ type: Output
21362
+ }], cancel: [{
21363
+ type: Output
21364
+ }], loadFailed: [{
21365
+ type: Output
21366
+ }] } });
21367
+
21199
21368
  /**
21200
21369
  * Configuración de espaciado predefinida
21201
21370
  */
@@ -30585,6 +30754,15 @@ class AuthService {
30585
30754
  .put(`${this.baseUrl}/profile`, request)
30586
30755
  .pipe(catchError$1(error => this.handleAuthError(error)));
30587
30756
  }
30757
+ /**
30758
+ * Actualiza el avatar del usuario en el backend.
30759
+ * Nota: El estado local del avatar se maneja a través de getProfile().
30760
+ */
30761
+ updateAvatar(request) {
30762
+ return this.http
30763
+ .put(`${this.baseUrl}/profile/avatar`, request)
30764
+ .pipe(catchError$1(error => this.handleAuthError(error)));
30765
+ }
30588
30766
  // =============================================
30589
30767
  // RECUPERACIÓN DE CONTRASEÑA
30590
30768
  // =============================================
@@ -30707,7 +30885,7 @@ class AuthService {
30707
30885
  */
30708
30886
  updateHandle(handle) {
30709
30887
  return this.http
30710
- .put(`${this.baseUrl}/handle`, { handle })
30888
+ .put(`${this.baseUrl}/profile/handle`, { handle })
30711
30889
  .pipe(catchError$1(error => this.handleAuthError(error)));
30712
30890
  }
30713
30891
  /**
@@ -33501,6 +33679,607 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
33501
33679
  type: Output
33502
33680
  }] } });
33503
33681
 
33682
+ /**
33683
+ * Default values for image processing
33684
+ */
33685
+ const IMAGE_DEFAULTS = {
33686
+ maxWidth: 800,
33687
+ maxHeight: 800,
33688
+ quality: 0.8,
33689
+ mimeType: 'image/jpeg',
33690
+ maxSize: 10 * 1024 * 1024, // 10MB
33691
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
33692
+ thumbnailSize: 150,
33693
+ };
33694
+
33695
+ /**
33696
+ * ImageService
33697
+ *
33698
+ * Service for image processing including compression, thumbnails, cropping and validation.
33699
+ * Uses HTML Canvas for all operations - no external dependencies.
33700
+ *
33701
+ * @example
33702
+ * ```typescript
33703
+ * const imageService = inject(ImageService);
33704
+ *
33705
+ * // Compress an image
33706
+ * const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
33707
+ *
33708
+ * // Generate thumbnail
33709
+ * const thumb = await imageService.thumbnail(file, 150);
33710
+ *
33711
+ * // Validate before processing
33712
+ * const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
33713
+ * if (!validation.valid) {
33714
+ * console.error(validation.message);
33715
+ * }
33716
+ * ```
33717
+ */
33718
+ class ImageService {
33719
+ /**
33720
+ * Compress an image maintaining aspect ratio
33721
+ * @param file - File or Blob to compress
33722
+ * @param options - Compression options
33723
+ * @returns Promise with processed image data
33724
+ */
33725
+ async compress(file, options) {
33726
+ const opts = {
33727
+ maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
33728
+ maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
33729
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
33730
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
33731
+ };
33732
+ const img = await this.loadImage(file);
33733
+ const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
33734
+ const canvas = document.createElement('canvas');
33735
+ canvas.width = width;
33736
+ canvas.height = height;
33737
+ const ctx = canvas.getContext('2d');
33738
+ ctx.drawImage(img, 0, 0, width, height);
33739
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
33740
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
33741
+ return {
33742
+ blob,
33743
+ dataUrl,
33744
+ width,
33745
+ height,
33746
+ size: blob.size,
33747
+ };
33748
+ }
33749
+ /**
33750
+ * Generate a square thumbnail from an image
33751
+ * @param file - File or Blob to process
33752
+ * @param size - Thumbnail size in pixels (default: 150)
33753
+ * @returns Promise with processed thumbnail
33754
+ */
33755
+ async thumbnail(file, size) {
33756
+ const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
33757
+ const img = await this.loadImage(file);
33758
+ // Calculate square crop from center
33759
+ const minDim = Math.min(img.width, img.height);
33760
+ const cropX = (img.width - minDim) / 2;
33761
+ const cropY = (img.height - minDim) / 2;
33762
+ const canvas = document.createElement('canvas');
33763
+ canvas.width = thumbSize;
33764
+ canvas.height = thumbSize;
33765
+ const ctx = canvas.getContext('2d');
33766
+ ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
33767
+ const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
33768
+ );
33769
+ const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
33770
+ return {
33771
+ blob,
33772
+ dataUrl,
33773
+ width: thumbSize,
33774
+ height: thumbSize,
33775
+ size: blob.size,
33776
+ };
33777
+ }
33778
+ /**
33779
+ * Crop an image with specific coordinates
33780
+ * @param file - File or Blob to crop
33781
+ * @param cropData - Crop coordinates and dimensions
33782
+ * @param options - Optional compression options for output
33783
+ * @returns Promise with cropped image
33784
+ */
33785
+ async crop(file, cropData, options) {
33786
+ const img = await this.loadImage(file);
33787
+ const opts = {
33788
+ quality: options?.quality ?? IMAGE_DEFAULTS.quality,
33789
+ mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
33790
+ };
33791
+ const canvas = document.createElement('canvas');
33792
+ canvas.width = cropData.width;
33793
+ canvas.height = cropData.height;
33794
+ const ctx = canvas.getContext('2d');
33795
+ ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
33796
+ // Apply max dimensions if specified
33797
+ if (options?.maxWidth || options?.maxHeight) {
33798
+ return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
33799
+ }
33800
+ const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
33801
+ const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
33802
+ return {
33803
+ blob,
33804
+ dataUrl,
33805
+ width: cropData.width,
33806
+ height: cropData.height,
33807
+ size: blob.size,
33808
+ };
33809
+ }
33810
+ /**
33811
+ * Validate an image file before processing
33812
+ * @param file - File to validate
33813
+ * @param options - Validation options
33814
+ * @returns Validation result with error details if invalid
33815
+ */
33816
+ validate(file, options) {
33817
+ const opts = {
33818
+ maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
33819
+ allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
33820
+ };
33821
+ // Check file type
33822
+ if (!opts.allowedTypes.includes(file.type)) {
33823
+ return {
33824
+ valid: false,
33825
+ error: 'invalidType',
33826
+ message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
33827
+ };
33828
+ }
33829
+ // Check file size
33830
+ if (file.size > opts.maxSize) {
33831
+ const maxMB = Math.round(opts.maxSize / (1024 * 1024));
33832
+ return {
33833
+ valid: false,
33834
+ error: 'fileTooLarge',
33835
+ message: `La imagen es muy grande. Máximo ${maxMB}MB`,
33836
+ };
33837
+ }
33838
+ return { valid: true };
33839
+ }
33840
+ /**
33841
+ * Validate image dimensions (async - requires loading image)
33842
+ * @param file - File to validate
33843
+ * @param options - Validation options with minWidth/minHeight
33844
+ * @returns Promise with validation result
33845
+ */
33846
+ async validateDimensions(file, options) {
33847
+ const img = await this.loadImage(file);
33848
+ if (options.minWidth && img.width < options.minWidth) {
33849
+ return {
33850
+ valid: false,
33851
+ error: 'imageTooSmall',
33852
+ message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
33853
+ };
33854
+ }
33855
+ if (options.minHeight && img.height < options.minHeight) {
33856
+ return {
33857
+ valid: false,
33858
+ error: 'imageTooSmall',
33859
+ message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
33860
+ };
33861
+ }
33862
+ return { valid: true };
33863
+ }
33864
+ /**
33865
+ * Convert a Blob/File to a data URL
33866
+ */
33867
+ async toDataUrl(file) {
33868
+ return new Promise((resolve, reject) => {
33869
+ const reader = new FileReader();
33870
+ reader.onload = () => resolve(reader.result);
33871
+ reader.onerror = reject;
33872
+ reader.readAsDataURL(file);
33873
+ });
33874
+ }
33875
+ /**
33876
+ * Convert a data URL to a Blob
33877
+ */
33878
+ dataUrlToBlob(dataUrl) {
33879
+ const arr = dataUrl.split(',');
33880
+ const mime = arr[0].match(/:(.*?);/)[1];
33881
+ const bstr = atob(arr[1]);
33882
+ let n = bstr.length;
33883
+ const u8arr = new Uint8Array(n);
33884
+ while (n--) {
33885
+ u8arr[n] = bstr.charCodeAt(n);
33886
+ }
33887
+ return new Blob([u8arr], { type: mime });
33888
+ }
33889
+ // ============== Private Helpers ==============
33890
+ loadImage(file) {
33891
+ return new Promise((resolve, reject) => {
33892
+ const img = new Image();
33893
+ img.onload = () => {
33894
+ URL.revokeObjectURL(img.src);
33895
+ resolve(img);
33896
+ };
33897
+ img.onerror = reject;
33898
+ img.src = URL.createObjectURL(file);
33899
+ });
33900
+ }
33901
+ calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
33902
+ let width = originalWidth;
33903
+ let height = originalHeight;
33904
+ // Scale down if necessary, maintaining aspect ratio
33905
+ if (width > maxWidth) {
33906
+ height = (height * maxWidth) / width;
33907
+ width = maxWidth;
33908
+ }
33909
+ if (height > maxHeight) {
33910
+ width = (width * maxHeight) / height;
33911
+ height = maxHeight;
33912
+ }
33913
+ return {
33914
+ width: Math.round(width),
33915
+ height: Math.round(height),
33916
+ };
33917
+ }
33918
+ canvasToBlob(canvas, mimeType, quality) {
33919
+ return new Promise((resolve, reject) => {
33920
+ canvas.toBlob((blob) => {
33921
+ if (blob)
33922
+ resolve(blob);
33923
+ else
33924
+ reject(new Error('Failed to create blob from canvas'));
33925
+ }, mimeType, quality);
33926
+ });
33927
+ }
33928
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
33929
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
33930
+ }
33931
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
33932
+ type: Injectable,
33933
+ args: [{ providedIn: 'root' }]
33934
+ }] });
33935
+
33936
+ /**
33937
+ * Default values
33938
+ */
33939
+ const AVATAR_UPLOAD_DEFAULTS = {
33940
+ size: 100,
33941
+ editable: true,
33942
+ storagePath: 'avatars',
33943
+ i18nNamespace: 'AvatarUpload',
33944
+ maxFileSize: 10 * 1024 * 1024, // 10MB
33945
+ compressQuality: 0.8,
33946
+ maxWidth: 800,
33947
+ thumbnailSize: 150,
33948
+ backgroundColor: '#6366f1', // Indigo
33949
+ };
33950
+
33951
+ addIcons({ cameraOutline });
33952
+ /**
33953
+ * AvatarUploadComponent
33954
+ *
33955
+ * A complete avatar upload solution with:
33956
+ * - Image selection from device
33957
+ * - Crop modal with round preview
33958
+ * - Automatic compression and thumbnail generation
33959
+ * - Upload to Firebase Storage
33960
+ * - Backend sync via AuthService
33961
+ *
33962
+ * @example Basic usage
33963
+ * ```html
33964
+ * <val-avatar-upload
33965
+ * [props]="{
33966
+ * currentUrl: user()?.avatarUrl,
33967
+ * initials: 'JD',
33968
+ * size: 120
33969
+ * }"
33970
+ * (uploaded)="onAvatarUploaded($event)"
33971
+ * (error)="onError($event)"
33972
+ * />
33973
+ * ```
33974
+ */
33975
+ class AvatarUploadComponent {
33976
+ constructor() {
33977
+ this.imageService = inject(ImageService);
33978
+ this.storageService = inject(StorageService);
33979
+ this.authService = inject(AuthService);
33980
+ this.i18n = inject(I18nService);
33981
+ /** Component configuration */
33982
+ this.props = input({});
33983
+ /** Emitted after successful upload and backend sync */
33984
+ this.uploaded = new EventEmitter();
33985
+ /** Emitted on any error during the process */
33986
+ this.error = new EventEmitter();
33987
+ /** Emitted when upload starts */
33988
+ this.uploadStart = new EventEmitter();
33989
+ // Internal state
33990
+ this.loading = signal(false);
33991
+ this.showCropModal = signal(false);
33992
+ this.selectedFile = signal(null);
33993
+ this.previewUrl = signal(null);
33994
+ this.imageLoadError = signal(false);
33995
+ /** Merged config with defaults */
33996
+ this.config = computed(() => ({
33997
+ ...AVATAR_UPLOAD_DEFAULTS,
33998
+ ...this.props(),
33999
+ }));
34000
+ /** URL to display (preview takes priority over current) */
34001
+ this.displayUrl = computed(() => {
34002
+ if (this.imageLoadError())
34003
+ return null;
34004
+ return this.previewUrl() || this.config().currentUrl || null;
34005
+ });
34006
+ /** Aria label for edit button */
34007
+ this.editButtonLabel = computed(() => {
34008
+ this.i18n.lang();
34009
+ return this.i18n.t('changePhoto', this.config().i18nNamespace) || 'Cambiar foto';
34010
+ });
34011
+ }
34012
+ /** Open file picker dialog */
34013
+ openFilePicker() {
34014
+ this.fileInput.nativeElement.click();
34015
+ }
34016
+ /** Handle file selection */
34017
+ onFileSelected(event) {
34018
+ const input = event.target;
34019
+ const file = input.files?.[0];
34020
+ if (!file)
34021
+ return;
34022
+ // Reset input for same file selection
34023
+ input.value = '';
34024
+ // Validate file
34025
+ const validation = this.imageService.validate(file, {
34026
+ maxSize: this.config().maxFileSize,
34027
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
34028
+ });
34029
+ if (!validation.valid) {
34030
+ this.emitError(validation.error, validation.message);
34031
+ return;
34032
+ }
34033
+ // Open crop modal
34034
+ this.selectedFile.set(file);
34035
+ this.showCropModal.set(true);
34036
+ }
34037
+ /** Handle crop completion */
34038
+ async onCropComplete(croppedBlob) {
34039
+ this.showCropModal.set(false);
34040
+ this.selectedFile.set(null);
34041
+ await this.processAndUpload(croppedBlob);
34042
+ }
34043
+ /** Handle crop cancel */
34044
+ onCropCancel() {
34045
+ this.showCropModal.set(false);
34046
+ this.selectedFile.set(null);
34047
+ }
34048
+ /** Handle crop load failure */
34049
+ onCropLoadFailed() {
34050
+ this.showCropModal.set(false);
34051
+ this.selectedFile.set(null);
34052
+ this.emitError('invalidType', this.i18n.t('loadFailed', this.config().i18nNamespace) || 'No se pudo cargar la imagen');
34053
+ }
34054
+ /** Handle image load error */
34055
+ onImageError() {
34056
+ this.imageLoadError.set(true);
34057
+ }
34058
+ /** Process cropped image and upload */
34059
+ async processAndUpload(croppedBlob) {
34060
+ this.loading.set(true);
34061
+ this.uploadStart.emit();
34062
+ try {
34063
+ const config = this.config();
34064
+ // 1. Compress image
34065
+ const compressed = await this.imageService.compress(croppedBlob, {
34066
+ maxWidth: config.maxWidth,
34067
+ maxHeight: config.maxWidth,
34068
+ quality: config.compressQuality,
34069
+ });
34070
+ // 2. Generate thumbnail
34071
+ const thumbnail = await this.imageService.thumbnail(compressed.blob, config.thumbnailSize);
34072
+ // 3. Set preview immediately
34073
+ this.previewUrl.set(compressed.dataUrl);
34074
+ this.imageLoadError.set(false);
34075
+ // 4. Get user ID for storage path
34076
+ const userId = this.authService.user()?.userId;
34077
+ if (!userId) {
34078
+ throw new Error('User not authenticated');
34079
+ }
34080
+ // 5. Upload to Firebase Storage
34081
+ const timestamp = Date.now();
34082
+ const avatarPath = `${config.storagePath}/${userId}/avatar_${timestamp}.jpg`;
34083
+ const thumbPath = `${config.storagePath}/${userId}/thumb_${timestamp}.jpg`;
34084
+ const [avatarResult, thumbResult] = await Promise.all([
34085
+ this.storageService.uploadAndGetUrl(avatarPath, compressed.blob),
34086
+ this.storageService.uploadAndGetUrl(thumbPath, thumbnail.blob),
34087
+ ]);
34088
+ // 6. Update backend
34089
+ await firstValueFrom(this.authService.updateAvatar({
34090
+ avatarUrl: avatarResult.downloadUrl,
34091
+ avatarThumbnail: thumbResult.downloadUrl,
34092
+ }));
34093
+ // 7. Emit success
34094
+ const result = {
34095
+ avatarUrl: avatarResult.downloadUrl,
34096
+ thumbnailUrl: thumbResult.downloadUrl,
34097
+ };
34098
+ this.uploaded.emit(result);
34099
+ }
34100
+ catch (err) {
34101
+ // Revert preview on error
34102
+ this.previewUrl.set(null);
34103
+ const message = err instanceof Error
34104
+ ? err.message
34105
+ : this.i18n.t('uploadError', this.config().i18nNamespace) || 'Error al subir la imagen';
34106
+ this.emitError('uploadFailed', message, err);
34107
+ }
34108
+ finally {
34109
+ this.loading.set(false);
34110
+ }
34111
+ }
34112
+ /** Emit error event */
34113
+ emitError(type, message, originalError) {
34114
+ this.error.emit({ type, message, originalError });
34115
+ }
34116
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AvatarUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
34117
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AvatarUploadComponent, isStandalone: true, selector: "val-avatar-upload", inputs: { props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploaded: "uploaded", error: "error", uploadStart: "uploadStart" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
34118
+ <div
34119
+ class="avatar-upload"
34120
+ [style.--avatar-size.px]="config().size"
34121
+ [class.avatar-upload--loading]="loading()"
34122
+ >
34123
+ <div class="avatar-container">
34124
+ <!-- Avatar Image or Initials -->
34125
+ @if (displayUrl()) {
34126
+ <img
34127
+ class="avatar-image"
34128
+ [src]="displayUrl()"
34129
+ alt="Avatar"
34130
+ (error)="onImageError()"
34131
+ />
34132
+ } @else {
34133
+ <div
34134
+ class="avatar-initials"
34135
+ [style.background-color]="config().backgroundColor"
34136
+ >
34137
+ {{ config().initials || '?' }}
34138
+ </div>
34139
+ }
34140
+
34141
+ <!-- Edit Button -->
34142
+ @if (config().editable && !loading()) {
34143
+ <button
34144
+ class="edit-button"
34145
+ type="button"
34146
+ (click)="openFilePicker()"
34147
+ [attr.aria-label]="editButtonLabel()"
34148
+ >
34149
+ <ion-icon name="camera-outline"></ion-icon>
34150
+ </button>
34151
+ }
34152
+
34153
+ <!-- Loading Overlay -->
34154
+ @if (loading()) {
34155
+ <div class="loading-overlay">
34156
+ <ion-spinner name="crescent"></ion-spinner>
34157
+ </div>
34158
+ }
34159
+ </div>
34160
+
34161
+ <!-- Hidden File Input -->
34162
+ <input
34163
+ #fileInput
34164
+ type="file"
34165
+ accept="image/jpeg,image/png,image/webp"
34166
+ (change)="onFileSelected($event)"
34167
+ hidden
34168
+ />
34169
+
34170
+ <!-- Crop Modal -->
34171
+ <ion-modal
34172
+ [isOpen]="showCropModal()"
34173
+ (didDismiss)="onCropCancel()"
34174
+ [breakpoints]="[0, 1]"
34175
+ [initialBreakpoint]="1"
34176
+ >
34177
+ <ng-template>
34178
+ @if (selectedFile()) {
34179
+ <val-image-crop
34180
+ [image]="selectedFile()!"
34181
+ [aspectRatio]="1"
34182
+ [roundCropper]="true"
34183
+ [i18nNamespace]="config().i18nNamespace"
34184
+ (cropComplete)="onCropComplete($event)"
34185
+ (cancel)="onCropCancel()"
34186
+ (loadFailed)="onCropLoadFailed()"
34187
+ />
34188
+ }
34189
+ </ng-template>
34190
+ </ion-modal>
34191
+ </div>
34192
+ `, isInline: true, styles: [".avatar-upload{--avatar-size: 100px;--edit-button-size: 32px;--edit-button-offset: 4px;display:inline-block}.avatar-container{position:relative;width:var(--avatar-size);height:var(--avatar-size);border-radius:50%;overflow:visible}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;background-color:var(--ion-color-light)}.avatar-initials{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:calc(var(--avatar-size) * .4);font-weight:600;color:#fff;text-transform:uppercase;-webkit-user-select:none;user-select:none}.edit-button{position:absolute;bottom:var(--edit-button-offset);right:var(--edit-button-offset);width:var(--edit-button-size);height:var(--edit-button-size);border-radius:50%;border:2px solid white;background:var(--ion-color-primary);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s ease,background-color .2s ease;box-shadow:0 2px 8px #00000026}.edit-button ion-icon{font-size:calc(var(--edit-button-size) * .5)}.edit-button:hover{transform:scale(1.1);background:var(--ion-color-primary-shade)}.edit-button:active{transform:scale(.95)}.edit-button:focus-visible{outline:2px solid var(--ion-color-primary);outline-offset:2px}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;background:#00000080;display:flex;align-items:center;justify-content:center}.loading-overlay ion-spinner{--color: white;width:calc(var(--avatar-size) * .4);height:calc(var(--avatar-size) * .4)}.avatar-upload--loading .edit-button{display:none}.avatar-upload--loading .avatar-image,.avatar-upload--loading .avatar-initials{filter:brightness(.7)}@container (max-width: 60px){.edit-button{--edit-button-size: 24px;--edit-button-offset: 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: ImageCropComponent, selector: "val-image-crop", inputs: ["image", "aspectRatio", "roundCropper", "resizeToWidth", "i18nNamespace"], outputs: ["cropComplete", "cancel", "loadFailed"] }] }); }
34193
+ }
34194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AvatarUploadComponent, decorators: [{
34195
+ type: Component,
34196
+ args: [{ selector: 'val-avatar-upload', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, IonModal, ImageCropComponent], template: `
34197
+ <div
34198
+ class="avatar-upload"
34199
+ [style.--avatar-size.px]="config().size"
34200
+ [class.avatar-upload--loading]="loading()"
34201
+ >
34202
+ <div class="avatar-container">
34203
+ <!-- Avatar Image or Initials -->
34204
+ @if (displayUrl()) {
34205
+ <img
34206
+ class="avatar-image"
34207
+ [src]="displayUrl()"
34208
+ alt="Avatar"
34209
+ (error)="onImageError()"
34210
+ />
34211
+ } @else {
34212
+ <div
34213
+ class="avatar-initials"
34214
+ [style.background-color]="config().backgroundColor"
34215
+ >
34216
+ {{ config().initials || '?' }}
34217
+ </div>
34218
+ }
34219
+
34220
+ <!-- Edit Button -->
34221
+ @if (config().editable && !loading()) {
34222
+ <button
34223
+ class="edit-button"
34224
+ type="button"
34225
+ (click)="openFilePicker()"
34226
+ [attr.aria-label]="editButtonLabel()"
34227
+ >
34228
+ <ion-icon name="camera-outline"></ion-icon>
34229
+ </button>
34230
+ }
34231
+
34232
+ <!-- Loading Overlay -->
34233
+ @if (loading()) {
34234
+ <div class="loading-overlay">
34235
+ <ion-spinner name="crescent"></ion-spinner>
34236
+ </div>
34237
+ }
34238
+ </div>
34239
+
34240
+ <!-- Hidden File Input -->
34241
+ <input
34242
+ #fileInput
34243
+ type="file"
34244
+ accept="image/jpeg,image/png,image/webp"
34245
+ (change)="onFileSelected($event)"
34246
+ hidden
34247
+ />
34248
+
34249
+ <!-- Crop Modal -->
34250
+ <ion-modal
34251
+ [isOpen]="showCropModal()"
34252
+ (didDismiss)="onCropCancel()"
34253
+ [breakpoints]="[0, 1]"
34254
+ [initialBreakpoint]="1"
34255
+ >
34256
+ <ng-template>
34257
+ @if (selectedFile()) {
34258
+ <val-image-crop
34259
+ [image]="selectedFile()!"
34260
+ [aspectRatio]="1"
34261
+ [roundCropper]="true"
34262
+ [i18nNamespace]="config().i18nNamespace"
34263
+ (cropComplete)="onCropComplete($event)"
34264
+ (cancel)="onCropCancel()"
34265
+ (loadFailed)="onCropLoadFailed()"
34266
+ />
34267
+ }
34268
+ </ng-template>
34269
+ </ion-modal>
34270
+ </div>
34271
+ `, styles: [".avatar-upload{--avatar-size: 100px;--edit-button-size: 32px;--edit-button-offset: 4px;display:inline-block}.avatar-container{position:relative;width:var(--avatar-size);height:var(--avatar-size);border-radius:50%;overflow:visible}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;background-color:var(--ion-color-light)}.avatar-initials{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:calc(var(--avatar-size) * .4);font-weight:600;color:#fff;text-transform:uppercase;-webkit-user-select:none;user-select:none}.edit-button{position:absolute;bottom:var(--edit-button-offset);right:var(--edit-button-offset);width:var(--edit-button-size);height:var(--edit-button-size);border-radius:50%;border:2px solid white;background:var(--ion-color-primary);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s ease,background-color .2s ease;box-shadow:0 2px 8px #00000026}.edit-button ion-icon{font-size:calc(var(--edit-button-size) * .5)}.edit-button:hover{transform:scale(1.1);background:var(--ion-color-primary-shade)}.edit-button:active{transform:scale(.95)}.edit-button:focus-visible{outline:2px solid var(--ion-color-primary);outline-offset:2px}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;background:#00000080;display:flex;align-items:center;justify-content:center}.loading-overlay ion-spinner{--color: white;width:calc(var(--avatar-size) * .4);height:calc(var(--avatar-size) * .4)}.avatar-upload--loading .edit-button{display:none}.avatar-upload--loading .avatar-image,.avatar-upload--loading .avatar-initials{filter:brightness(.7)}@container (max-width: 60px){.edit-button{--edit-button-size: 24px;--edit-button-offset: 0}}\n"] }]
34272
+ }], propDecorators: { fileInput: [{
34273
+ type: ViewChild,
34274
+ args: ['fileInput']
34275
+ }], uploaded: [{
34276
+ type: Output
34277
+ }], error: [{
34278
+ type: Output
34279
+ }], uploadStart: [{
34280
+ type: Output
34281
+ }] } });
34282
+
33504
34283
  class LayoutComponent {
33505
34284
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
33506
34285
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: LayoutComponent, isStandalone: true, selector: "val-layout", ngImport: i0, template: `
@@ -37624,35 +38403,38 @@ class FeedbackService {
37624
38403
  * ```
37625
38404
  */
37626
38405
  async checkFeedback(entityType, entityId) {
38406
+ // Si no hay usuario autenticado, no puede haber feedback previo
38407
+ // Retornar inmediatamente sin llamar al API (evita 401 y redirect a login)
38408
+ const userId = this.auth?.user()?.userId;
38409
+ if (!userId) {
38410
+ return { operationId: '', hasFeedback: false };
38411
+ }
37627
38412
  // 1. Intentar Firebase primero (si está disponible)
37628
- if (this.firestore && this.auth) {
38413
+ if (this.firestore) {
37629
38414
  try {
37630
- const userId = this.auth.user()?.userId;
37631
- if (userId) {
37632
- // Path: feedback/{entityType}/{entityId}/{userId}
37633
- // FirestoreService agrega automáticamente el prefijo apps/{appId}/
37634
- const collectionPath = `feedback/${entityType}/${entityId}`;
37635
- const doc = await this.firestore.getDoc(collectionPath, userId);
37636
- if (doc) {
37637
- return {
37638
- operationId: '',
37639
- hasFeedback: true,
37640
- feedbackId: doc.feedbackId,
37641
- type: doc.type,
37642
- reactionValue: doc.reactionValue,
37643
- createdAt: doc.createdAt?.toISOString(),
37644
- };
37645
- }
37646
- // Doc no existe = no hay feedback
37647
- return { operationId: '', hasFeedback: false };
38415
+ // Path: feedback/{entityType}/{entityId}/{userId}
38416
+ // FirestoreService agrega automáticamente el prefijo apps/{appId}/
38417
+ const collectionPath = `feedback/${entityType}/${entityId}`;
38418
+ const doc = await this.firestore.getDoc(collectionPath, userId);
38419
+ if (doc) {
38420
+ return {
38421
+ operationId: '',
38422
+ hasFeedback: true,
38423
+ feedbackId: doc.feedbackId,
38424
+ type: doc.type,
38425
+ reactionValue: doc.reactionValue,
38426
+ createdAt: doc.createdAt?.toISOString(),
38427
+ };
37648
38428
  }
38429
+ // Doc no existe = no hay feedback
38430
+ return { operationId: '', hasFeedback: false };
37649
38431
  }
37650
38432
  catch (error) {
37651
38433
  console.warn('[FeedbackService] Firebase check failed, falling back to API:', error);
37652
38434
  // Fallback a API
37653
38435
  }
37654
38436
  }
37655
- // 2. Fallback: llamar API
38437
+ // 2. Fallback: llamar API (solo si hay usuario autenticado)
37656
38438
  const params = new URLSearchParams({
37657
38439
  appId: this.config.appId,
37658
38440
  entityType,
@@ -40446,6 +41228,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40446
41228
  class DocsPageComponent {
40447
41229
  constructor() {
40448
41230
  this.elementRef = inject(ElementRef);
41231
+ this.router = inject(Router);
40449
41232
  // Use signal internally so computed properties react to changes
40450
41233
  this._props = signal({ title: '' });
40451
41234
  this.tocItems = signal([]);
@@ -40491,6 +41274,22 @@ class DocsPageComponent {
40491
41274
  homeRoute: props.breadcrumb.homeRoute ?? ['/'],
40492
41275
  };
40493
41276
  });
41277
+ this.feedbackProps = computed(() => {
41278
+ const props = this._props();
41279
+ const feedback = props.feedback;
41280
+ // Derive entityId from route if not provided
41281
+ const entityId = feedback?.entityId || this.router.url.replace(/^\//, '').replace(/\//g, '-') || 'unknown';
41282
+ return {
41283
+ entityRef: {
41284
+ entityType: feedback?.entityType || 'docs-page',
41285
+ entityId: entityId,
41286
+ },
41287
+ question: feedback?.question,
41288
+ showComment: feedback?.showComment ?? true,
41289
+ allowAnonymous: feedback?.allowAnonymous ?? true,
41290
+ showThankYou: true,
41291
+ };
41292
+ });
40494
41293
  }
40495
41294
  set props(value) {
40496
41295
  this._props.set(value);
@@ -40562,6 +41361,12 @@ class DocsPageComponent {
40562
41361
  <ng-content></ng-content>
40563
41362
  </div>
40564
41363
 
41364
+ @if (props.feedback?.enabled) {
41365
+ <div class="docs-page__feedback">
41366
+ <val-content-reaction [props]="feedbackProps()"></val-content-reaction>
41367
+ </div>
41368
+ }
41369
+
40565
41370
  @if (showNavLinks()) {
40566
41371
  <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
40567
41372
  }
@@ -40573,11 +41378,11 @@ class DocsPageComponent {
40573
41378
  </aside>
40574
41379
  }
40575
41380
  </div>
40576
- `, isInline: true, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem}@media (min-width: 1200px){.docs-page{grid-template-columns:1fr 220px;padding:2rem}}.docs-page__content{min-width:0;max-width:900px}val-docs-breadcrumb{display:block;margin-bottom:1rem}.docs-page__header{margin-bottom:2rem}.docs-page__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.docs-page__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color, #1a1a1a);line-height:1.2}@media (min-width: 768px){.docs-page__title{font-size:2.5rem}}.docs-page__badge{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.6875rem;font-weight:500;text-transform:uppercase;letter-spacing:.03em;border-radius:4px;background:#0000000a;color:#888}.docs-page__badge--success{background:#0000000a;color:#888}.docs-page__badge--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}.docs-page__badge--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}.docs-page__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium, #666)}.docs-page__sections>*:last-child{margin-bottom:0}.docs-page__toc{display:none}@media (min-width: 1200px){.docs-page__toc{display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}.docs-page__sections h2{font-size:1.5rem;font-weight:600;margin:0 0 1rem;color:var(--ion-text-color, #1a1a1a);scroll-margin-top:2rem}.docs-page__sections h3{font-size:1.125rem;font-weight:600;margin:1.5rem 0 1rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections h4{font-size:1rem;font-weight:600;margin:1.25rem 0 .75rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections p{line-height:1.7;color:var(--ion-text-color, #1a1a1a);margin:0 0 1rem}.docs-page__sections ul,.docs-page__sections ol{padding-left:1.5rem;margin:0 0 1rem}.docs-page__sections li{margin-bottom:.5rem;line-height:1.6;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections a{color:var(--ion-color-primary, #3880ff);text-decoration:none}.docs-page__sections a:hover{text-decoration:underline}.docs-page__sections code:not([class*=language-]){background:#0000000f;padding:.125rem .375rem;border-radius:4px;font-family:SF Mono,Fira Code,Consolas,monospace;font-size:.875em}.docs-page__sections pre:not([class*=language-]){background:#0000000a;padding:1rem;border-radius:8px;overflow-x:auto;margin:0 0 1rem}.docs-page__sections pre:not([class*=language-]) code{background:none;padding:0}.docs-page__sections table{width:100%;border-collapse:collapse;margin:0 0 1rem;font-size:.875rem}.docs-page__sections th,.docs-page__sections td{padding:.75rem;text-align:left;border-bottom:1px solid rgba(0,0,0,.1)}.docs-page__sections th{font-weight:600;background:#00000005}.docs-page__sections blockquote{margin:0 0 1rem;padding:1rem 1.5rem;border-left:4px solid var(--ion-color-primary, #3880ff);background:#00000005;border-radius:0 8px 8px 0}.docs-page__sections blockquote p:last-child{margin-bottom:0}.docs-page__sections img{max-width:100%;height:auto;border-radius:8px}.docs-page__sections hr{border:none;border-top:1px solid rgba(0,0,0,.1);margin:2rem 0}.docs-page__sections strong{font-weight:600}.docs-page__sections section{margin-bottom:2rem}:host-context(.dark) .docs-page__badge,:host-context([color-scheme=\"dark\"]) .docs-page__badge{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--success,:host-context([color-scheme=\"dark\"]) .docs-page__badge--success{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--warning,:host-context([color-scheme=\"dark\"]) .docs-page__badge--warning{background:#e6510033;color:#ffb74d}:host-context(.dark) .docs-page__badge--danger,:host-context([color-scheme=\"dark\"]) .docs-page__badge--danger{background:#c6282833;color:#e57373}:host-context(.dark) .docs-page__sections code:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections code:not([class*=language-]){background:#ffffff1a}:host-context(.dark) .docs-page__sections pre:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections pre:not([class*=language-]){background:#ffffff0f}:host-context(.dark) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections th{background:#ffffff0a}:host-context(.dark) .docs-page__sections th,:host-context(.dark) .docs-page__sections td,:host-context([color-scheme=\"dark\"]) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections td{border-color:#ffffff1a}:host-context(.dark) .docs-page__sections blockquote,:host-context([color-scheme=\"dark\"]) .docs-page__sections blockquote{background:#ffffff0a}:host-context(.dark) .docs-page__sections hr,:host-context([color-scheme=\"dark\"]) .docs-page__sections hr{border-color:#ffffff1a}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DocsBreadcrumbComponent, selector: "val-docs-breadcrumb", inputs: ["props"], outputs: ["navigate"] }, { kind: "component", type: DocsNavLinksComponent, selector: "val-docs-nav-links", inputs: ["props"], outputs: ["navigate"] }, { kind: "component", type: DocsTocComponent, selector: "val-docs-toc", inputs: ["props"], outputs: ["sectionChange"] }] }); }
41381
+ `, isInline: true, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem}@media (min-width: 1200px){.docs-page{grid-template-columns:1fr 220px;padding:2rem}}.docs-page__content{min-width:0;max-width:900px}val-docs-breadcrumb{display:block;margin-bottom:1rem}.docs-page__header{margin-bottom:2rem}.docs-page__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.docs-page__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color, #1a1a1a);line-height:1.2}@media (min-width: 768px){.docs-page__title{font-size:2.5rem}}.docs-page__badge{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.6875rem;font-weight:500;text-transform:uppercase;letter-spacing:.03em;border-radius:4px;background:#0000000a;color:#888}.docs-page__badge--success{background:#0000000a;color:#888}.docs-page__badge--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}.docs-page__badge--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}.docs-page__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium, #666)}.docs-page__sections>*:last-child{margin-bottom:0}.docs-page__feedback{margin-top:3rem;padding-top:2rem;border-top:1px solid rgba(0,0,0,.08);display:flex;justify-content:center}.docs-page__toc{display:none}@media (min-width: 1200px){.docs-page__toc{display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}.docs-page__sections h2{font-size:1.5rem;font-weight:600;margin:0 0 1rem;color:var(--ion-text-color, #1a1a1a);scroll-margin-top:2rem}.docs-page__sections h3{font-size:1.125rem;font-weight:600;margin:1.5rem 0 1rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections h4{font-size:1rem;font-weight:600;margin:1.25rem 0 .75rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections p{line-height:1.7;color:var(--ion-text-color, #1a1a1a);margin:0 0 1rem}.docs-page__sections ul,.docs-page__sections ol{padding-left:1.5rem;margin:0 0 1rem}.docs-page__sections li{margin-bottom:.5rem;line-height:1.6;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections a{color:var(--ion-color-primary, #3880ff);text-decoration:none}.docs-page__sections a:hover{text-decoration:underline}.docs-page__sections code:not([class*=language-]){background:#0000000f;padding:.125rem .375rem;border-radius:4px;font-family:SF Mono,Fira Code,Consolas,monospace;font-size:.875em}.docs-page__sections pre:not([class*=language-]){background:#0000000a;padding:1rem;border-radius:8px;overflow-x:auto;margin:0 0 1rem}.docs-page__sections pre:not([class*=language-]) code{background:none;padding:0}.docs-page__sections table{width:100%;border-collapse:collapse;margin:0 0 1rem;font-size:.875rem}.docs-page__sections th,.docs-page__sections td{padding:.75rem;text-align:left;border-bottom:1px solid rgba(0,0,0,.1)}.docs-page__sections th{font-weight:600;background:#00000005}.docs-page__sections blockquote{margin:0 0 1rem;padding:1rem 1.5rem;border-left:4px solid var(--ion-color-primary, #3880ff);background:#00000005;border-radius:0 8px 8px 0}.docs-page__sections blockquote p:last-child{margin-bottom:0}.docs-page__sections img{max-width:100%;height:auto;border-radius:8px}.docs-page__sections hr{border:none;border-top:1px solid rgba(0,0,0,.1);margin:2rem 0}.docs-page__sections strong{font-weight:600}.docs-page__sections section{margin-bottom:2rem}:host-context(.dark) .docs-page__badge,:host-context([color-scheme=\"dark\"]) .docs-page__badge{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--success,:host-context([color-scheme=\"dark\"]) .docs-page__badge--success{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--warning,:host-context([color-scheme=\"dark\"]) .docs-page__badge--warning{background:#e6510033;color:#ffb74d}:host-context(.dark) .docs-page__badge--danger,:host-context([color-scheme=\"dark\"]) .docs-page__badge--danger{background:#c6282833;color:#e57373}:host-context(.dark) .docs-page__sections code:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections code:not([class*=language-]){background:#ffffff1a}:host-context(.dark) .docs-page__sections pre:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections pre:not([class*=language-]){background:#ffffff0f}:host-context(.dark) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections th{background:#ffffff0a}:host-context(.dark) .docs-page__sections th,:host-context(.dark) .docs-page__sections td,:host-context([color-scheme=\"dark\"]) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections td{border-color:#ffffff1a}:host-context(.dark) .docs-page__sections blockquote,:host-context([color-scheme=\"dark\"]) .docs-page__sections blockquote{background:#ffffff0a}:host-context(.dark) .docs-page__sections hr,:host-context([color-scheme=\"dark\"]) .docs-page__sections hr{border-color:#ffffff1a}:host-context(.dark) .docs-page__feedback,:host-context([color-scheme=\"dark\"]) .docs-page__feedback{border-color:#ffffff1a}@media (max-width: 768px){.docs-page__feedback{margin-top:2rem;padding-top:1.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DocsBreadcrumbComponent, selector: "val-docs-breadcrumb", inputs: ["props"], outputs: ["navigate"] }, { kind: "component", type: DocsNavLinksComponent, selector: "val-docs-nav-links", inputs: ["props"], outputs: ["navigate"] }, { kind: "component", type: DocsTocComponent, selector: "val-docs-toc", inputs: ["props"], outputs: ["sectionChange"] }, { kind: "component", type: ContentReactionComponent, selector: "val-content-reaction", inputs: ["props"], outputs: ["reactionSubmit", "reactionChange"] }] }); }
40577
41382
  }
40578
41383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsPageComponent, decorators: [{
40579
41384
  type: Component,
40580
- args: [{ selector: 'val-docs-page', standalone: true, imports: [CommonModule, DocsBreadcrumbComponent, DocsNavLinksComponent, DocsTocComponent], template: `
41385
+ args: [{ selector: 'val-docs-page', standalone: true, imports: [CommonModule, DocsBreadcrumbComponent, DocsNavLinksComponent, DocsTocComponent, ContentReactionComponent], template: `
40581
41386
  <div class="docs-page" [class]="props.cssClass">
40582
41387
  <div class="docs-page__content" #content>
40583
41388
  @if (breadcrumbProps()) {
@@ -40606,6 +41411,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40606
41411
  <ng-content></ng-content>
40607
41412
  </div>
40608
41413
 
41414
+ @if (props.feedback?.enabled) {
41415
+ <div class="docs-page__feedback">
41416
+ <val-content-reaction [props]="feedbackProps()"></val-content-reaction>
41417
+ </div>
41418
+ }
41419
+
40609
41420
  @if (showNavLinks()) {
40610
41421
  <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
40611
41422
  }
@@ -40617,7 +41428,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40617
41428
  </aside>
40618
41429
  }
40619
41430
  </div>
40620
- `, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem}@media (min-width: 1200px){.docs-page{grid-template-columns:1fr 220px;padding:2rem}}.docs-page__content{min-width:0;max-width:900px}val-docs-breadcrumb{display:block;margin-bottom:1rem}.docs-page__header{margin-bottom:2rem}.docs-page__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.docs-page__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color, #1a1a1a);line-height:1.2}@media (min-width: 768px){.docs-page__title{font-size:2.5rem}}.docs-page__badge{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.6875rem;font-weight:500;text-transform:uppercase;letter-spacing:.03em;border-radius:4px;background:#0000000a;color:#888}.docs-page__badge--success{background:#0000000a;color:#888}.docs-page__badge--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}.docs-page__badge--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}.docs-page__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium, #666)}.docs-page__sections>*:last-child{margin-bottom:0}.docs-page__toc{display:none}@media (min-width: 1200px){.docs-page__toc{display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}.docs-page__sections h2{font-size:1.5rem;font-weight:600;margin:0 0 1rem;color:var(--ion-text-color, #1a1a1a);scroll-margin-top:2rem}.docs-page__sections h3{font-size:1.125rem;font-weight:600;margin:1.5rem 0 1rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections h4{font-size:1rem;font-weight:600;margin:1.25rem 0 .75rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections p{line-height:1.7;color:var(--ion-text-color, #1a1a1a);margin:0 0 1rem}.docs-page__sections ul,.docs-page__sections ol{padding-left:1.5rem;margin:0 0 1rem}.docs-page__sections li{margin-bottom:.5rem;line-height:1.6;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections a{color:var(--ion-color-primary, #3880ff);text-decoration:none}.docs-page__sections a:hover{text-decoration:underline}.docs-page__sections code:not([class*=language-]){background:#0000000f;padding:.125rem .375rem;border-radius:4px;font-family:SF Mono,Fira Code,Consolas,monospace;font-size:.875em}.docs-page__sections pre:not([class*=language-]){background:#0000000a;padding:1rem;border-radius:8px;overflow-x:auto;margin:0 0 1rem}.docs-page__sections pre:not([class*=language-]) code{background:none;padding:0}.docs-page__sections table{width:100%;border-collapse:collapse;margin:0 0 1rem;font-size:.875rem}.docs-page__sections th,.docs-page__sections td{padding:.75rem;text-align:left;border-bottom:1px solid rgba(0,0,0,.1)}.docs-page__sections th{font-weight:600;background:#00000005}.docs-page__sections blockquote{margin:0 0 1rem;padding:1rem 1.5rem;border-left:4px solid var(--ion-color-primary, #3880ff);background:#00000005;border-radius:0 8px 8px 0}.docs-page__sections blockquote p:last-child{margin-bottom:0}.docs-page__sections img{max-width:100%;height:auto;border-radius:8px}.docs-page__sections hr{border:none;border-top:1px solid rgba(0,0,0,.1);margin:2rem 0}.docs-page__sections strong{font-weight:600}.docs-page__sections section{margin-bottom:2rem}:host-context(.dark) .docs-page__badge,:host-context([color-scheme=\"dark\"]) .docs-page__badge{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--success,:host-context([color-scheme=\"dark\"]) .docs-page__badge--success{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--warning,:host-context([color-scheme=\"dark\"]) .docs-page__badge--warning{background:#e6510033;color:#ffb74d}:host-context(.dark) .docs-page__badge--danger,:host-context([color-scheme=\"dark\"]) .docs-page__badge--danger{background:#c6282833;color:#e57373}:host-context(.dark) .docs-page__sections code:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections code:not([class*=language-]){background:#ffffff1a}:host-context(.dark) .docs-page__sections pre:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections pre:not([class*=language-]){background:#ffffff0f}:host-context(.dark) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections th{background:#ffffff0a}:host-context(.dark) .docs-page__sections th,:host-context(.dark) .docs-page__sections td,:host-context([color-scheme=\"dark\"]) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections td{border-color:#ffffff1a}:host-context(.dark) .docs-page__sections blockquote,:host-context([color-scheme=\"dark\"]) .docs-page__sections blockquote{background:#ffffff0a}:host-context(.dark) .docs-page__sections hr,:host-context([color-scheme=\"dark\"]) .docs-page__sections hr{border-color:#ffffff1a}\n"] }]
41431
+ `, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem}@media (min-width: 1200px){.docs-page{grid-template-columns:1fr 220px;padding:2rem}}.docs-page__content{min-width:0;max-width:900px}val-docs-breadcrumb{display:block;margin-bottom:1rem}.docs-page__header{margin-bottom:2rem}.docs-page__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}.docs-page__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color, #1a1a1a);line-height:1.2}@media (min-width: 768px){.docs-page__title{font-size:2.5rem}}.docs-page__badge{display:inline-flex;align-items:center;padding:.25rem .5rem;font-size:.6875rem;font-weight:500;text-transform:uppercase;letter-spacing:.03em;border-radius:4px;background:#0000000a;color:#888}.docs-page__badge--success{background:#0000000a;color:#888}.docs-page__badge--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}.docs-page__badge--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}.docs-page__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium, #666)}.docs-page__sections>*:last-child{margin-bottom:0}.docs-page__feedback{margin-top:3rem;padding-top:2rem;border-top:1px solid rgba(0,0,0,.08);display:flex;justify-content:center}.docs-page__toc{display:none}@media (min-width: 1200px){.docs-page__toc{display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}.docs-page__sections h2{font-size:1.5rem;font-weight:600;margin:0 0 1rem;color:var(--ion-text-color, #1a1a1a);scroll-margin-top:2rem}.docs-page__sections h3{font-size:1.125rem;font-weight:600;margin:1.5rem 0 1rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections h4{font-size:1rem;font-weight:600;margin:1.25rem 0 .75rem;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections p{line-height:1.7;color:var(--ion-text-color, #1a1a1a);margin:0 0 1rem}.docs-page__sections ul,.docs-page__sections ol{padding-left:1.5rem;margin:0 0 1rem}.docs-page__sections li{margin-bottom:.5rem;line-height:1.6;color:var(--ion-text-color, #1a1a1a)}.docs-page__sections a{color:var(--ion-color-primary, #3880ff);text-decoration:none}.docs-page__sections a:hover{text-decoration:underline}.docs-page__sections code:not([class*=language-]){background:#0000000f;padding:.125rem .375rem;border-radius:4px;font-family:SF Mono,Fira Code,Consolas,monospace;font-size:.875em}.docs-page__sections pre:not([class*=language-]){background:#0000000a;padding:1rem;border-radius:8px;overflow-x:auto;margin:0 0 1rem}.docs-page__sections pre:not([class*=language-]) code{background:none;padding:0}.docs-page__sections table{width:100%;border-collapse:collapse;margin:0 0 1rem;font-size:.875rem}.docs-page__sections th,.docs-page__sections td{padding:.75rem;text-align:left;border-bottom:1px solid rgba(0,0,0,.1)}.docs-page__sections th{font-weight:600;background:#00000005}.docs-page__sections blockquote{margin:0 0 1rem;padding:1rem 1.5rem;border-left:4px solid var(--ion-color-primary, #3880ff);background:#00000005;border-radius:0 8px 8px 0}.docs-page__sections blockquote p:last-child{margin-bottom:0}.docs-page__sections img{max-width:100%;height:auto;border-radius:8px}.docs-page__sections hr{border:none;border-top:1px solid rgba(0,0,0,.1);margin:2rem 0}.docs-page__sections strong{font-weight:600}.docs-page__sections section{margin-bottom:2rem}:host-context(.dark) .docs-page__badge,:host-context([color-scheme=\"dark\"]) .docs-page__badge{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--success,:host-context([color-scheme=\"dark\"]) .docs-page__badge--success{background:#ffffff0f;color:#999}:host-context(.dark) .docs-page__badge--warning,:host-context([color-scheme=\"dark\"]) .docs-page__badge--warning{background:#e6510033;color:#ffb74d}:host-context(.dark) .docs-page__badge--danger,:host-context([color-scheme=\"dark\"]) .docs-page__badge--danger{background:#c6282833;color:#e57373}:host-context(.dark) .docs-page__sections code:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections code:not([class*=language-]){background:#ffffff1a}:host-context(.dark) .docs-page__sections pre:not([class*=language-]),:host-context([color-scheme=\"dark\"]) .docs-page__sections pre:not([class*=language-]){background:#ffffff0f}:host-context(.dark) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections th{background:#ffffff0a}:host-context(.dark) .docs-page__sections th,:host-context(.dark) .docs-page__sections td,:host-context([color-scheme=\"dark\"]) .docs-page__sections th,:host-context([color-scheme=\"dark\"]) .docs-page__sections td{border-color:#ffffff1a}:host-context(.dark) .docs-page__sections blockquote,:host-context([color-scheme=\"dark\"]) .docs-page__sections blockquote{background:#ffffff0a}:host-context(.dark) .docs-page__sections hr,:host-context([color-scheme=\"dark\"]) .docs-page__sections hr{border-color:#ffffff1a}:host-context(.dark) .docs-page__feedback,:host-context([color-scheme=\"dark\"]) .docs-page__feedback{border-color:#ffffff1a}@media (max-width: 768px){.docs-page__feedback{margin-top:2rem;padding-top:1.5rem}}\n"] }]
40621
41432
  }], propDecorators: { props: [{
40622
41433
  type: Input
40623
41434
  }] } });
@@ -41109,5 +41920,5 @@ function buildFooterLinks(links, t) {
41109
41920
  * Generated bundle index. Do not edit.
41110
41921
  */
41111
41922
 
41112
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, ContentReactionComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTION, MaintenancePageComponent, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UpdateBannerComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_SOCIAL_LINKS, VERSION, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildFooterLinks, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAppConfig, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
41923
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, ContentReactionComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTION, MaintenancePageComponent, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UpdateBannerComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_SOCIAL_LINKS, VERSION, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildFooterLinks, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAppConfig, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
41113
41924
  //# sourceMappingURL=valtech-components.mjs.map