valtech-components 2.0.969 → 2.0.971

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, Injector, isSignal, ViewEncapsulation } from '@angular/core';
2
+ import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, viewChild, Injector, isSignal, ViewEncapsulation } from '@angular/core';
3
3
  import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, shareReplay, race, timer, forkJoin } from 'rxjs';
4
4
  import { catchError, switchMap, finalize, filter, take, map as map$1, tap, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
5
5
  import * as i1$3 from '@angular/common/http';
@@ -17,7 +17,7 @@ import * as i1$2 from '@angular/fire/storage';
17
17
  import { provideStorage, getStorage, connectStorageEmulator, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
18
18
  import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
19
19
  import * as i1$4 from '@angular/router';
20
- import { Router, NavigationEnd, RouterLink, RouterOutlet, RouterModule } from '@angular/router';
20
+ import { Router, NavigationEnd, RouterLink, ActivatedRoute, RouterOutlet, RouterModule } from '@angular/router';
21
21
  import * as i2 from '@ionic/angular';
22
22
  import { IonicModule, ToastController, NavController } from '@ionic/angular';
23
23
  import { isSupported, getMessaging as getMessaging$1 } from 'firebase/messaging';
@@ -54,7 +54,7 @@ import 'prismjs/components/prism-json';
54
54
  * Current version of valtech-components.
55
55
  * This is automatically updated during the publish process.
56
56
  */
57
- const VERSION = '2.0.969';
57
+ const VERSION = '2.0.971';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -41489,6 +41489,786 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
41489
41489
  type: Input
41490
41490
  }] } });
41491
41491
 
