valtech-components 2.0.681 → 2.0.682

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 (30) 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/image/image.service.mjs +244 -0
  10. package/esm2022/lib/services/image/index.mjs +3 -0
  11. package/esm2022/lib/services/image/types.mjs +13 -0
  12. package/esm2022/lib/version.mjs +2 -2
  13. package/esm2022/public-api.mjs +7 -1
  14. package/fesm2022/valtech-components.mjs +815 -7
  15. package/fesm2022/valtech-components.mjs.map +1 -1
  16. package/lib/components/molecules/image-crop/image-crop.component.d.ts +59 -0
  17. package/lib/components/molecules/image-crop/index.d.ts +1 -0
  18. package/lib/components/organisms/avatar-upload/avatar-upload.component.d.ts +82 -0
  19. package/lib/components/organisms/avatar-upload/types.d.ts +62 -0
  20. package/lib/components/templates/docs-page/docs-page.component.d.ts +3 -0
  21. package/lib/components/templates/docs-page/types.d.ts +39 -0
  22. package/lib/services/auth/auth.service.d.ts +6 -1
  23. package/lib/services/auth/types.d.ts +18 -0
  24. package/lib/services/image/image.service.d.ts +76 -0
  25. package/lib/services/image/index.d.ts +2 -0
  26. package/lib/services/image/types.d.ts +74 -0
  27. package/lib/version.d.ts +1 -1
  28. package/package.json +2 -1
  29. package/public-api.d.ts +4 -0
  30. 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.682';
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: `
@@ -40446,6 +41225,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40446
41225
  class DocsPageComponent {
40447
41226
  constructor() {
40448
41227
  this.elementRef = inject(ElementRef);
41228
+ this.router = inject(Router);
40449
41229
  // Use signal internally so computed properties react to changes
40450
41230
  this._props = signal({ title: '' });
40451
41231
  this.tocItems = signal([]);
@@ -40491,6 +41271,22 @@ class DocsPageComponent {
40491
41271
  homeRoute: props.breadcrumb.homeRoute ?? ['/'],
40492
41272
  };
40493
41273
  });
41274
+ this.feedbackProps = computed(() => {
41275
+ const props = this._props();
41276
+ const feedback = props.feedback;
41277
+ // Derive entityId from route if not provided
41278
+ const entityId = feedback?.entityId || this.router.url.replace(/^\//, '').replace(/\//g, '-') || 'unknown';
41279
+ return {
41280
+ entityRef: {
41281
+ entityType: feedback?.entityType || 'docs-page',
41282
+ entityId: entityId,
41283
+ },
41284
+ question: feedback?.question,
41285
+ showComment: feedback?.showComment ?? true,
41286
+ allowAnonymous: feedback?.allowAnonymous ?? true,
41287
+ showThankYou: true,
41288
+ };
41289
+ });
40494
41290
  }
40495
41291
  set props(value) {
40496
41292
  this._props.set(value);
@@ -40562,6 +41358,12 @@ class DocsPageComponent {
40562
41358
  <ng-content></ng-content>
40563
41359
  </div>
40564
41360
 
41361
+ @if (props.feedback?.enabled) {
41362
+ <div class="docs-page__feedback">
41363
+ <val-content-reaction [props]="feedbackProps()"></val-content-reaction>
41364
+ </div>
41365
+ }
41366
+
40565
41367
  @if (showNavLinks()) {
40566
41368
  <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
40567
41369
  }
@@ -40573,11 +41375,11 @@ class DocsPageComponent {
40573
41375
  </aside>
40574
41376
  }
40575
41377
  </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"] }] }); }
41378
+ `, 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
41379
  }
40578
41380
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsPageComponent, decorators: [{
40579
41381
  type: Component,
40580
- args: [{ selector: 'val-docs-page', standalone: true, imports: [CommonModule, DocsBreadcrumbComponent, DocsNavLinksComponent, DocsTocComponent], template: `
41382
+ args: [{ selector: 'val-docs-page', standalone: true, imports: [CommonModule, DocsBreadcrumbComponent, DocsNavLinksComponent, DocsTocComponent, ContentReactionComponent], template: `
40581
41383
  <div class="docs-page" [class]="props.cssClass">
40582
41384
  <div class="docs-page__content" #content>
40583
41385
  @if (breadcrumbProps()) {
@@ -40606,6 +41408,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40606
41408
  <ng-content></ng-content>
40607
41409
  </div>
40608
41410
 
41411
+ @if (props.feedback?.enabled) {
41412
+ <div class="docs-page__feedback">
41413
+ <val-content-reaction [props]="feedbackProps()"></val-content-reaction>
41414
+ </div>
41415
+ }
41416
+
40609
41417
  @if (showNavLinks()) {
40610
41418
  <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
40611
41419
  }
@@ -40617,7 +41425,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40617
41425
  </aside>
40618
41426
  }
40619
41427
  </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"] }]
41428
+ `, 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
41429
  }], propDecorators: { props: [{
40622
41430
  type: Input
40623
41431
  }] } });
@@ -41109,5 +41917,5 @@ function buildFooterLinks(links, t) {
41109
41917
  * Generated bundle index. Do not edit.
41110
41918
  */
41111
41919
 
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 };
41920
+ 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
41921
  //# sourceMappingURL=valtech-components.mjs.map