41492
+ /**
41493
+ * PageRefreshService — bus del pull-to-refresh estándar del factory.
41494
+ *
41495
+ * **Por qué existe:** `val-page-wrapper` posee un único `<ion-content>` y todas
41496
+ * las páginas se renderizan dentro vía `<router-outlet>`. Un `<ion-refresher>`
41497
+ * tiene que vivir dentro de ese `ion-content`, así que el refresher NO puede
41498
+ * declararse en el template de cada página — vive una sola vez en
41499
+ * `val-page-wrapper`. Este servicio es el puente: la página activa registra
41500
+ * *qué hacer* al refrescar; el page-wrapper dispara el gesto y cierra el spinner.
41501
+ *
41502
+ * **Uso en una página** (ver `frontend/CLAUDE.md` — es el patrón estándar):
41503
+ *
41504
+ * ```ts
41505
+ * export class MiPage implements ViewWillEnter, ViewWillLeave {
41506
+ * private pageRefresh = inject(PageRefreshService);
41507
+ *
41508
+ * ionViewWillEnter(): void {
41509
+ * this.pageRefresh.register(() => this.reload());
41510
+ * }
41511
+ * ionViewWillLeave(): void {
41512
+ * this.pageRefresh.unregister();
41513
+ * }
41514
+ *
41515
+ * private async reload(): Promise<void> {
41516
+ * // re-fetch / re-suscribir streams / etc.
41517
+ * }
41518
+ * }
41519
+ * ```
41520
+ *
41521
+ * Páginas que no llaman `register()` simplemente no muestran refresher —
41522
+ * opt-in, sin impacto.
41523
+ */
41524
+ class PageRefreshService {
41525
+ constructor() {
41526
+ this.handler = null;
41527
+ /**
41528
+ * `true` cuando hay una página con handler registrado. `val-page-wrapper`
41529
+ * renderiza el `<val-refresher>` sólo cuando esto es `true`.
41530
+ */
41531
+ this.hasHandler = signal(false);
41532
+ }
41533
+ /**
41534
+ * Registra el handler de refresh de la página activa. Llamar en
41535
+ * `ionViewWillEnter`. Si ya había uno, lo reemplaza (sólo hay una página
41536
+ * activa a la vez en el router-outlet de Ionic).
41537
+ */
41538
+ register(handler) {
41539
+ this.handler = handler;
41540
+ this.hasHandler.set(true);
41541
+ }
41542
+ /**
41543
+ * Quita el handler. Llamar en `ionViewWillLeave` para que el refresher
41544
+ * desaparezca al salir de la vista.
41545
+ */
41546
+ unregister() {
41547
+ this.handler = null;
41548
+ this.hasHandler.set(false);
41549
+ }
41550
+ /**
41551
+ * Ejecuta el handler registrado y espera a que termine. Lo invoca
41552
+ * `val-page-wrapper` al detectar el gesto de pull. No-op si no hay handler.
41553
+ */
41554
+ async run() {
41555
+ if (!this.handler)
41556
+ return;
41557
+ await this.handler();
41558
+ }
41559
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
41560
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, providedIn: 'root' }); }
41561
+ }
41562
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, decorators: [{
41563
+ type: Injectable,
41564
+ args: [{ providedIn: 'root' }]
41565
+ }] });
41566
+ /**
41567
+ * Conecta una página al pull-to-refresh estándar en UNA línea, sin lifecycle
41568
+ * hooks manuales: registra el handler y lo desregistra al destruir la página
41569
+ * (vía `DestroyRef`).
41570
+ *
41571
+ * DEBE llamarse en un injection context (constructor o field initializer).
41572
+ *
41573
+ * @example
41574
+ * ```ts
41575
+ * export class MiPage {
41576
+ * private data = createRefreshableStream(() => this.svc.getX());
41577
+ * readonly items = this.data.data;
41578
+ *
41579
+ * constructor() {
41580
+ * connectPageRefresh(() => this.data.reload());
41581
+ * }
41582
+ * }
41583
+ * ```
41584
+ *
41585
+ * Reemplaza el patrón `ngOnInit`→`register` / `ngOnDestroy`→`unregister`.
41586
+ */
41587
+ function connectPageRefresh(handler) {
41588
+ const svc = inject(PageRefreshService);
41589
+ const destroyRef = inject(DestroyRef);
41590
+ svc.register(handler);
41591
+ destroyRef.onDestroy(() => svc.unregister());
41592
+ }
41593
+
41594
+ /**
41595
+ * Defaults i18n (es/en) embebidos en `val-profile-view`. Auto-registrados en el
41596
+ * constructor del componente si el consumer no proveyó el namespace
41597
+ * (`Settings.Profile` por default). Garantiza que nunca haya una key faltante
41598
+ * evaluada por cada change-detection (ver nota en valtech-components/CLAUDE.md
41599
+ * sobre el storm de CD con `val-debug-console`). El consumer puede override
41600
+ * registrando el mismo namespace antes de que el componente monte.
41601
+ *
41602
+ * SOLO incluye las keys que la vista realmente usa.
41603
+ */
41604
+ const PROFILE_VIEW_I18N = {
41605
+ es: {
41606
+ pageTitle: 'Perfil',
41607
+ pageDescription: 'Administra tu informacion personal',
41608
+ name: 'Nombre completo',
41609
+ namePlaceholder: 'Tu nombre completo',
41610
+ nameHint: 'Tu nombre real que se mostrara en tu perfil.',
41611
+ email: 'Correo electronico',
41612
+ handle: 'Nombre de usuario',
41613
+ handlePlaceholder: 'usuario',
41614
+ handleHint: 'Tu identificador unico (@usuario). Otros pueden buscarte con este nombre.',
41615
+ handleInUse: 'Ese nombre de usuario ya está tomado',
41616
+ handleError: 'Error al actualizar el nombre de usuario',
41617
+ phoneInUse: 'Ese teléfono ya está en uso por otra cuenta',
41618
+ phoneInvalid: 'Formato de teléfono inválido (usa E.164: +56...)',
41619
+ phone: 'Telefono',
41620
+ phonePlaceholder: '+1 234 567 8900',
41621
+ phoneHint: 'Opcional. Para recuperacion de cuenta y notificaciones.',
41622
+ save: 'Guardar cambios',
41623
+ saving: 'Guardando...',
41624
+ saveSuccess: 'Perfil actualizado',
41625
+ saveError: 'Error al actualizar el perfil',
41626
+ usernameInvalid: 'Solo letras, numeros y guion bajo',
41627
+ avatarSuccess: 'Foto actualizada',
41628
+ avatarError: 'Error al actualizar la foto',
41629
+ changePhoto: 'Cambiar foto',
41630
+ viewPhoto: 'Ver foto',
41631
+ errorTitle: 'No pudimos cargar tu perfil',
41632
+ offlineTitle: 'Sin conexión',
41633
+ offlineHint: 'Revisa tu conexión a internet e intenta nuevamente.',
41634
+ retry: 'Reintentar',
41635
+ },
41636
+ en: {
41637
+ pageTitle: 'Profile',
41638
+ pageDescription: 'Manage your personal information',
41639
+ name: 'Full name',
41640
+ namePlaceholder: 'Your full name',
41641
+ nameHint: 'Your real name that will be displayed on your profile.',
41642
+ email: 'Email',
41643
+ handle: 'Username',
41644
+ handlePlaceholder: 'username',
41645
+ handleHint: 'Your unique identifier (@username). Others can search for you with this name.',
41646
+ handleInUse: 'That username is already taken',
41647
+ handleError: 'Failed to update username',
41648
+ phoneInUse: 'That phone is already in use by another account',
41649
+ phoneInvalid: 'Invalid phone format (use E.164: +1...)',
41650
+ phone: 'Phone',
41651
+ phonePlaceholder: '+1 234 567 8900',
41652
+ phoneHint: 'Optional. For account recovery and notifications.',
41653
+ save: 'Save changes',
41654
+ saving: 'Saving...',
41655
+ saveSuccess: 'Profile updated',
41656
+ saveError: 'Failed to update profile',
41657
+ usernameInvalid: 'Only letters, numbers and underscore',
41658
+ avatarSuccess: 'Photo updated',
41659
+ avatarError: 'Failed to update photo',
41660
+ changePhoto: 'Change photo',
41661
+ viewPhoto: 'View photo',
41662
+ errorTitle: "We couldn't load your profile",
41663
+ offlineTitle: 'Offline',
41664
+ offlineHint: 'Check your internet connection and try again.',
41665
+ retry: 'Retry',
41666
+ },
41667
+ };
41668
+
41669
+ /** Patrón sincronizado con backend handleRegex: lowercase alfanum + underscore. */
41670
+ const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41671
+ const HANDLE_MIN = 3;
41672
+ const HANDLE_MAX = 20;
41673
+ const DEFAULT_NAMESPACE = 'Settings.Profile';
41674
+ /**
41675
+ * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41676
+ *
41677
+ * Edita los campos públicos del usuario en UN solo formulario con UN solo botón
41678
+ * "Guardar". Wireup:
41679
+ * - Avatar — val-avatar-upload (Firebase Storage + backend sync). Separado del
41680
+ * form porque es upload sync independiente.
41681
+ * - Handle (@usuario) — InputType.HANDLE (val-username-input con check de
41682
+ * disponibilidad async resuelto vía AuthService inyectado).
41683
+ * - Nombre — InputType.TEXT.
41684
+ * - Teléfono — InputType.PHONE.
41685
+ *
41686
+ * Submit handler dispatcha:
41687
+ * 1. Si handle cambió → updateHandle PRIMERO (uniqueness).
41688
+ * 2. Si name o phone cambió → updateProfile con ambos.
41689
+ *
41690
+ * Lectura del perfil: suscribe al doc Firestore /users/{uid} (mirror que mantiene
41691
+ * el backend). Reactivo cross-tab + auto-refresca tras save. Fallback REST
41692
+ * (auth.getProfile) si el doc no existe o el listener falla.
41693
+ *
41694
+ * NO renderiza ion-content — vive dentro de val-page-wrapper.
41695
+ *
41696
+ * Auto-registra sus defaults i18n (es/en) en el constructor si el consumer no
41697
+ * proveyó el namespace configurado (default `Settings.Profile`).
41698
+ */
41699
+ class ProfileViewComponent {
41700
+ /** Namespace i18n resuelto (capturado una vez para llamadas no-reactivas). */
41701
+ get ns() {
41702
+ return this.resolvedConfig().i18nNamespace;
41703
+ }
41704
+ constructor() {
41705
+ this.nav = inject(NavigationService);
41706
+ this.auth = inject(AuthService);
41707
+ this.i18n = inject(I18nService);
41708
+ this.toast = inject(ToastService);
41709
+ this.errors = inject(ValtechErrorService);
41710
+ this.firebase = inject(FirebaseService);
41711
+ this.collections = inject(FirestoreCollectionFactory);
41712
+ this.route = inject(ActivatedRoute, { optional: true });
41713
+ /** Config mergeada con defaults. @Input gana sobre route data. */
41714
+ this.resolvedConfig = computed(() => {
41715
+ const fromRoute = (this.route?.snapshot.data['profileConfig'] ?? {});
41716
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
41717
+ return {
41718
+ showAvatar: merged.showAvatar ?? true,
41719
+ showHandle: merged.showHandle ?? true,
41720
+ showPhone: merged.showPhone ?? true,
41721
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
41722
+ onSaved: merged.onSaved,
41723
+ onAvatarUploaded: merged.onAvatarUploaded,
41724
+ };
41725
+ });
41726
+ /**
41727
+ * Colección global /users/{uid} (skipAppPrefix: true — el doc vive fuera de
41728
+ * apps/{appId}/... porque es cross-app, ver firestore.rules).
41729
+ */
41730
+ this.usersCol = this.collections.create('users', {
41731
+ skipAppPrefix: true,
41732
+ });
41733
+ this.formCmp = viewChild('form');
41734
+ this.profile = signal(null);
41735
+ this.loading = signal(true);
41736
+ this.saving = signal(false);
41737
+ this.loadError = signal(null);
41738
+ this._userId = computed(() => this.auth.user()?.userId ?? null);
41739
+ this.errorState = computed(() => {
41740
+ this.i18n.lang();
41741
+ const err = this.loadError();
41742
+ if (!err)
41743
+ return null;
41744
+ return createErrorStateProps(err, {
41745
+ title: {
41746
+ offline: this.t('offlineTitle'),
41747
+ error: this.t('errorTitle'),
41748
+ },
41749
+ description: {
41750
+ offline: this.t('offlineHint'),
41751
+ error: interpretError(err).message,
41752
+ },
41753
+ retryLabel: this.t('retry'),
41754
+ onRetry: () => this.loadFromApi(),
41755
+ retrying: this.loading(),
41756
+ });
41757
+ });
41758
+ this.avatarProps = computed(() => {
41759
+ this.i18n.lang();
41760
+ const p = this.profile();
41761
+ return {
41762
+ currentUrl: p?.avatarUrl,
41763
+ initials: this.initialsFromName(p?.name),
41764
+ size: 96,
41765
+ editable: true,
41766
+ showViewButton: true,
41767
+ i18nNamespace: this.ns,
41768
+ };
41769
+ });
41770
+ this.formMeta = this.buildFormMeta();
41771
+ // Auto-registro i18n — respeta override del consumer. El namespace puede
41772
+ // venir de @Input.config o del route data; lo resolvemos acá.
41773
+ const ns = this.ns;
41774
+ if (!this.i18n.hasNamespace(ns)) {
41775
+ this.i18n.registerContent(ns, PROFILE_VIEW_I18N);
41776
+ }
41777
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
41778
+ connectPageRefresh(() => this.loadFromApi());
41779
+ effect(onCleanup => {
41780
+ const uid = this._userId();
41781
+ if (!uid) {
41782
+ this.profile.set(null);
41783
+ this.loading.set(false);
41784
+ return;
41785
+ }
41786
+ this.loading.set(true);
41787
+ let sub = null;
41788
+ let cancelled = false;
41789
+ let apiFallbackUsed = false;
41790
+ void this.firebase.whenFirebaseAuthReady().then(() => {
41791
+ if (cancelled)
41792
+ return;
41793
+ sub = this.usersCol.watch(uid).subscribe({
41794
+ next: doc => {
41795
+ if (doc) {
41796
+ this.profile.set(this.mapMirrorToProfile(doc));
41797
+ this.loadError.set(null);
41798
+ this.loading.set(false);
41799
+ }
41800
+ else if (!apiFallbackUsed) {
41801
+ apiFallbackUsed = true;
41802
+ void this.loadFromApi();
41803
+ }
41804
+ },
41805
+ error: () => {
41806
+ if (!apiFallbackUsed) {
41807
+ apiFallbackUsed = true;
41808
+ void this.loadFromApi();
41809
+ }
41810
+ },
41811
+ });
41812
+ });
41813
+ onCleanup(() => {
41814
+ cancelled = true;
41815
+ sub?.unsubscribe();
41816
+ });
41817
+ }, { allowSignalWrites: true });
41818
+ // saving → mutar state (val-form.ngDoCheck lo detecta).
41819
+ effect(() => {
41820
+ const isSaving = this.saving();
41821
+ const state = isSaving ? ComponentStates.WORKING : ComponentStates.ENABLED;
41822
+ this.formMeta.state = state;
41823
+ this.formMeta.actions.state = state;
41824
+ this.formMeta.actions.text = isSaving ? this.t('saving') : this.t('save');
41825
+ });
41826
+ // profile loaded + form mounted → patchValue al FormGroup.
41827
+ effect(() => {
41828
+ const p = this.profile();
41829
+ const form = this.formCmp()?.Form;
41830
+ if (!p || !form)
41831
+ return;
41832
+ form.patchValue({
41833
+ handle: p.handle || '',
41834
+ name: p.name || '',
41835
+ phone: p.phone || '',
41836
+ }, { emitEvent: false });
41837
+ });
41838
+ // i18n.lang change → mutar labels (rare, no afecta typing).
41839
+ effect(() => {
41840
+ this.i18n.lang();
41841
+ this.applyI18nLabels();
41842
+ });
41843
+ }
41844
+ /** Construcción inicial — UNA vez. Gatea handle/phone según config. */
41845
+ buildFormMeta() {
41846
+ const cfg = this.resolvedConfig();
41847
+ const handleField = {
41848
+ token: 'profile-handle',
41849
+ name: 'handle',
41850
+ label: this.t('handle'),
41851
+ hint: this.t('handleHint'),
41852
+ placeholder: this.t('handlePlaceholder'),
41853
+ type: InputType.HANDLE,
41854
+ order: 1,
41855
+ validators: [
41856
+ Validators.required,
41857
+ Validators.minLength(HANDLE_MIN),
41858
+ Validators.maxLength(HANDLE_MAX),
41859
+ Validators.pattern(HANDLE_PATTERN),
41860
+ ],
41861
+ errors: {
41862
+ required: this.t('usernameInvalid'),
41863
+ minlength: this.t('usernameInvalid'),
41864
+ maxlength: this.t('usernameInvalid'),
41865
+ pattern: this.t('usernameInvalid'),
41866
+ },
41867
+ value: '',
41868
+ range: { min: HANDLE_MIN, max: HANDLE_MAX },
41869
+ state: ComponentStates.ENABLED,
41870
+ };
41871
+ const nameField = {
41872
+ token: 'profile-name',
41873
+ name: 'name',
41874
+ label: this.t('name'),
41875
+ hint: this.t('nameHint'),
41876
+ placeholder: this.t('namePlaceholder'),
41877
+ type: InputType.TEXT,
41878
+ order: 2,
41879
+ validators: [Validators.required, Validators.minLength(2), Validators.maxLength(100)],
41880
+ errors: {},
41881
+ value: '',
41882
+ state: ComponentStates.ENABLED,
41883
+ };
41884
+ const phoneField = {
41885
+ token: 'profile-phone',
41886
+ name: 'phone',
41887
+ label: this.t('phone'),
41888
+ hint: this.t('phoneHint'),
41889
+ placeholder: this.t('phonePlaceholder'),
41890
+ type: InputType.PHONE,
41891
+ order: 3,
41892
+ validators: [],
41893
+ errors: {},
41894
+ value: '',
41895
+ state: ComponentStates.ENABLED,
41896
+ };
41897
+ // Filtrado por config: el name siempre va; handle/phone son opt-out.
41898
+ const fields = [];
41899
+ if (cfg.showHandle)
41900
+ fields.push(handleField);
41901
+ fields.push(nameField);
41902
+ if (cfg.showPhone)
41903
+ fields.push(phoneField);
41904
+ const submitButton = {
41905
+ text: this.t('save'),
41906
+ color: 'dark',
41907
+ type: 'submit',
41908
+ fill: 'solid',
41909
+ size: 'default',
41910
+ shape: 'round',
41911
+ expand: 'block',
41912
+ state: ComponentStates.ENABLED,
41913
+ token: 'profile-save',
41914
+ };
41915
+ return {
41916
+ name: '',
41917
+ state: ComponentStates.ENABLED,
41918
+ sections: [
41919
+ {
41920
+ name: '',
41921
+ order: 1,
41922
+ fields,
41923
+ },
41924
+ ],
41925
+ actions: submitButton,
41926
+ };
41927
+ }
41928
+ /** Re-aplica labels/hints/placeholders en cambio de idioma. */
41929
+ applyI18nLabels() {
41930
+ const section = this.formMeta.sections[0];
41931
+ for (const field of section.fields) {
41932
+ switch (field.name) {
41933
+ case 'handle':
41934
+ field.label = this.t('handle');
41935
+ field.hint = this.t('handleHint');
41936
+ field.placeholder = this.t('handlePlaceholder');
41937
+ field.errors = {
41938
+ required: this.t('usernameInvalid'),
41939
+ minlength: this.t('usernameInvalid'),
41940
+ maxlength: this.t('usernameInvalid'),
41941
+ pattern: this.t('usernameInvalid'),
41942
+ };
41943
+ break;
41944
+ case 'name':
41945
+ field.label = this.t('name');
41946
+ field.hint = this.t('nameHint');
41947
+ field.placeholder = this.t('namePlaceholder');
41948
+ break;
41949
+ case 'phone':
41950
+ field.label = this.t('phone');
41951
+ field.hint = this.t('phoneHint');
41952
+ field.placeholder = this.t('phonePlaceholder');
41953
+ break;
41954
+ }
41955
+ }
41956
+ const button = this.formMeta.actions;
41957
+ button.text = this.saving() ? this.t('saving') : this.t('save');
41958
+ }
41959
+ /**
41960
+ * Fallback REST. Capa 4 (UI declarativa): NO toasteamos acá — el
41961
+ * val-empty-state ES la UI del error.
41962
+ */
41963
+ async loadFromApi() {
41964
+ this.loading.set(true);
41965
+ try {
41966
+ const p = await firstValueFrom(this.auth.getProfile());
41967
+ this.profile.set(p);
41968
+ this.loadError.set(null);
41969
+ }
41970
+ catch (err) {
41971
+ this.loadError.set(err);
41972
+ }
41973
+ finally {
41974
+ this.loading.set(false);
41975
+ }
41976
+ }
41977
+ mapMirrorToProfile(doc) {
41978
+ return {
41979
+ operationId: '',
41980
+ userId: doc.userId,
41981
+ email: doc.email,
41982
+ name: doc.name ?? '',
41983
+ handle: doc.handle,
41984
+ phone: doc.phone,
41985
+ avatarUrl: doc.avatarUrl,
41986
+ emailVerified: false,
41987
+ phoneVerified: false,
41988
+ mfaEnabled: false,
41989
+ createdAt: '',
41990
+ updatedAt: '',
41991
+ };
41992
+ }
41993
+ async onSubmit(event) {
41994
+ if (this.saving())
41995
+ return;
41996
+ const current = this.profile();
41997
+ if (!current)
41998
+ return;
41999
+ const cfg = this.resolvedConfig();
42000
+ const fields = event.fields || {};
42001
+ const nextHandle = String(fields['handle'] || '')
42002
+ .trim()
42003
+ .toLowerCase();
42004
+ const nextName = String(fields['name'] || '').trim();
42005
+ const nextPhone = String(fields['phone'] || '').trim();
42006
+ const handleChanged = cfg.showHandle && !!nextHandle && nextHandle !== (current.handle || '').toLowerCase();
42007
+ const nameChanged = !!nextName && nextName !== (current.name || '');
42008
+ const phoneChanged = cfg.showPhone && nextPhone !== (current.phone || '');
42009
+ if (!handleChanged && !nameChanged && !phoneChanged) {
42010
+ this.toast.show({
42011
+ message: this.t('saveSuccess'),
42012
+ duration: 2000,
42013
+ color: 'dark',
42014
+ position: 'top',
42015
+ });
42016
+ return;
42017
+ }
42018
+ this.saving.set(true);
42019
+ try {
42020
+ if (handleChanged) {
42021
+ try {
42022
+ await firstValueFrom(this.auth.updateHandle(nextHandle));
42023
+ }
42024
+ catch (err) {
42025
+ this.errors.handle(err, {
42026
+ context: 'profile.updateHandle',
42027
+ i18nMap: { AUTH_USERNAME_EXISTS: 'handleInUse' },
42028
+ fallbackKey: 'handleError',
42029
+ i18nNamespace: this.ns,
42030
+ });
42031
+ await this.loadFromApi();
42032
+ return;
42033
+ }
42034
+ }
42035
+ if (nameChanged || phoneChanged) {
42036
+ const payload = {};
42037
+ if (nameChanged)
42038
+ payload.name = nextName;
42039
+ if (phoneChanged)
42040
+ payload.phone = nextPhone;
42041
+ try {
42042
+ await firstValueFrom(this.auth.updateProfile(payload));
42043
+ }
42044
+ catch (err) {
42045
+ this.errors.handle(err, {
42046
+ context: 'profile.updateProfile',
42047
+ i18nMap: {
42048
+ AUTHV2_PHONE_EXISTS: 'phoneInUse',
42049
+ AUTHV2_INVALID_PHONE: 'phoneInvalid',
42050
+ },
42051
+ fallbackKey: 'saveError',
42052
+ i18nNamespace: this.ns,
42053
+ });
42054
+ await this.loadFromApi();
42055
+ return;
42056
+ }
42057
+ }
42058
+ this.toast.show({
42059
+ message: this.t('saveSuccess'),
42060
+ duration: 2500,
42061
+ color: 'dark',
42062
+ position: 'top',
42063
+ });
42064
+ cfg.onSaved?.(this.profile() ?? current);
42065
+ }
42066
+ finally {
42067
+ this.saving.set(false);
42068
+ }
42069
+ }
42070
+ onAvatarUploaded(result) {
42071
+ this.toast.show({
42072
+ message: this.t('avatarSuccess'),
42073
+ duration: 2500,
42074
+ color: 'dark',
42075
+ position: 'top',
42076
+ });
42077
+ this.resolvedConfig().onAvatarUploaded?.(result);
42078
+ }
42079
+ onAvatarError(_err) {
42080
+ this.toast.show({
42081
+ message: this.t('avatarError'),
42082
+ duration: 3000,
42083
+ color: 'dark',
42084
+ position: 'top',
42085
+ });
42086
+ }
42087
+ /** Lee del namespace configurado. */
42088
+ t(key) {
42089
+ return this.i18n.t(key, this.ns);
42090
+ }
42091
+ initialsFromName(name) {
42092
+ if (!name)
42093
+ return '?';
42094
+ const parts = name.trim().split(/\s+/).filter(Boolean);
42095
+ if (parts.length === 0)
42096
+ return '?';
42097
+ const first = parts[0]?.[0] ?? '';
42098
+ const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? '') : '';
42099
+ return (first + last).toUpperCase() || '?';
42100
+ }
42101
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
42102
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ProfileViewComponent, isStandalone: true, selector: "val-profile-view", inputs: { config: "config" }, viewQueries: [{ propertyName: "formCmp", first: true, predicate: ["form"], descendants: true, isSignal: true }], ngImport: i0, template: `
42103
+ <div class="page">
42104
+ <header class="page-header">
42105
+ <val-display [props]="{ size: 'small', color: 'dark', content: t('pageTitle') }" />
42106
+ <val-title
42107
+ [props]="{
42108
+ size: 'large',
42109
+ color: 'dark',
42110
+ bold: false,
42111
+ content: t('pageDescription'),
42112
+ }"
42113
+ />
42114
+ </header>
42115
+
42116
+ @if (loading()) {
42117
+ <val-skeleton-layout [props]="{ preset: 'form', rows: 3, showAvatar: true }" />
42118
+ } @else if (errorState()) {
42119
+ <val-empty-state [props]="errorState()!" />
42120
+ } @else {
42121
+ @if (profile(); as p) {
42122
+ @if (resolvedConfig().showAvatar) {
42123
+ <section class="settings-section avatar-section">
42124
+ <val-avatar-upload
42125
+ [props]="avatarProps()"
42126
+ (uploaded)="onAvatarUploaded($event)"
42127
+ (error)="onAvatarError($event)"
42128
+ />
42129
+ <div class="avatar-meta">
42130
+ <val-title
42131
+ [props]="{
42132
+ size: 'medium',
42133
+ color: 'dark',
42134
+ bold: true,
42135
+ content: p.name || '—',
42136
+ }"
42137
+ />
42138
+ <val-text
42139
+ [props]="{
42140
+ size: 'small',
42141
+ color: 'dark',
42142
+ bold: false,
42143
+ content: p.email,
42144
+ }"
42145
+ />
42146
+ @if (p.handle) {
42147
+ <val-text
42148
+ [props]="{
42149
+ size: 'small',
42150
+ color: 'dark',
42151
+ bold: true,
42152
+ content: '@' + p.handle,
42153
+ }"
42154
+ />
42155
+ }
42156
+ </div>
42157
+ </section>
42158
+ }
42159
+
42160
+ <val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
42161
+ }
42162
+ }
42163
+ </div>
42164
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.page-header h1{font-size:22px;font-weight:700;margin:0 0 4px}.hint{font-size:13px;opacity:.7;margin:0 0 12px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.avatar-section{display:flex;align-items:center;gap:16px}.avatar-meta{display:flex;flex-direction:column;gap:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: AvatarUploadComponent, selector: "val-avatar-upload", inputs: ["props"], outputs: ["uploaded", "error", "uploadStart"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }] }); }
42165
+ }
42166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileViewComponent, decorators: [{
42167
+ type: Component,
42168
+ args: [{ selector: 'val-profile-view', standalone: true, imports: [
42169
+ CommonModule,
42170
+ ReactiveFormsModule,
42171
+ AvatarUploadComponent,
42172
+ EmptyStateComponent,
42173
+ FormComponent,
42174
+ DisplayComponent,
42175
+ TextComponent,
42176
+ TitleComponent,
42177
+ SkeletonLayoutComponent,
42178
+ ], template: `
42179
+ <div class="page">
42180
+ <header class="page-header">
42181
+ <val-display [props]="{ size: 'small', color: 'dark', content: t('pageTitle') }" />
42182
+ <val-title
42183
+ [props]="{
42184
+ size: 'large',
42185
+ color: 'dark',
42186
+ bold: false,
42187
+ content: t('pageDescription'),
42188
+ }"
42189
+ />
42190
+ </header>
42191
+
42192
+ @if (loading()) {
42193
+ <val-skeleton-layout [props]="{ preset: 'form', rows: 3, showAvatar: true }" />
42194
+ } @else if (errorState()) {
42195
+ <val-empty-state [props]="errorState()!" />
42196
+ } @else {
42197
+ @if (profile(); as p) {
42198
+ @if (resolvedConfig().showAvatar) {
42199
+ <section class="settings-section avatar-section">
42200
+ <val-avatar-upload
42201
+ [props]="avatarProps()"
42202
+ (uploaded)="onAvatarUploaded($event)"
42203
+ (error)="onAvatarError($event)"
42204
+ />
42205
+ <div class="avatar-meta">
42206
+ <val-title
42207
+ [props]="{
42208
+ size: 'medium',
42209
+ color: 'dark',
42210
+ bold: true,
42211
+ content: p.name || '—',
42212
+ }"
42213
+ />
42214
+ <val-text
42215
+ [props]="{
42216
+ size: 'small',
42217
+ color: 'dark',
42218
+ bold: false,
42219
+ content: p.email,
42220
+ }"
42221
+ />
42222
+ @if (p.handle) {
42223
+ <val-text
42224
+ [props]="{
42225
+ size: 'small',
42226
+ color: 'dark',
42227
+ bold: true,
42228
+ content: '@' + p.handle,
42229
+ }"
42230
+ />
42231
+ }
42232
+ </div>
42233
+ </section>
42234
+ }
42235
+
42236
+ <val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
42237
+ }
42238
+ }
42239
+ </div>
42240
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.page-header h1{font-size:22px;font-weight:700;margin:0 0 4px}.hint{font-size:13px;opacity:.7;margin:0 0 12px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.avatar-section{display:flex;align-items:center;gap:16px}.avatar-meta{display:flex;flex-direction:column;gap:2px}\n"] }]
42241
+ }], ctorParameters: () => [], propDecorators: { config: [{
42242
+ type: Input
42243
+ }] } });
42244
+
42245
+ /**
42246
+ * Helper para montar la vista Perfil (`val-profile-view`) como ruta en una app
42247
+ * del factory. El `config` se pasa por route `data` (`profileConfig`) y el
42248
+ * componente lo lee como fallback de su `@Input() config`.
42249
+ *
42250
+ * @example
42251
+ * ```ts
42252
+ * // settings.routes.ts
42253
+ * export const settingsRoutes: Routes = [
42254
+ * ...provideValtechProfileRoutes(),
42255
+ * { path: 'preferences', loadComponent: () => ... },
42256
+ * ];
42257
+ *
42258
+ * // con config acotada:
42259
+ * ...provideValtechProfileRoutes({ config: { showPhone: false } }),
42260
+ * ```
42261
+ */
42262
+ function provideValtechProfileRoutes(opts) {
42263
+ return [
42264
+ {
42265
+ path: opts?.path ?? 'profile',
42266
+ component: ProfileViewComponent,
42267
+ data: { profileConfig: opts?.config },
42268
+ },
42269
+ ];
42270
+ }
42271
+
41492
42272
  /** Built-in label sets for the three platform locales. */
41493
42273
  const CALLOUT_LABELS = {
41494
42274
  es: {
@@ -42451,108 +43231,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
42451
43231
  type: Output
42452
43232
  }] } });
42453
43233
 
42454
- /**
42455
- * PageRefreshService — bus del pull-to-refresh estándar del factory.
42456
- *
42457
- * **Por qué existe:** `val-page-wrapper` posee un único `<ion-content>` y todas
42458
- * las páginas se renderizan dentro vía `<router-outlet>`. Un `<ion-refresher>`
42459
- * tiene que vivir dentro de ese `ion-content`, así que el refresher NO puede
42460
- * declararse en el template de cada página — vive una sola vez en
42461
- * `val-page-wrapper`. Este servicio es el puente: la página activa registra
42462
- * *qué hacer* al refrescar; el page-wrapper dispara el gesto y cierra el spinner.
42463
- *
42464
- * **Uso en una página** (ver `frontend/CLAUDE.md` — es el patrón estándar):
42465
- *
42466
- * ```ts
42467
- * export class MiPage implements ViewWillEnter, ViewWillLeave {
42468
- * private pageRefresh = inject(PageRefreshService);
42469
- *
42470
- * ionViewWillEnter(): void {
42471
- * this.pageRefresh.register(() => this.reload());
42472
- * }
42473
- * ionViewWillLeave(): void {
42474
- * this.pageRefresh.unregister();
42475
- * }
42476
- *
42477
- * private async reload(): Promise<void> {
42478
- * // re-fetch / re-suscribir streams / etc.
42479
- * }
42480
- * }
42481
- * ```
42482
- *
42483
- * Páginas que no llaman `register()` simplemente no muestran refresher —
42484
- * opt-in, sin impacto.
42485
- */
42486
- class PageRefreshService {
42487
- constructor() {
42488
- this.handler = null;
42489
- /**
42490
- * `true` cuando hay una página con handler registrado. `val-page-wrapper`
42491
- * renderiza el `<val-refresher>` sólo cuando esto es `true`.
42492
- */
42493
- this.hasHandler = signal(false);
42494
- }
42495
- /**
42496
- * Registra el handler de refresh de la página activa. Llamar en
42497
- * `ionViewWillEnter`. Si ya había uno, lo reemplaza (sólo hay una página
42498
- * activa a la vez en el router-outlet de Ionic).
42499
- */
42500
- register(handler) {
42501
- this.handler = handler;
42502
- this.hasHandler.set(true);
42503
- }
42504
- /**
42505
- * Quita el handler. Llamar en `ionViewWillLeave` para que el refresher
42506
- * desaparezca al salir de la vista.
42507
- */
42508
- unregister() {
42509
- this.handler = null;
42510
- this.hasHandler.set(false);
42511
- }
42512
- /**
42513
- * Ejecuta el handler registrado y espera a que termine. Lo invoca
42514
- * `val-page-wrapper` al detectar el gesto de pull. No-op si no hay handler.
42515
- */
42516
- async run() {
42517
- if (!this.handler)
42518
- return;
42519
- await this.handler();
42520
- }
42521
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
42522
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, providedIn: 'root' }); }
42523
- }
42524
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, decorators: [{
42525
- type: Injectable,
42526
- args: [{ providedIn: 'root' }]
42527
- }] });
42528
- /**
42529
- * Conecta una página al pull-to-refresh estándar en UNA línea, sin lifecycle
42530
- * hooks manuales: registra el handler y lo desregistra al destruir la página
42531
- * (vía `DestroyRef`).
42532
- *
42533
- * DEBE llamarse en un injection context (constructor o field initializer).
42534
- *
42535
- * @example
42536
- * ```ts
42537
- * export class MiPage {
42538
- * private data = createRefreshableStream(() => this.svc.getX());
42539
- * readonly items = this.data.data;
42540
- *
42541
- * constructor() {
42542
- * connectPageRefresh(() => this.data.reload());
42543
- * }
42544
- * }
42545
- * ```
42546
- *
42547
- * Reemplaza el patrón `ngOnInit`→`register` / `ngOnDestroy`→`unregister`.
42548
- */
42549
- function connectPageRefresh(handler) {
42550
- const svc = inject(PageRefreshService);
42551
- const destroyRef = inject(DestroyRef);
42552
- svc.register(handler);
42553
- destroyRef.onDestroy(() => svc.unregister());
42554
- }
42555
-
42556
43234
  class NetworkStatusService {
42557
43235
  constructor() {
42558
43236
  this._isOnline = signal(typeof navigator !== 'undefined' ? navigator.onLine : true);
@@ -48790,6 +49468,7 @@ class ContentReactionComponent {
48790
49468
  ...s,
48791
49469
  isLoading: false,
48792
49470
  isSubmitted: true,
49471
+ comment: '',
48793
49472
  hadPreviousReaction: !useAnonymous, // Solo para autenticados
48794
49473
  }));
48795
49474
  this.reactionSubmit.emit({
@@ -52654,5 +53333,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
52654
53333
  * Generated bundle index. Do not edit.
52655
53334
  */
52656
53335
 
52657
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, 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, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, 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, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
53336
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, 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, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechProfileRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
52658
53337
  //# sourceMappingURL=valtech-components.mjs.map