valtech-components 2.0.996 → 2.0.998

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 (27) hide show
  1. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.mjs +667 -0
  2. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.mjs +71 -0
  3. package/esm2022/lib/components/organisms/notification-preferences-view/notification-preferences.routes.mjs +31 -0
  4. package/esm2022/lib/components/organisms/notification-preferences-view/types.mjs +2 -0
  5. package/esm2022/lib/components/organisms/permissions-view/permissions-view.component.mjs +511 -0
  6. package/esm2022/lib/components/organisms/permissions-view/permissions-view.i18n.mjs +116 -0
  7. package/esm2022/lib/components/organisms/permissions-view/permissions.routes.mjs +29 -0
  8. package/esm2022/lib/components/organisms/permissions-view/types.mjs +2 -0
  9. package/esm2022/lib/components/organisms/settings-hub/settings.routes.mjs +12 -1
  10. package/esm2022/lib/services/org/permission-catalog.service.mjs +33 -0
  11. package/esm2022/lib/version.mjs +2 -2
  12. package/esm2022/public-api.mjs +8 -1
  13. package/fesm2022/valtech-components.mjs +1532 -111
  14. package/fesm2022/valtech-components.mjs.map +1 -1
  15. package/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.d.ts +146 -0
  16. package/lib/components/organisms/notification-preferences-view/notification-preferences-view.i18n.d.ts +11 -0
  17. package/lib/components/organisms/notification-preferences-view/notification-preferences.routes.d.ts +26 -0
  18. package/lib/components/organisms/notification-preferences-view/types.d.ts +36 -0
  19. package/lib/components/organisms/permissions-view/permissions-view.component.d.ts +71 -0
  20. package/lib/components/organisms/permissions-view/permissions-view.i18n.d.ts +9 -0
  21. package/lib/components/organisms/permissions-view/permissions.routes.d.ts +24 -0
  22. package/lib/components/organisms/permissions-view/types.d.ts +26 -0
  23. package/lib/components/organisms/settings-hub/settings.routes.d.ts +15 -0
  24. package/lib/services/org/permission-catalog.service.d.ts +34 -0
  25. package/lib/version.d.ts +1 -1
  26. package/package.json +1 -1
  27. package/public-api.d.ts +7 -0
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
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, untracked, Injector, isSignal, ViewEncapsulation } from '@angular/core';
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, race, timer, shareReplay, forkJoin } from 'rxjs';
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, race, timer, forkJoin, shareReplay } from 'rxjs';
4
4
  import { catchError, switchMap, finalize, filter, take, map as map$1, tap, distinctUntilChanged as distinctUntilChanged$1, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
5
5
  import * as i1$3 from '@angular/common/http';
6
6
  import { provideHttpClient, withInterceptors, HttpClient, HttpErrorResponse } from '@angular/common/http';
@@ -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.996';
57
+ const VERSION = '2.0.998';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -41750,7 +41750,7 @@ const PROFILE_VIEW_I18N = {
41750
41750
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41751
41751
  const HANDLE_MIN = 3;
41752
41752
  const HANDLE_MAX = 20;
41753
- const DEFAULT_NAMESPACE$f = 'Settings.Profile';
41753
+ const DEFAULT_NAMESPACE$h = 'Settings.Profile';
41754
41754
  /**
41755
41755
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41756
41756
  *
@@ -41800,7 +41800,7 @@ class ProfileViewComponent {
41800
41800
  showPhone: merged.showPhone ?? true,
41801
41801
  showShareCta: merged.showShareCta ?? true,
41802
41802
  profileBaseUrl: merged.profileBaseUrl ?? '',
41803
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
41803
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$h,
41804
41804
  onSaved: merged.onSaved,
41805
41805
  onAvatarUploaded: merged.onAvatarUploaded,
41806
41806
  };
@@ -42716,7 +42716,7 @@ const PREFERENCES_VIEW_I18N = {
42716
42716
  },
42717
42717
  };
42718
42718
 
42719
- const DEFAULT_NAMESPACE$e = 'Settings.General';
42719
+ const DEFAULT_NAMESPACE$g = 'Settings.General';
42720
42720
  const DEFAULT_LANGUAGES = ['es', 'en'];
42721
42721
  /**
42722
42722
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42753,7 +42753,7 @@ class PreferencesViewComponent {
42753
42753
  showLanguage: merged.showLanguage ?? true,
42754
42754
  showFontSize: merged.showFontSize ?? true,
42755
42755
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42756
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
42756
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$g,
42757
42757
  onThemeChanged: merged.onThemeChanged,
42758
42758
  onLanguageChanged: merged.onLanguageChanged,
42759
42759
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -43401,62 +43401,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
43401
43401
  type: Input
43402
43402
  }] } });
43403
43403
 
43404
- /**
43405
- * Helper para montar el bloque completo de Settings en una app del factory: el
43406
- * HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
43407
- * (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
43408
- *
43409
- * Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
43410
- * lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
43411
- * secciones no promovidas — ver guía de promoción), pero el provider debe
43412
- * existir y compilar para el resto de apps.
43413
- *
43414
- * @example
43415
- * ```ts
43416
- * // app.routes.ts
43417
- * import { provideValtechSettingsRoutes } from 'valtech-components';
43418
- *
43419
- * export const routes: Routes = [
43420
- * {
43421
- * path: 'app',
43422
- * children: [
43423
- * ...provideValtechSettingsRoutes({
43424
- * hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
43425
- * extraRoutes: [
43426
- * { path: 'security', loadComponent: () => ... },
43427
- * ],
43428
- * }),
43429
- * ],
43430
- * },
43431
- * ];
43432
- * ```
43433
- */
43434
- function provideValtechSettingsRoutes(opts) {
43435
- const basePath = opts?.basePath ?? 'settings';
43436
- const includeProfile = opts?.includeProfile ?? true;
43437
- const includePreferences = opts?.includePreferences ?? true;
43438
- return [
43439
- {
43440
- path: basePath,
43441
- children: [
43442
- {
43443
- path: '',
43444
- component: SettingsHubComponent,
43445
- data: { settingsHubConfig: opts?.hubConfig },
43446
- },
43447
- ...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
43448
- ...(includePreferences
43449
- ? provideValtechPreferencesRoutes({
43450
- path: opts?.preferencesPath ?? 'preferences',
43451
- config: opts?.preferencesConfig,
43452
- })
43453
- : []),
43454
- ...(opts?.extraRoutes ?? []),
43455
- ],
43456
- },
43457
- ];
43458
- }
43459
-
43460
43404
  addIcons({ shieldCheckmarkOutline, checkmarkCircleOutline });
43461
43405
  /**
43462
43406
  * `val-login-attempt-modal` — modal de confirmación de intento de inicio de sesión.
@@ -44565,7 +44509,7 @@ const SECURITY_VIEW_I18N = {
44565
44509
  };
44566
44510
 
44567
44511
  addIcons({ laptopOutline, phonePortraitOutline });
44568
- const DEFAULT_NAMESPACE$d = 'Settings.Security';
44512
+ const DEFAULT_NAMESPACE$f = 'Settings.Security';
44569
44513
  /**
44570
44514
  * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44571
44515
  *
@@ -44614,7 +44558,7 @@ class SecurityViewComponent {
44614
44558
  showChangeEmail: merged.showChangeEmail ?? true,
44615
44559
  showMfa: merged.showMfa ?? true,
44616
44560
  showPolicies: merged.showPolicies ?? true,
44617
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
44561
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
44618
44562
  handleDeepLinks: merged.handleDeepLinks ?? true,
44619
44563
  homeRoute: merged.homeRoute ?? '/app/home',
44620
44564
  onMfaCompleted: merged.onMfaCompleted,
@@ -45527,7 +45471,7 @@ const CREATE_ORG_MODAL_I18N = {
45527
45471
  },
45528
45472
  };
45529
45473
 
45530
- const DEFAULT_NAMESPACE$c = 'CreateOrgModal';
45474
+ const DEFAULT_NAMESPACE$e = 'CreateOrgModal';
45531
45475
  /**
45532
45476
  * `val-create-org-modal` — modal de creación de organización (organism
45533
45477
  * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
@@ -45553,10 +45497,10 @@ class CreateOrgModalComponent {
45553
45497
  this.toast = inject(ToastService);
45554
45498
  this.errors = inject(ValtechErrorService);
45555
45499
  /** Namespace i18n con que la vista resuelve sus textos. */
45556
- this.i18nNamespace = DEFAULT_NAMESPACE$c;
45500
+ this.i18nNamespace = DEFAULT_NAMESPACE$e;
45557
45501
  this._busy = signal(false);
45558
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
45559
- this.i18n.registerContent(DEFAULT_NAMESPACE$c, CREATE_ORG_MODAL_I18N);
45502
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$e)) {
45503
+ this.i18n.registerContent(DEFAULT_NAMESPACE$e, CREATE_ORG_MODAL_I18N);
45560
45504
  }
45561
45505
  this.formMeta = signal(this.buildFormMeta());
45562
45506
  effect(() => {
@@ -45792,7 +45736,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
45792
45736
  },
45793
45737
  };
45794
45738
 
45795
- const DEFAULT_NAMESPACE$b = 'Settings.DeleteAccount';
45739
+ const DEFAULT_NAMESPACE$d = 'Settings.DeleteAccount';
45796
45740
  /**
45797
45741
  * `val-delete-account-modal` — modal de eliminación de cuenta (organism,
45798
45742
  * cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -45818,7 +45762,7 @@ class DeleteAccountModalComponent {
45818
45762
  this.i18n = inject(I18nService);
45819
45763
  this.toast = inject(ToastService);
45820
45764
  /** Namespace i18n con que la vista resuelve sus textos. */
45821
- this.i18nNamespace = DEFAULT_NAMESPACE$b;
45765
+ this.i18nNamespace = DEFAULT_NAMESPACE$d;
45822
45766
  this.hasPassword = signal(null);
45823
45767
  this.codeSent = signal(false);
45824
45768
  this.formState = signal(ComponentStates.ENABLED);
@@ -45885,8 +45829,8 @@ class DeleteAccountModalComponent {
45885
45829
  ],
45886
45830
  actions: this.submitBtn('delete-code-submit', 'confirmCta'),
45887
45831
  }));
45888
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45889
- this.i18n.registerContent(DEFAULT_NAMESPACE$b, DELETE_ACCOUNT_MODAL_I18N);
45832
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$d)) {
45833
+ this.i18n.registerContent(DEFAULT_NAMESPACE$d, DELETE_ACCOUNT_MODAL_I18N);
45890
45834
  }
45891
45835
  }
45892
45836
  ngOnInit() {
@@ -46087,7 +46031,7 @@ const ORG_INFO_SHEET_I18N = {
46087
46031
  },
46088
46032
  };
46089
46033
 
46090
- const DEFAULT_NAMESPACE$a = 'OrgInfoSheet';
46034
+ const DEFAULT_NAMESPACE$c = 'OrgInfoSheet';
46091
46035
  /**
46092
46036
  * `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
46093
46037
  * Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
@@ -46132,15 +46076,15 @@ class OrgInfoSheetComponent {
46132
46076
  text: this.closeProps.text || this.resolvedCloseLabel(),
46133
46077
  }));
46134
46078
  addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
46135
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
46136
- this.i18n.registerContent(DEFAULT_NAMESPACE$a, ORG_INFO_SHEET_I18N);
46079
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
46080
+ this.i18n.registerContent(DEFAULT_NAMESPACE$c, ORG_INFO_SHEET_I18N);
46137
46081
  }
46138
46082
  }
46139
46083
  dismiss() {
46140
46084
  this._modalRef?.dismiss(null, 'cancel');
46141
46085
  }
46142
46086
  tt(key) {
46143
- return this.i18n.t(key, DEFAULT_NAMESPACE$a);
46087
+ return this.i18n.t(key, DEFAULT_NAMESPACE$c);
46144
46088
  }
46145
46089
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46146
46090
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: OrgInfoSheetComponent, isStandalone: true, selector: "val-org-info-sheet", inputs: { title: "title", subtitle: "subtitle", body: "body", closeLabel: "closeLabel", closeProps: "closeProps", _modalRef: "_modalRef" }, ngImport: i0, template: `
@@ -46307,7 +46251,7 @@ const SWITCH_ORG_MODAL_I18N = {
46307
46251
  },
46308
46252
  };
46309
46253
 
46310
- const DEFAULT_NAMESPACE$9 = 'Settings.SwitchOrg';
46254
+ const DEFAULT_NAMESPACE$b = 'Settings.SwitchOrg';
46311
46255
  addIcons({ businessOutline, checkmarkCircleOutline });
46312
46256
  /**
46313
46257
  * `val-switch-org-modal` — modal de cambio de organización activa (organism
@@ -46333,7 +46277,7 @@ class SwitchOrgModalComponent {
46333
46277
  this.orgSwitch = inject(OrgSwitchService);
46334
46278
  this.errors = inject(ValtechErrorService);
46335
46279
  /** Namespace i18n con que la vista resuelve sus textos. */
46336
- this.i18nNamespace = DEFAULT_NAMESPACE$9;
46280
+ this.i18nNamespace = DEFAULT_NAMESPACE$b;
46337
46281
  this.orgs = signal([]);
46338
46282
  this.loading = signal(false);
46339
46283
  this.query = signal('');
@@ -46349,8 +46293,8 @@ class SwitchOrgModalComponent {
46349
46293
  return list;
46350
46294
  return list.filter(o => o.name.toLowerCase().includes(q));
46351
46295
  });
46352
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46353
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, SWITCH_ORG_MODAL_I18N);
46296
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
46297
+ this.i18n.registerContent(DEFAULT_NAMESPACE$b, SWITCH_ORG_MODAL_I18N);
46354
46298
  }
46355
46299
  this.loadOrgs();
46356
46300
  }
@@ -46668,7 +46612,7 @@ const ACCOUNT_VIEW_I18N = {
46668
46612
  };
46669
46613
 
46670
46614
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
46671
- const DEFAULT_NAMESPACE$8 = 'Settings.Account';
46615
+ const DEFAULT_NAMESPACE$a = 'Settings.Account';
46672
46616
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
46673
46617
  /**
46674
46618
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -46709,7 +46653,7 @@ class AccountViewComponent {
46709
46653
  showNewOrgCta: merged.showNewOrgCta ?? true,
46710
46654
  showLogout: merged.showLogout ?? true,
46711
46655
  showDeleteAccount: merged.showDeleteAccount ?? true,
46712
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$8,
46656
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$a,
46713
46657
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
46714
46658
  onOrgSwitched: merged.onOrgSwitched,
46715
46659
  onOrgCreated: merged.onOrgCreated,
@@ -47434,7 +47378,7 @@ const EDIT_ORG_MODAL_I18N = {
47434
47378
  },
47435
47379
  };
47436
47380
 
47437
- const DEFAULT_NAMESPACE$7 = 'EditOrgModal';
47381
+ const DEFAULT_NAMESPACE$9 = 'EditOrgModal';
47438
47382
  /**
47439
47383
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
47440
47384
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -47461,10 +47405,10 @@ class EditOrgModalComponent {
47461
47405
  /** Organización a editar (pasada por `componentProps`). */
47462
47406
  this.org = null;
47463
47407
  /** Namespace i18n con que la vista resuelve sus textos. */
47464
- this.i18nNamespace = DEFAULT_NAMESPACE$7;
47408
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
47465
47409
  this.saving = signal(false);
47466
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
47467
- this.i18n.registerContent(DEFAULT_NAMESPACE$7, EDIT_ORG_MODAL_I18N);
47410
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
47411
+ this.i18n.registerContent(DEFAULT_NAMESPACE$9, EDIT_ORG_MODAL_I18N);
47468
47412
  }
47469
47413
  this.formMeta = signal(this.buildFormMeta());
47470
47414
  effect(() => {
@@ -47702,7 +47646,7 @@ const INVITE_MEMBER_MODAL_I18N = {
47702
47646
  },
47703
47647
  };
47704
47648
 
47705
- const DEFAULT_NAMESPACE$6 = 'InviteModal';
47649
+ const DEFAULT_NAMESPACE$8 = 'InviteModal';
47706
47650
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
47707
47651
  /**
47708
47652
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -47733,7 +47677,7 @@ class InviteMemberModalComponent {
47733
47677
  /** Ids de miembros ya existentes (excluidos de los resultados). */
47734
47678
  this.existingMemberIds = [];
47735
47679
  /** Namespace i18n con que la vista resuelve sus textos. */
47736
- this.i18nNamespace = DEFAULT_NAMESPACE$6;
47680
+ this.i18nNamespace = DEFAULT_NAMESPACE$8;
47737
47681
  this.query = signal('');
47738
47682
  this.searching = signal(false);
47739
47683
  this.searchResults = signal([]);
@@ -47780,8 +47724,8 @@ class InviteMemberModalComponent {
47780
47724
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
47781
47725
  }));
47782
47726
  this.searchSubject = new Subject();
47783
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47784
- this.i18n.registerContent(DEFAULT_NAMESPACE$6, INVITE_MEMBER_MODAL_I18N);
47727
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
47728
+ this.i18n.registerContent(DEFAULT_NAMESPACE$8, INVITE_MEMBER_MODAL_I18N);
47785
47729
  }
47786
47730
  this.loadRoles();
47787
47731
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
@@ -48270,7 +48214,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
48270
48214
  },
48271
48215
  };
48272
48216
 
48273
- const DEFAULT_NAMESPACE$5 = 'Settings.MemberDetail';
48217
+ const DEFAULT_NAMESPACE$7 = 'Settings.MemberDetail';
48274
48218
  /**
48275
48219
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
48276
48220
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -48307,7 +48251,7 @@ class MemberDetailModalComponent {
48307
48251
  { id: 'admin', name: 'admin' },
48308
48252
  ];
48309
48253
  /** Namespace i18n con que la vista resuelve sus textos. */
48310
- this.i18nNamespace = DEFAULT_NAMESPACE$5;
48254
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
48311
48255
  this.working = signal(false);
48312
48256
  this.currentRole = signal('');
48313
48257
  this.roleControl = new FormControl('');
@@ -48347,8 +48291,8 @@ class MemberDetailModalComponent {
48347
48291
  type: 'button',
48348
48292
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
48349
48293
  }));
48350
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48351
- this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_DETAIL_MODAL_I18N);
48294
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
48295
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, MEMBER_DETAIL_MODAL_I18N);
48352
48296
  }
48353
48297
  }
48354
48298
  ngOnInit() {
@@ -48685,7 +48629,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
48685
48629
  },
48686
48630
  };
48687
48631
 
48688
- const DEFAULT_NAMESPACE$4 = 'Settings.TransferModal';
48632
+ const DEFAULT_NAMESPACE$6 = 'Settings.TransferModal';
48689
48633
  /**
48690
48634
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
48691
48635
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -48709,7 +48653,7 @@ class TransferOwnershipModalComponent {
48709
48653
  /** Lista de miembros transferibles (todos excepto el owner actual). */
48710
48654
  this.members = [];
48711
48655
  /** Namespace i18n con que la vista resuelve sus textos. */
48712
- this.i18nNamespace = DEFAULT_NAMESPACE$4;
48656
+ this.i18nNamespace = DEFAULT_NAMESPACE$6;
48713
48657
  this.query = signal('');
48714
48658
  this.selectedId = signal('');
48715
48659
  this.filtered = computed(() => {
@@ -48721,8 +48665,8 @@ class TransferOwnershipModalComponent {
48721
48665
  m.userId.toLowerCase().includes(q));
48722
48666
  });
48723
48667
  this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
48724
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
48725
- this.i18n.registerContent(DEFAULT_NAMESPACE$4, TRANSFER_OWNERSHIP_MODAL_I18N);
48668
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
48669
+ this.i18n.registerContent(DEFAULT_NAMESPACE$6, TRANSFER_OWNERSHIP_MODAL_I18N);
48726
48670
  }
48727
48671
  }
48728
48672
  ngOnInit() {
@@ -48970,7 +48914,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
48970
48914
  },
48971
48915
  };
48972
48916
 
48973
- const DEFAULT_NAMESPACE$3 = 'Settings.ImportModal';
48917
+ const DEFAULT_NAMESPACE$5 = 'Settings.ImportModal';
48974
48918
  /**
48975
48919
  * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
48976
48920
  *
@@ -48989,7 +48933,7 @@ class MemberImportModalComponent {
48989
48933
  /** Nombres de roles disponibles (para el hint). Opcional. */
48990
48934
  this.availableRoles = [];
48991
48935
  /** Namespace i18n. */
48992
- this.i18nNamespace = DEFAULT_NAMESPACE$3;
48936
+ this.i18nNamespace = DEFAULT_NAMESPACE$5;
48993
48937
  this.csv = '';
48994
48938
  this.onConflict = 'assignRole';
48995
48939
  this.sendActivation = true;
@@ -49003,8 +48947,8 @@ class MemberImportModalComponent {
49003
48947
  return this.t('summary');
49004
48948
  return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
49005
48949
  });
49006
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$3)) {
49007
- this.i18n.registerContent(DEFAULT_NAMESPACE$3, MEMBER_IMPORT_MODAL_I18N);
48950
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48951
+ this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_IMPORT_MODAL_I18N);
49008
48952
  }
49009
48953
  }
49010
48954
  ngOnInit() {
@@ -49376,7 +49320,7 @@ const API_KEYS_MODAL_I18N = {
49376
49320
  },
49377
49321
  };
49378
49322
 
49379
- const DEFAULT_NAMESPACE$2 = 'Settings.ApiKeysModal';
49323
+ const DEFAULT_NAMESPACE$4 = 'Settings.ApiKeysModal';
49380
49324
  /**
49381
49325
  * `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
49382
49326
  *
@@ -49389,7 +49333,7 @@ class ApiKeysModalComponent {
49389
49333
  this.i18n = inject(I18nService);
49390
49334
  this.apiKeys = inject(ApiKeyService);
49391
49335
  /** Namespace i18n. */
49392
- this.i18nNamespace = DEFAULT_NAMESPACE$2;
49336
+ this.i18nNamespace = DEFAULT_NAMESPACE$4;
49393
49337
  this.keys = signal([]);
49394
49338
  this.availablePermissions = signal([]);
49395
49339
  this.loading = signal(true);
@@ -49401,8 +49345,8 @@ class ApiKeysModalComponent {
49401
49345
  this.name = '';
49402
49346
  this.expiresInDays = null;
49403
49347
  this.selectedPerms = signal(new Set());
49404
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$2)) {
49405
- this.i18n.registerContent(DEFAULT_NAMESPACE$2, API_KEYS_MODAL_I18N);
49348
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
49349
+ this.i18n.registerContent(DEFAULT_NAMESPACE$4, API_KEYS_MODAL_I18N);
49406
49350
  }
49407
49351
  }
49408
49352
  ngOnInit() {
@@ -49902,7 +49846,7 @@ const ORGANIZATION_VIEW_I18N = {
49902
49846
  },
49903
49847
  };
49904
49848
 
49905
- const DEFAULT_NAMESPACE$1 = 'Settings.Organization';
49849
+ const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49906
49850
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
49907
49851
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49908
49852
  /**
@@ -49959,7 +49903,7 @@ class OrganizationViewComponent {
49959
49903
  showInvite: merged.showInvite ?? true,
49960
49904
  showTransferOwnership: merged.showTransferOwnership ?? true,
49961
49905
  showLeave: merged.showLeave ?? true,
49962
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
49906
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49963
49907
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49964
49908
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49965
49909
  onOrgEdited: merged.onOrgEdited,
@@ -51039,6 +50983,70 @@ function provideValtechOrganizationRoutes(opts) {
51039
50983
  ];
51040
50984
  }
51041
50985
 
50986
+ /**
50987
+ * Helper para montar el bloque completo de Settings en una app del factory: el
50988
+ * HUB de navegación (`val-settings-hub`) en el index + las vistas promovidas
50989
+ * (Perfil, Preferencias) + las rutas propias de la app (`extraRoutes`).
50990
+ *
50991
+ * Pensado para apps nuevas (greenfield) que adoptan el set entero. showcase NO
50992
+ * lo usa hoy (preserva su `settings.routes.ts` por la semántica de sus muchas
50993
+ * secciones no promovidas — ver guía de promoción), pero el provider debe
50994
+ * existir y compilar para el resto de apps.
50995
+ *
50996
+ * @example
50997
+ * ```ts
50998
+ * // app.routes.ts
50999
+ * import { provideValtechSettingsRoutes } from 'valtech-components';
51000
+ *
51001
+ * export const routes: Routes = [
51002
+ * {
51003
+ * path: 'app',
51004
+ * children: [
51005
+ * ...provideValtechSettingsRoutes({
51006
+ * hubConfig: { onLogout: () => auth.logoutWithConfirmation() },
51007
+ * extraRoutes: [
51008
+ * { path: 'security', loadComponent: () => ... },
51009
+ * ],
51010
+ * }),
51011
+ * ],
51012
+ * },
51013
+ * ];
51014
+ * ```
51015
+ */
51016
+ function provideValtechSettingsRoutes(opts) {
51017
+ const basePath = opts?.basePath ?? 'settings';
51018
+ const includeProfile = opts?.includeProfile ?? true;
51019
+ const includePreferences = opts?.includePreferences ?? true;
51020
+ const includeSecurity = opts?.includeSecurity ?? true;
51021
+ const includeAccount = opts?.includeAccount ?? true;
51022
+ const includeOrganization = opts?.includeOrganization ?? true;
51023
+ return [
51024
+ {
51025
+ path: basePath,
51026
+ children: [
51027
+ {
51028
+ path: '',
51029
+ component: SettingsHubComponent,
51030
+ data: { settingsHubConfig: opts?.hubConfig },
51031
+ },
51032
+ ...(includeProfile ? provideValtechProfileRoutes({ config: opts?.profileConfig }) : []),
51033
+ ...(includePreferences
51034
+ ? provideValtechPreferencesRoutes({
51035
+ path: opts?.preferencesPath ?? 'preferences',
51036
+ config: opts?.preferencesConfig,
51037
+ })
51038
+ : []),
51039
+ ...(includeSecurity ? provideValtechSecurityRoutes({ config: opts?.securityConfig }) : []),
51040
+ ...(includeAccount ? provideValtechAccountRoutes({ config: opts?.accountConfig }) : []),
51041
+ ...(includeOrganization
51042
+ ? provideValtechOrganizationRoutes({ config: opts?.organizationConfig })
51043
+ : []),
51044
+ ...(opts?.extraRoutes ?? []),
51045
+ ],
51046
+ },
51047
+ ];
51048
+ }
51049
+
51042
51050
  /**
51043
51051
  * Refreshable Stream
51044
51052
  *
@@ -51247,7 +51255,7 @@ addIcons({
51247
51255
  openOutline,
51248
51256
  personOutline,
51249
51257
  });
51250
- const DEFAULT_NAMESPACE = 'Notifications';
51258
+ const DEFAULT_NAMESPACE$2 = 'Notifications';
51251
51259
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
51252
51260
  const DEFAULT_PAGE_SIZE = 10;
51253
51261
  const DEFAULT_MAX_LOAD = 50;
@@ -51307,7 +51315,7 @@ class NotificationsViewComponent {
51307
51315
  showPushOffCta: merged.showPushOffCta ?? true,
51308
51316
  enableMarkAll: merged.enableMarkAll ?? true,
51309
51317
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
51310
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
51318
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
51311
51319
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
51312
51320
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
51313
51321
  onNotificationOpen: merged.onNotificationOpen,
@@ -51975,6 +51983,1419 @@ function provideValtechNotificationsRoutes(opts) {
51975
51983
  ];
51976
51984
  }
51977
51985
 
51986
+ /**
51987
+ * Defaults i18n (es/en) embebidos en `val-notification-preferences-view`.
51988
+ * Auto-registrados en el constructor del componente si el consumer no proveyó
51989
+ * el namespace (`Settings.Notifications` por default). SOLO incluye las keys que
51990
+ * la vista usa.
51991
+ *
51992
+ * Portado del override `Settings.Notifications` (showcase) bajo el proceso de
51993
+ * ADR-021.
51994
+ */
51995
+ const NOTIFICATION_PREFERENCES_VIEW_I18N = {
51996
+ es: {
51997
+ pageTitle: 'Notificaciones',
51998
+ pageDescription: 'Configura como quieres recibir notificaciones',
51999
+ // Control principal (UX simplificado)
52000
+ simpleTitle: 'Notificaciones push en este dispositivo',
52001
+ simpleDescription: 'Recibe alertas en tiempo real incluso con la app cerrada.',
52002
+ enabling: 'Activando…',
52003
+ disabling: 'Desactivando…',
52004
+ enabled: 'Activadas',
52005
+ errorGeneric: 'No pudimos completar la configuración. Verifica los permisos del navegador y reintenta.',
52006
+ retryCta: 'Reintentar',
52007
+ // Mensajes contextuales
52008
+ permissionUnsupported: 'Tu navegador no soporta notificaciones push.',
52009
+ permissionDeniedInfo: 'Tu navegador esta bloqueando las notificaciones. Para habilitarlas, ve a la configuracion del sitio y permitelas manualmente.',
52010
+ // iOS PWA install
52011
+ iosInstallTitle: 'En iOS, instala la app para recibir notificaciones',
52012
+ iosInstallHint: 'Las notificaciones push en iOS (iPhone/iPad) solo funcionan cuando la app está instalada en la pantalla de inicio. Edge, Chrome y Firefox en iOS comparten esta limitación.',
52013
+ iosInstallStep1: 'Abre este sitio en Safari (no en Chrome/Edge).',
52014
+ iosInstallStep2: 'Toca el botón Compartir (□ con flecha hacia arriba).',
52015
+ iosInstallStep3: "Elige 'Añadir a pantalla de inicio'.",
52016
+ iosInstallStep4: 'Abre la app desde el ícono recién creado y vuelve aquí.',
52017
+ // Detalles técnicos (expandible)
52018
+ detailsShow: 'Ver detalles técnicos',
52019
+ detailsHide: 'Ocultar detalles',
52020
+ detailsToken: 'Token',
52021
+ detailsNone: 'Sin registrar',
52022
+ diagnosticsPermission: 'Permiso del navegador',
52023
+ diagnosticsSupported: 'Soporte del navegador',
52024
+ // Toasts
52025
+ enableSuccess: 'Notificaciones habilitadas',
52026
+ disableSuccess: 'Notificaciones deshabilitadas',
52027
+ },
52028
+ en: {
52029
+ pageTitle: 'Notifications',
52030
+ pageDescription: 'Configure how you want to receive notifications',
52031
+ simpleTitle: 'Push notifications on this device',
52032
+ simpleDescription: 'Get real-time alerts even when the app is closed.',
52033
+ enabling: 'Enabling…',
52034
+ disabling: 'Disabling…',
52035
+ enabled: 'Enabled',
52036
+ errorGeneric: "We couldn't complete the setup. Check your browser permissions and retry.",
52037
+ retryCta: 'Retry',
52038
+ permissionUnsupported: 'Your browser does not support push notifications.',
52039
+ permissionDeniedInfo: 'Your browser is blocking notifications. To enable them, go to site settings and allow them manually.',
52040
+ iosInstallTitle: 'On iOS, install the app to receive notifications',
52041
+ iosInstallHint: 'Push notifications on iOS (iPhone/iPad) only work when the app is installed on the Home Screen. Edge, Chrome and Firefox on iOS share this limitation.',
52042
+ iosInstallStep1: 'Open this site in Safari (not Chrome/Edge).',
52043
+ iosInstallStep2: 'Tap the Share button (□ with an up arrow).',
52044
+ iosInstallStep3: "Choose 'Add to Home Screen'.",
52045
+ iosInstallStep4: 'Open the app from the new icon and come back here.',
52046
+ detailsShow: 'View technical details',
52047
+ detailsHide: 'Hide details',
52048
+ detailsToken: 'Token',
52049
+ detailsNone: 'Not registered',
52050
+ diagnosticsPermission: 'Browser permission',
52051
+ diagnosticsSupported: 'Browser support',
52052
+ enableSuccess: 'Notifications enabled',
52053
+ disableSuccess: 'Notifications disabled',
52054
+ },
52055
+ };
52056
+
52057
+ addIcons({
52058
+ alertCircleOutline,
52059
+ chevronDownOutline,
52060
+ chevronUpOutline,
52061
+ closeCircleOutline,
52062
+ notificationsOffOutline,
52063
+ notificationsOutline,
52064
+ refreshOutline,
52065
+ });
52066
+ const DEFAULT_NAMESPACE$1 = 'Settings.Notifications';
52067
+ /**
52068
+ * `val-notification-preferences-view` — **preferencias de notificaciones push**
52069
+ * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
52070
+ * proceso de ADR-021.
52071
+ *
52072
+ * Separada de `val-notifications-view` (el inbox / feed). Aquí el user **activa o
52073
+ * desactiva** las notificaciones push en este dispositivo.
52074
+ *
52075
+ * UX simplificado: UN solo toggle que ejecuta toda la secuencia (pedir permiso →
52076
+ * SW ready → getToken → registrar en backend → set master pref) vía
52077
+ * `MessagingService.enable()`. El registro del device en el backend va como
52078
+ * callback (la lib no depende de `AuthService` para evitar ciclos). Mensajes
52079
+ * contextuales y botón "Reintentar" aparecen solo cuando aplican.
52080
+ *
52081
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
52082
+ * inyecta `{ optional: true }` → funciona sin router (embebida / Storybook).
52083
+ */
52084
+ class NotificationPreferencesViewComponent {
52085
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
52086
+ get ns() {
52087
+ return this.resolvedConfig().i18nNamespace;
52088
+ }
52089
+ // ---------------------------------------------------------------------------
52090
+ // Lifecycle
52091
+ // ---------------------------------------------------------------------------
52092
+ constructor() {
52093
+ this.nav = inject(NavigationService);
52094
+ this.i18n = inject(I18nService);
52095
+ this.toast = inject(ToastService);
52096
+ this.auth = inject(AuthService);
52097
+ this.messaging = inject(MessagingService);
52098
+ this.pageRefresh = inject(PageRefreshService);
52099
+ this.prefs = inject(PreferencesService);
52100
+ // `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
52101
+ // router (solo lo usa para leer el route data de config como fallback).
52102
+ this.route = inject(ActivatedRoute, { optional: true });
52103
+ this.resolvedConfig = computed(() => {
52104
+ const fromRoute = (this.route?.snapshot.data['notificationPreferencesConfig'] ??
52105
+ {});
52106
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
52107
+ return {
52108
+ showDetails: merged.showDetails ?? true,
52109
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
52110
+ onPushEnabled: merged.onPushEnabled,
52111
+ onPushDisabled: merged.onPushDisabled,
52112
+ };
52113
+ });
52114
+ /** Operación en curso (gobierna spinner del toggle + label "Activando…"). */
52115
+ this._busy = signal(null);
52116
+ /** True si el último intento falló — gobierna estado `error` + retry button. */
52117
+ this._lastFailed = signal(false);
52118
+ /** True mientras se re-obtiene el token FCM en background (no afecta la UI). */
52119
+ this._repairingToken = false;
52120
+ /** Toggle del expand de detalles técnicos. */
52121
+ this.showDetails = signal(false);
52122
+ this.permission = signal('default');
52123
+ this.isSupported = signal(false);
52124
+ this.currentToken = signal(null);
52125
+ this.needsIOSInstall = signal(this.detectNeedsIOSInstall());
52126
+ /** FormControl del toggle (sync-eado con `isEnabled` via effect). */
52127
+ this.toggleControl = new FormControl(false, {
52128
+ nonNullable: true,
52129
+ });
52130
+ /** Handler del listener `focus` — referencia guardada para poder removerlo. */
52131
+ this.onWindowFocus = () => this.scheduleBrowserRefresh();
52132
+ /** Timer del debounce del refresh disparado por `focus`. */
52133
+ this.focusDebounceTimer = null;
52134
+ /** Ventana de debounce (ms) para el refresh disparado por `focus`. */
52135
+ this.FOCUS_DEBOUNCE_MS = 500;
52136
+ // ---------------------------------------------------------------------------
52137
+ // Estado derivado
52138
+ // ---------------------------------------------------------------------------
52139
+ this.pageTitle = computed(() => this.tt('pageTitle'));
52140
+ this.pageDescription = computed(() => this.tt('pageDescription'));
52141
+ /**
52142
+ * Estado efectivo del toggle: "el usuario tiene push activo".
52143
+ *
52144
+ * Se deriva SOLO de la decisión del usuario (`master`) y del permiso del SO
52145
+ * (`permission`). El token FCM NO forma parte del estado de UI: vive en
52146
+ * memoria, se pierde en cada recarga/suspensión de la PWA y produciría flicker.
52147
+ * Si falta token con `master && granted`, es un detalle reparable en
52148
+ * background (ver `repairTokenIfNeeded`), no un cambio del toggle.
52149
+ */
52150
+ this.isEnabled = computed(() => this.prefs.notificationsMaster() && this.permission() === 'granted');
52151
+ this.state = computed(() => {
52152
+ if (this.needsIOSInstall())
52153
+ return 'needs-install';
52154
+ if (this.permission() === 'unsupported' || !this.isSupported()) {
52155
+ // iOS standalone aún puede dar isSupported false temprano antes de init —
52156
+ // solo bloqueamos si el browser explícitamente no soporta APIs.
52157
+ if (this.permission() === 'unsupported')
52158
+ return 'unsupported';
52159
+ }
52160
+ const busy = this._busy();
52161
+ if (busy === 'enabling')
52162
+ return 'enabling';
52163
+ if (busy === 'disabling')
52164
+ return 'disabling';
52165
+ if (this.isEnabled())
52166
+ return 'enabled';
52167
+ if (this.permission() === 'denied')
52168
+ return 'blocked';
52169
+ if (this._lastFailed())
52170
+ return 'error';
52171
+ return 'idle';
52172
+ });
52173
+ this.busyLabel = computed(() => {
52174
+ const s = this.state();
52175
+ if (s === 'enabling')
52176
+ return this.tt('enabling');
52177
+ if (s === 'disabling')
52178
+ return this.tt('disabling');
52179
+ return null;
52180
+ });
52181
+ this.contextMessage = computed(() => {
52182
+ switch (this.state()) {
52183
+ case 'unsupported':
52184
+ return this.tt('permissionUnsupported');
52185
+ case 'blocked':
52186
+ return this.tt('permissionDeniedInfo');
52187
+ case 'error':
52188
+ return this.tt('errorGeneric');
52189
+ default:
52190
+ return null;
52191
+ }
52192
+ });
52193
+ this.alertVariant = computed(() => {
52194
+ const s = this.state();
52195
+ if (s === 'error' || s === 'blocked')
52196
+ return 'warning';
52197
+ if (s === 'unsupported')
52198
+ return 'danger';
52199
+ return 'info';
52200
+ });
52201
+ this.alertIcon = computed(() => {
52202
+ const s = this.state();
52203
+ if (s === 'unsupported')
52204
+ return 'close-circle-outline';
52205
+ return 'alert-circle-outline';
52206
+ });
52207
+ this.toggleProps = computed(() => ({
52208
+ control: this.toggleControl,
52209
+ color: 'primary',
52210
+ disabled: this._busy() !== null ||
52211
+ this.state() === 'unsupported' ||
52212
+ this.state() === 'needs-install' ||
52213
+ this.state() === 'blocked',
52214
+ }));
52215
+ this.retryButtonProps = computed(() => ({
52216
+ text: this.tt('retryCta'),
52217
+ color: 'dark',
52218
+ fill: 'outline',
52219
+ size: 'default',
52220
+ shape: 'round',
52221
+ type: 'button',
52222
+ icon: { name: 'refresh-outline', slot: 'start' },
52223
+ state: this._busy() !== null ? ComponentStates.WORKING : ComponentStates.ENABLED,
52224
+ }));
52225
+ this.tokenPreview = computed(() => {
52226
+ const t = this.currentToken();
52227
+ if (!t)
52228
+ return null;
52229
+ return `${t.slice(0, 22)}…${t.slice(-6)}`;
52230
+ });
52231
+ const ns = this.ns;
52232
+ if (!this.i18n.hasNamespace(ns)) {
52233
+ this.i18n.registerContent(ns, NOTIFICATION_PREFERENCES_VIEW_I18N);
52234
+ }
52235
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
52236
+ void this.refreshBrowserState();
52237
+ // Re-aplicar header en cambio de idioma.
52238
+ effect(() => {
52239
+ this.i18n.lang();
52240
+ this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
52241
+ }, { allowSignalWrites: true });
52242
+ // Sync FormControl ←→ estado efectivo. Cuando el estado cambia (cambio de
52243
+ // permission por el browser, o snapshot Firestore de `master`), el toggle se
52244
+ // re-alinea con la realidad.
52245
+ //
52246
+ // IMPORTANTE: se ignoran los cambios mientras hay una operación en curso
52247
+ // (`_busy() !== null`). Durante `onEnable`/`onDisable` las señales fuente se
52248
+ // actualizan en momentos distintos y producirían estados intermedios →
52249
+ // flicker visible. El toggle se re-alinea UNA sola vez en el `finally`.
52250
+ effect(() => {
52251
+ const eff = this.isEnabled();
52252
+ if (this._busy() !== null)
52253
+ return;
52254
+ if (this.toggleControl.value !== eff) {
52255
+ this.toggleControl.setValue(eff, { emitEvent: false });
52256
+ }
52257
+ }, { allowSignalWrites: true });
52258
+ // Interceptar click del user: el toggle ya cambió visualmente, ejecutar
52259
+ // la acción correspondiente.
52260
+ this.toggleControl.valueChanges.subscribe(v => {
52261
+ if (this._busy() !== null)
52262
+ return;
52263
+ if (v && !this.isEnabled()) {
52264
+ void this.onEnable();
52265
+ }
52266
+ else if (!v && this.isEnabled()) {
52267
+ void this.onDisable();
52268
+ }
52269
+ });
52270
+ // Detectar regreso a la pestaña — el user pudo haber cambiado permisos
52271
+ // desde la config del browser. El refresh va con debounce para no
52272
+ // dispararse en ráfagas (en PWA mobile `focus` salta con cada cambio de
52273
+ // app / aparición del teclado). El listener se remueve en ngOnDestroy.
52274
+ if (typeof window !== 'undefined') {
52275
+ window.addEventListener('focus', this.onWindowFocus);
52276
+ }
52277
+ }
52278
+ /**
52279
+ * Registra el pull-to-refresh estándar. Aunque esta vista no muestra una
52280
+ * lista remota, sí refleja estado mutable del browser (permiso, token,
52281
+ * soporte FCM) que puede cambiar fuera de la app — el refresh re-evalúa todo.
52282
+ *
52283
+ * Se usa `ngOnInit`/`ngOnDestroy` (NO `ionViewWillEnter`) porque
52284
+ * `val-page-wrapper` usa `<router-outlet>` plano — los hooks `ionView*` solo
52285
+ * disparan dentro de `<ion-router-outlet>`.
52286
+ */
52287
+ ngOnInit() {
52288
+ this.pageRefresh.register(() => this.refreshBrowserState());
52289
+ }
52290
+ ngOnDestroy() {
52291
+ this.pageRefresh.unregister();
52292
+ if (typeof window !== 'undefined') {
52293
+ window.removeEventListener('focus', this.onWindowFocus);
52294
+ }
52295
+ if (this.focusDebounceTimer !== null) {
52296
+ clearTimeout(this.focusDebounceTimer);
52297
+ this.focusDebounceTimer = null;
52298
+ }
52299
+ }
52300
+ /** Refresca el estado del browser con debounce — evita ráfagas de `focus`. */
52301
+ scheduleBrowserRefresh() {
52302
+ if (this.focusDebounceTimer !== null) {
52303
+ clearTimeout(this.focusDebounceTimer);
52304
+ }
52305
+ this.focusDebounceTimer = setTimeout(() => {
52306
+ this.focusDebounceTimer = null;
52307
+ void this.refreshBrowserState();
52308
+ }, this.FOCUS_DEBOUNCE_MS);
52309
+ }
52310
+ // ---------------------------------------------------------------------------
52311
+ // Actions
52312
+ // ---------------------------------------------------------------------------
52313
+ /**
52314
+ * Secuencia completa de activación.
52315
+ *
52316
+ * Toda la mecánica (permission → SW ready → getToken con retry → watchdog de
52317
+ * auto-reload) vive en `MessagingService.enable()` de la lib. El paso de
52318
+ * registro del device en el backend se pasa como callback (`registerDevice`)
52319
+ * porque la lib no puede depender de `AuthService` sin crear un ciclo.
52320
+ *
52321
+ * `enable()` no hace throw: resuelve con un `EnablePushResult` descriptivo.
52322
+ */
52323
+ async onEnable() {
52324
+ this._busy.set('enabling');
52325
+ this._lastFailed.set(false);
52326
+ try {
52327
+ const result = await this.messaging.enable({
52328
+ registerDevice: token => this.auth.registerDevice(token).then(r => r.registered),
52329
+ });
52330
+ if (result.status === 'enabled') {
52331
+ // Persistir flag master si aún no estaba activo.
52332
+ if (!this.prefs.notificationsMaster()) {
52333
+ await this.prefs.setNotificationsMaster(true);
52334
+ }
52335
+ this.toast.show({
52336
+ message: this.tt('enableSuccess'),
52337
+ duration: 2000,
52338
+ color: 'dark',
52339
+ position: 'top',
52340
+ });
52341
+ this.resolvedConfig().onPushEnabled?.();
52342
+ }
52343
+ else if (result.status === 'timeout' && result.reloaded) {
52344
+ // La lib disparó el auto-reload — la página está por recargarse.
52345
+ return;
52346
+ }
52347
+ else {
52348
+ // denied / unsupported / error / timeout sin reload → fallo recuperable.
52349
+ this._lastFailed.set(true);
52350
+ this.toast.show({
52351
+ message: this.tt('errorGeneric'),
52352
+ duration: 3000,
52353
+ color: 'dark',
52354
+ position: 'top',
52355
+ });
52356
+ }
52357
+ }
52358
+ catch {
52359
+ // `enable()` no hace throw, pero el callback de registro sí puede.
52360
+ this._lastFailed.set(true);
52361
+ this.toast.show({
52362
+ message: this.tt('errorGeneric'),
52363
+ duration: 3000,
52364
+ color: 'dark',
52365
+ position: 'top',
52366
+ });
52367
+ }
52368
+ finally {
52369
+ // Único refresh final: re-alinea el toggle UNA sola vez con el estado real.
52370
+ await this.refreshBrowserState();
52371
+ this._busy.set(null);
52372
+ }
52373
+ }
52374
+ async onDisable() {
52375
+ this._busy.set('disabling');
52376
+ this._lastFailed.set(false);
52377
+ try {
52378
+ await this.auth.disableNotifications();
52379
+ if (this.prefs.notificationsMaster()) {
52380
+ await this.prefs.setNotificationsMaster(false);
52381
+ }
52382
+ this.toast.show({
52383
+ message: this.tt('disableSuccess'),
52384
+ duration: 2000,
52385
+ color: 'dark',
52386
+ position: 'top',
52387
+ });
52388
+ this.resolvedConfig().onPushDisabled?.();
52389
+ }
52390
+ catch {
52391
+ this._lastFailed.set(true);
52392
+ }
52393
+ finally {
52394
+ await this.refreshBrowserState();
52395
+ this._busy.set(null);
52396
+ }
52397
+ }
52398
+ /** Reintentar la secuencia completa de activación, sin importar el estado actual. */
52399
+ async onRetry() {
52400
+ this._lastFailed.set(false);
52401
+ await this.onEnable();
52402
+ }
52403
+ toggleDetails() {
52404
+ this.showDetails.update(v => !v);
52405
+ }
52406
+ // ---------------------------------------------------------------------------
52407
+ // Privados
52408
+ // ---------------------------------------------------------------------------
52409
+ async refreshBrowserState() {
52410
+ this.isSupported.set(await this.messaging.isSupported());
52411
+ this.permission.set(this.auth.getNotificationPermissionState());
52412
+ this.currentToken.set(this.messaging.currentToken ?? null);
52413
+ void this.repairTokenIfNeeded();
52414
+ }
52415
+ /**
52416
+ * Repara el token FCM en background.
52417
+ *
52418
+ * Si el usuario tiene push activo (`master && granted`) pero no hay token en
52419
+ * memoria — caso típico tras un cold start de la PWA — re-obtenemos el token
52420
+ * y re-registramos el device sin tocar el toggle ni mostrar un error
52421
+ * alarmista. El token es un detalle de implementación reparable, no parte del
52422
+ * estado de UI (ver `isEnabled`).
52423
+ */
52424
+ async repairTokenIfNeeded() {
52425
+ if (this._busy() !== null)
52426
+ return;
52427
+ if (this._repairingToken)
52428
+ return;
52429
+ if (!this.isEnabled())
52430
+ return;
52431
+ if (this.currentToken())
52432
+ return;
52433
+ this._repairingToken = true;
52434
+ try {
52435
+ const token = await this.messaging.getToken();
52436
+ if (token) {
52437
+ this.currentToken.set(token);
52438
+ await this.auth.registerDevice(token);
52439
+ }
52440
+ }
52441
+ catch {
52442
+ // Reparación best-effort: no degradamos el toggle ni mostramos error.
52443
+ }
52444
+ finally {
52445
+ this._repairingToken = false;
52446
+ }
52447
+ }
52448
+ /**
52449
+ * iOS browser sin estar instalado como PWA — push web no disponible.
52450
+ * Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple).
52451
+ */
52452
+ detectNeedsIOSInstall() {
52453
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
52454
+ return false;
52455
+ }
52456
+ const ua = navigator.userAgent || '';
52457
+ const isIOS = /iPad|iPhone|iPod/.test(ua);
52458
+ if (!isIOS)
52459
+ return false;
52460
+ const isStandalone = window.matchMedia?.('(display-mode: standalone)').matches ||
52461
+ navigator.standalone === true;
52462
+ return !isStandalone;
52463
+ }
52464
+ tt(key) {
52465
+ return this.i18n.t(key, this.ns);
52466
+ }
52467
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
52468
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationPreferencesViewComponent, isStandalone: true, selector: "val-notification-preferences-view", inputs: { config: "config" }, ngImport: i0, template: `
52469
+ <div class="page">
52470
+ <header class="page-header">
52471
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
52472
+ <val-title
52473
+ [props]="{
52474
+ size: 'large',
52475
+ color: 'dark',
52476
+ bold: false,
52477
+ content: pageDescription(),
52478
+ }"
52479
+ />
52480
+ </header>
52481
+
52482
+ @if (state() === 'needs-install') {
52483
+ <section class="settings-section">
52484
+ <val-title
52485
+ [props]="{
52486
+ size: 'medium',
52487
+ color: 'dark',
52488
+ bold: true,
52489
+ content: tt('iosInstallTitle'),
52490
+ }"
52491
+ />
52492
+ <div class="section-body">
52493
+ <val-text
52494
+ [props]="{
52495
+ size: 'small',
52496
+ color: 'medium',
52497
+ bold: false,
52498
+ content: tt('iosInstallHint'),
52499
+ }"
52500
+ />
52501
+ <ol class="install-steps">
52502
+ <li>{{ tt('iosInstallStep1') }}</li>
52503
+ <li>{{ tt('iosInstallStep2') }}</li>
52504
+ <li>{{ tt('iosInstallStep3') }}</li>
52505
+ <li>{{ tt('iosInstallStep4') }}</li>
52506
+ </ol>
52507
+ </div>
52508
+ </section>
52509
+ }
52510
+
52511
+ <section class="settings-section">
52512
+ <div class="notif-row">
52513
+ <div class="notif-row__icon">
52514
+ <ion-icon name="notifications-outline" aria-hidden="true" />
52515
+ </div>
52516
+ <div class="notif-row__body">
52517
+ <val-title
52518
+ [props]="{
52519
+ size: 'medium',
52520
+ color: 'dark',
52521
+ bold: true,
52522
+ content: tt('simpleTitle'),
52523
+ }"
52524
+ />
52525
+ <val-text
52526
+ [props]="{
52527
+ size: 'small',
52528
+ color: 'medium',
52529
+ bold: false,
52530
+ content: tt('simpleDescription'),
52531
+ }"
52532
+ />
52533
+ @if (busyLabel(); as label) {
52534
+ <span class="notif-row__busy">{{ label }}</span>
52535
+ }
52536
+ </div>
52537
+ <div class="notif-row__control">
52538
+ <val-toggle-input [props]="toggleProps()" />
52539
+ </div>
52540
+ </div>
52541
+
52542
+ @if (contextMessage(); as msg) {
52543
+ <div class="alert" [class]="'alert--' + alertVariant()">
52544
+ <ion-icon [name]="alertIcon()" aria-hidden="true" />
52545
+ <span>{{ msg }}</span>
52546
+ </div>
52547
+ }
52548
+
52549
+ @if (state() === 'error') {
52550
+ <div class="row-actions">
52551
+ <val-button [props]="retryButtonProps()" (click)="onRetry()" />
52552
+ </div>
52553
+ }
52554
+ </section>
52555
+
52556
+ @if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
52557
+ <section class="settings-section">
52558
+ <button class="details-toggle" type="button" (click)="toggleDetails()">
52559
+ <span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
52560
+ <ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
52561
+ </button>
52562
+ @if (showDetails()) {
52563
+ <dl class="details">
52564
+ <div class="details__row">
52565
+ <dt>{{ tt('detailsToken') }}</dt>
52566
+ <dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
52567
+ </div>
52568
+ <div class="details__row">
52569
+ <dt>{{ tt('diagnosticsPermission') }}</dt>
52570
+ <dd>{{ permission() }}</dd>
52571
+ </div>
52572
+ <div class="details__row">
52573
+ <dt>{{ tt('diagnosticsSupported') }}</dt>
52574
+ <dd>{{ isSupported() ? '✓' : '✗' }}</dd>
52575
+ </div>
52576
+ </dl>
52577
+ }
52578
+ </section>
52579
+ }
52580
+ </div>
52581
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ToggleInputComponent, selector: "val-toggle-input", inputs: ["preset", "props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
52582
+ }
52583
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationPreferencesViewComponent, decorators: [{
52584
+ type: Component,
52585
+ args: [{ selector: 'val-notification-preferences-view', standalone: true, imports: [IonIcon, ToggleInputComponent, ButtonComponent, DisplayComponent, TitleComponent, TextComponent], template: `
52586
+ <div class="page">
52587
+ <header class="page-header">
52588
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
52589
+ <val-title
52590
+ [props]="{
52591
+ size: 'large',
52592
+ color: 'dark',
52593
+ bold: false,
52594
+ content: pageDescription(),
52595
+ }"
52596
+ />
52597
+ </header>
52598
+
52599
+ @if (state() === 'needs-install') {
52600
+ <section class="settings-section">
52601
+ <val-title
52602
+ [props]="{
52603
+ size: 'medium',
52604
+ color: 'dark',
52605
+ bold: true,
52606
+ content: tt('iosInstallTitle'),
52607
+ }"
52608
+ />
52609
+ <div class="section-body">
52610
+ <val-text
52611
+ [props]="{
52612
+ size: 'small',
52613
+ color: 'medium',
52614
+ bold: false,
52615
+ content: tt('iosInstallHint'),
52616
+ }"
52617
+ />
52618
+ <ol class="install-steps">
52619
+ <li>{{ tt('iosInstallStep1') }}</li>
52620
+ <li>{{ tt('iosInstallStep2') }}</li>
52621
+ <li>{{ tt('iosInstallStep3') }}</li>
52622
+ <li>{{ tt('iosInstallStep4') }}</li>
52623
+ </ol>
52624
+ </div>
52625
+ </section>
52626
+ }
52627
+
52628
+ <section class="settings-section">
52629
+ <div class="notif-row">
52630
+ <div class="notif-row__icon">
52631
+ <ion-icon name="notifications-outline" aria-hidden="true" />
52632
+ </div>
52633
+ <div class="notif-row__body">
52634
+ <val-title
52635
+ [props]="{
52636
+ size: 'medium',
52637
+ color: 'dark',
52638
+ bold: true,
52639
+ content: tt('simpleTitle'),
52640
+ }"
52641
+ />
52642
+ <val-text
52643
+ [props]="{
52644
+ size: 'small',
52645
+ color: 'medium',
52646
+ bold: false,
52647
+ content: tt('simpleDescription'),
52648
+ }"
52649
+ />
52650
+ @if (busyLabel(); as label) {
52651
+ <span class="notif-row__busy">{{ label }}</span>
52652
+ }
52653
+ </div>
52654
+ <div class="notif-row__control">
52655
+ <val-toggle-input [props]="toggleProps()" />
52656
+ </div>
52657
+ </div>
52658
+
52659
+ @if (contextMessage(); as msg) {
52660
+ <div class="alert" [class]="'alert--' + alertVariant()">
52661
+ <ion-icon [name]="alertIcon()" aria-hidden="true" />
52662
+ <span>{{ msg }}</span>
52663
+ </div>
52664
+ }
52665
+
52666
+ @if (state() === 'error') {
52667
+ <div class="row-actions">
52668
+ <val-button [props]="retryButtonProps()" (click)="onRetry()" />
52669
+ </div>
52670
+ }
52671
+ </section>
52672
+
52673
+ @if (resolvedConfig().showDetails && state() !== 'needs-install' && state() !== 'unsupported') {
52674
+ <section class="settings-section">
52675
+ <button class="details-toggle" type="button" (click)="toggleDetails()">
52676
+ <span>{{ showDetails() ? tt('detailsHide') : tt('detailsShow') }}</span>
52677
+ <ion-icon [name]="showDetails() ? 'chevron-up-outline' : 'chevron-down-outline'" aria-hidden="true" />
52678
+ </button>
52679
+ @if (showDetails()) {
52680
+ <dl class="details">
52681
+ <div class="details__row">
52682
+ <dt>{{ tt('detailsToken') }}</dt>
52683
+ <dd>{{ tokenPreview() ?? tt('detailsNone') }}</dd>
52684
+ </div>
52685
+ <div class="details__row">
52686
+ <dt>{{ tt('diagnosticsPermission') }}</dt>
52687
+ <dd>{{ permission() }}</dd>
52688
+ </div>
52689
+ <div class="details__row">
52690
+ <dt>{{ tt('diagnosticsSupported') }}</dt>
52691
+ <dd>{{ isSupported() ? '✓' : '✗' }}</dd>
52692
+ </div>
52693
+ </dl>
52694
+ }
52695
+ </section>
52696
+ }
52697
+ </div>
52698
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:8px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.notif-row{display:grid;grid-template-columns:44px 1fr auto;gap:16px;align-items:center}.notif-row__icon{width:44px;height:44px;border-radius:50%;background:var(--ion-color-light, rgba(0, 0, 0, .06));display:flex;align-items:center;justify-content:center}.notif-row__icon ion-icon{font-size:22px;color:var(--ion-color-dark, #1a1a1a)}.notif-row__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif-row__busy{font-size:12px;color:var(--ion-color-medium, #92949c);font-style:italic;margin-top:4px}.notif-row__control{flex-shrink:0}.alert{display:flex;gap:10px;align-items:flex-start;margin-top:12px;padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.45;background:var(--ion-color-light, rgba(0, 0, 0, .04));color:var(--ion-color-dark, #1a1a1a)}.alert ion-icon{font-size:18px;flex-shrink:0;margin-top:1px}.alert--warning ion-icon{color:var(--ion-color-warning-shade, #c79e08)}.alert--danger ion-icon{color:var(--ion-color-danger-shade, #cf3c4f)}.alert--info ion-icon{color:var(--ion-color-medium, #6b6b6b)}.install-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.6;color:var(--ion-color-dark, #1a1a1a)}.install-steps li+li{margin-top:4px}.details-toggle{display:flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-medium, #6b6b6b);cursor:pointer}.details-toggle ion-icon{font-size:14px}.details{margin:12px 0 0;display:flex;flex-direction:column;gap:6px}.details__row{display:grid;grid-template-columns:140px 1fr;gap:12px;font-size:12px;align-items:baseline}.details__row dt{margin:0;color:var(--ion-color-medium, #6b6b6b);text-transform:uppercase;letter-spacing:.4px;font-weight:500}.details__row dd{margin:0;color:var(--ion-color-dark, #1a1a1a);font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;word-break:break-all}\n"] }]
52699
+ }], ctorParameters: () => [], propDecorators: { config: [{
52700
+ type: Input
52701
+ }] } });
52702
+
52703
+ /**
52704
+ * Helper para montar las **preferencias de notificaciones push**
52705
+ * (`val-notification-preferences-view`) como ruta en una app del factory. El
52706
+ * `config` se pasa por route `data` (`notificationPreferencesConfig`) y el
52707
+ * componente lo lee como fallback de su `@Input() config`.
52708
+ *
52709
+ * No confundir con `provideValtechNotificationsRoutes` (el inbox / feed).
52710
+ *
52711
+ * @example
52712
+ * ```ts
52713
+ * // app.routes.ts (típicamente bajo settings)
52714
+ * export const settingsRoutes: Routes = [
52715
+ * ...provideValtechNotificationPreferencesRoutes(), // → notifications
52716
+ * ];
52717
+ *
52718
+ * // con config acotada:
52719
+ * ...provideValtechNotificationPreferencesRoutes({ config: { showDetails: false } }),
52720
+ * ...provideValtechNotificationPreferencesRoutes({ path: 'push' }),
52721
+ * ```
52722
+ */
52723
+ function provideValtechNotificationPreferencesRoutes(opts) {
52724
+ return [
52725
+ {
52726
+ path: opts?.path ?? 'notifications',
52727
+ component: NotificationPreferencesViewComponent,
52728
+ data: { notificationPreferencesConfig: opts?.config },
52729
+ },
52730
+ ];
52731
+ }
52732
+
52733
+ /**
52734
+ * Consume el catálogo de permisos del backend (`GET /org/{orgId}/permissions-catalog`).
52735
+ * El backend es la fuente de verdad de qué permisos existen y de su alcance
52736
+ * (globales de plataforma vs. propios de una app). La respuesta incluye el
52737
+ * `appId` del request para que la vista distinga la app actual.
52738
+ *
52739
+ * Promovido desde `showcase` a la lib bajo el proceso de ADR-021. Usa
52740
+ * `VALTECH_AUTH_CONFIG.apiUrl` (como `OrgService`) en vez del `environment` de la
52741
+ * app — la lib no importa el environment del consumer.
52742
+ */
52743
+ class PermissionCatalogService {
52744
+ constructor(config, http) {
52745
+ this.config = config;
52746
+ this.http = http;
52747
+ }
52748
+ getCatalog(orgId) {
52749
+ return this.http.get(`${this.config.apiUrl}/org/${orgId}/permissions-catalog`);
52750
+ }
52751
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
52752
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, providedIn: 'root' }); }
52753
+ }
52754
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionCatalogService, decorators: [{
52755
+ type: Injectable,
52756
+ args: [{ providedIn: 'root' }]
52757
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
52758
+ type: Inject,
52759
+ args: [VALTECH_AUTH_CONFIG]
52760
+ }] }, { type: i1$3.HttpClient }] });
52761
+
52762
+ /**
52763
+ * Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
52764
+ * el constructor del componente si el consumer no proveyó el namespace
52765
+ * (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
52766
+ *
52767
+ * Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
52768
+ */
52769
+ const PERMISSIONS_VIEW_I18N = {
52770
+ es: {
52771
+ pageTitle: 'Permisos',
52772
+ pageDescription: 'Roles y permisos de esta app en la organización activa',
52773
+ systemRolesTitle: 'Roles del sistema',
52774
+ systemRolesHint: 'Predefinidos por la plataforma. No editables.',
52775
+ customRolesTitle: 'Roles personalizados',
52776
+ customRolesHint: 'Definidos por tu organización.',
52777
+ systemBadge: 'Sistema',
52778
+ noCustomRoles: 'Esta organización no tiene roles personalizados.',
52779
+ noPermissions: 'Sin permisos asignados.',
52780
+ // Grupos de alcance
52781
+ groupApp: 'Permisos de la app',
52782
+ groupPlatform: 'Permisos de plataforma',
52783
+ groupOther: 'Otras apps',
52784
+ // Roles del sistema
52785
+ roleViewer: 'Visor',
52786
+ roleEditor: 'Editor',
52787
+ roleAdmin: 'Administrador',
52788
+ roleSuperAdmin: 'Super Administrador',
52789
+ roleBingoAdmin: 'Bingo · Administrador',
52790
+ roleBingoTesorero: 'Bingo · Tesorero',
52791
+ roleBingoTaquilla: 'Bingo · Taquilla',
52792
+ roleBingoOperator: 'Bingo · Operador',
52793
+ // Apps
52794
+ app_showcase: 'Showcase',
52795
+ app_bingo: 'Bingo',
52796
+ // Recursos
52797
+ permUsers: 'Usuarios',
52798
+ permRoles: 'Roles',
52799
+ permRbac: 'Control de acceso',
52800
+ permApps: 'Apps',
52801
+ 'permApi-keys': 'API keys',
52802
+ 'permEmail-logs': 'Logs de correo',
52803
+ 'permSms-logs': 'Logs de SMS',
52804
+ permDocuments: 'Documentos',
52805
+ permMedia: 'Media',
52806
+ permTemplates: 'Plantillas',
52807
+ permRequests: 'Solicitudes',
52808
+ permBingo: 'Bingo',
52809
+ // Acciones
52810
+ permRead: 'Leer',
52811
+ permWrite: 'Escribir',
52812
+ permCreate: 'Crear',
52813
+ permDelete: 'Eliminar',
52814
+ permManage: 'Gestionar',
52815
+ permEdit: 'Editar',
52816
+ permSell: 'Vender',
52817
+ permCheckin: 'Canjear',
52818
+ permSettle: 'Rendir',
52819
+ permExport: 'Exportar',
52820
+ permOperate: 'Operar',
52821
+ permAll: 'Todo',
52822
+ // Loading / error
52823
+ errorTitle: 'Error al cargar permisos',
52824
+ retry: 'Reintentar',
52825
+ },
52826
+ en: {
52827
+ pageTitle: 'Permissions',
52828
+ pageDescription: 'Roles and permissions of this app in the active organization',
52829
+ systemRolesTitle: 'System roles',
52830
+ systemRolesHint: 'Defined by the platform. Not editable.',
52831
+ customRolesTitle: 'Custom roles',
52832
+ customRolesHint: 'Defined by your organization.',
52833
+ systemBadge: 'System',
52834
+ noCustomRoles: 'This organization has no custom roles.',
52835
+ noPermissions: 'No permissions assigned.',
52836
+ groupApp: 'App permissions',
52837
+ groupPlatform: 'Platform permissions',
52838
+ groupOther: 'Other apps',
52839
+ roleViewer: 'Viewer',
52840
+ roleEditor: 'Editor',
52841
+ roleAdmin: 'Administrator',
52842
+ roleSuperAdmin: 'Super Administrator',
52843
+ roleBingoAdmin: 'Bingo · Administrator',
52844
+ roleBingoTesorero: 'Bingo · Treasurer',
52845
+ roleBingoTaquilla: 'Bingo · Box office',
52846
+ roleBingoOperator: 'Bingo · Operator',
52847
+ app_showcase: 'Showcase',
52848
+ app_bingo: 'Bingo',
52849
+ permUsers: 'Users',
52850
+ permRoles: 'Roles',
52851
+ permRbac: 'Access control',
52852
+ permApps: 'Apps',
52853
+ 'permApi-keys': 'API keys',
52854
+ 'permEmail-logs': 'Email logs',
52855
+ 'permSms-logs': 'SMS logs',
52856
+ permDocuments: 'Documents',
52857
+ permMedia: 'Media',
52858
+ permTemplates: 'Templates',
52859
+ permRequests: 'Requests',
52860
+ permBingo: 'Bingo',
52861
+ permRead: 'Read',
52862
+ permWrite: 'Write',
52863
+ permCreate: 'Create',
52864
+ permDelete: 'Delete',
52865
+ permManage: 'Manage',
52866
+ permEdit: 'Edit',
52867
+ permSell: 'Sell',
52868
+ permCheckin: 'Check-in',
52869
+ permSettle: 'Settle',
52870
+ permExport: 'Export',
52871
+ permOperate: 'Operate',
52872
+ permAll: 'All',
52873
+ errorTitle: 'Error loading permissions',
52874
+ retry: 'Retry',
52875
+ },
52876
+ };
52877
+
52878
+ const DEFAULT_NAMESPACE = 'Settings.Permissions';
52879
+ /**
52880
+ * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
52881
+ * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
52882
+ * roles de plataforma) y los roles personalizados de la org activa, con el
52883
+ * catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
52884
+ * Promovida desde `showcase` bajo el proceso de ADR-021.
52885
+ *
52886
+ * **RBAC interno** (como `organization-view`): lee la org activa vía
52887
+ * `AuthService.user().activeOrgId`. La vista NO muta nada — el control de quién
52888
+ * puede verla es responsabilidad del guard de ruta del consumer; no hay gating
52889
+ * fino interno ni se expone como config.
52890
+ *
52891
+ * Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
52892
+ * `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
52893
+ * catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
52894
+ *
52895
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
52896
+ * inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
52897
+ * sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
52898
+ * `Settings.Permissions`).
52899
+ */
52900
+ class PermissionsViewComponent {
52901
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
52902
+ get ns() {
52903
+ return this.resolvedConfig().i18nNamespace;
52904
+ }
52905
+ constructor() {
52906
+ this.i18n = inject(I18nService);
52907
+ this.orgService = inject(OrgService);
52908
+ this.catalog = inject(PermissionCatalogService);
52909
+ this.auth = inject(AuthService);
52910
+ this.nav = inject(NavigationService);
52911
+ this.route = inject(ActivatedRoute, { optional: true });
52912
+ this.resolvedConfig = computed(() => {
52913
+ const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
52914
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
52915
+ return {
52916
+ showSystemRoles: merged.showSystemRoles ?? true,
52917
+ showCustomRoles: merged.showCustomRoles ?? true,
52918
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
52919
+ };
52920
+ });
52921
+ this.loading = signal(false);
52922
+ this.loadError = signal(null);
52923
+ this.roles = signal([]);
52924
+ this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
52925
+ this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
52926
+ this.activeOrgId = computed(() => {
52927
+ const u = this.auth.user();
52928
+ return u?.activeOrgId ?? u?.activeOrg ?? '';
52929
+ });
52930
+ this.retryButtonProps = computed(() => ({
52931
+ text: this.tt('retry'),
52932
+ color: 'dark',
52933
+ fill: 'outline',
52934
+ size: 'default',
52935
+ type: 'button',
52936
+ state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
52937
+ }));
52938
+ const ns = this.ns;
52939
+ if (!this.i18n.hasNamespace(ns)) {
52940
+ this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
52941
+ }
52942
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
52943
+ this.load();
52944
+ connectPageRefresh(() => this.load());
52945
+ }
52946
+ load() {
52947
+ const orgId = this.activeOrgId();
52948
+ if (!orgId)
52949
+ return;
52950
+ this.loading.set(true);
52951
+ this.loadError.set(null);
52952
+ forkJoin({
52953
+ roles: this.orgService.listOrgRoles(orgId),
52954
+ catalog: this.catalog.getCatalog(orgId),
52955
+ }).subscribe({
52956
+ next: ({ roles: rawRoles, catalog }) => {
52957
+ const byResource = new Map();
52958
+ for (const r of catalog.permissions)
52959
+ byResource.set(r.resource, r);
52960
+ const currentAppId = catalog.appId;
52961
+ this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
52962
+ id: r.id,
52963
+ name: r.name,
52964
+ description: r.description,
52965
+ permissions: r.permissions ?? [],
52966
+ isSystem: r.isSystem ?? false,
52967
+ }, byResource, currentAppId)));
52968
+ this.loading.set(false);
52969
+ },
52970
+ error: err => {
52971
+ this.loadError.set(err);
52972
+ this.loading.set(false);
52973
+ },
52974
+ });
52975
+ }
52976
+ buildRoleView(role, byResource, currentAppId) {
52977
+ const appPerms = [];
52978
+ const platformPerms = [];
52979
+ const otherByApp = new Map();
52980
+ for (const perm of role.permissions) {
52981
+ const [resource, action] = perm.split(':');
52982
+ const label = this.permLabel(resource, action);
52983
+ const entry = byResource.get(resource);
52984
+ if (entry?.scope === 'app') {
52985
+ if (entry.appId === currentAppId) {
52986
+ appPerms.push(label);
52987
+ }
52988
+ else {
52989
+ const appId = entry.appId ?? '?';
52990
+ const arr = otherByApp.get(appId) ?? [];
52991
+ arr.push(label);
52992
+ otherByApp.set(appId, arr);
52993
+ }
52994
+ }
52995
+ else {
52996
+ // global o desconocido → plataforma
52997
+ platformPerms.push(label);
52998
+ }
52999
+ }
53000
+ const groups = [];
53001
+ if (appPerms.length) {
53002
+ groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
53003
+ }
53004
+ if (platformPerms.length) {
53005
+ groups.push({
53006
+ key: 'platform',
53007
+ label: this.tt('groupPlatform'),
53008
+ perms: platformPerms,
53009
+ });
53010
+ }
53011
+ for (const [appId, perms] of otherByApp) {
53012
+ groups.push({
53013
+ key: `other:${appId}`,
53014
+ label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
53015
+ perms,
53016
+ });
53017
+ }
53018
+ return {
53019
+ id: role.id,
53020
+ name: role.name,
53021
+ description: role.description,
53022
+ isSystem: role.isSystem,
53023
+ groups,
53024
+ hasPerms: groups.length > 0,
53025
+ };
53026
+ }
53027
+ tt(key) {
53028
+ return this.i18n.t(key, this.ns);
53029
+ }
53030
+ appLabel(appId) {
53031
+ const key = `app_${appId}`;
53032
+ const label = this.tt(key);
53033
+ return label === key ? appId : label;
53034
+ }
53035
+ roleLabel(name) {
53036
+ const map = {
53037
+ viewer: 'roleViewer',
53038
+ editor: 'roleEditor',
53039
+ admin: 'roleAdmin',
53040
+ super_admin: 'roleSuperAdmin',
53041
+ bingo_admin: 'roleBingoAdmin',
53042
+ bingo_tesorero: 'roleBingoTesorero',
53043
+ bingo_taquilla: 'roleBingoTaquilla',
53044
+ bingo_operator: 'roleBingoOperator',
53045
+ };
53046
+ return this.tt(map[name] ?? name);
53047
+ }
53048
+ permLabel(resource, action) {
53049
+ if (resource === '*' && action === '*')
53050
+ return this.tt('permAll');
53051
+ const resKey = resource === '*' ? 'permAll' : `perm${resource.charAt(0).toUpperCase()}${resource.slice(1)}`;
53052
+ const actKey = action === '*' ? 'permAll' : `perm${action.charAt(0).toUpperCase()}${action.slice(1)}`;
53053
+ const res = this.tt(resKey);
53054
+ const act = this.tt(actKey);
53055
+ return `${res}: ${act}`;
53056
+ }
53057
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53058
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config" }, ngImport: i0, template: `
53059
+ <div class="page">
53060
+ <header class="page-header">
53061
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
53062
+ <val-title
53063
+ [props]="{
53064
+ size: 'large',
53065
+ color: 'dark',
53066
+ bold: false,
53067
+ content: tt('pageDescription'),
53068
+ }"
53069
+ />
53070
+ </header>
53071
+
53072
+ @if (loading()) {
53073
+ <div class="spinner-row">
53074
+ <ion-spinner name="crescent" />
53075
+ </div>
53076
+ } @else if (loadError()) {
53077
+ <div class="error-row">
53078
+ <val-text
53079
+ [props]="{
53080
+ content: tt('errorTitle'),
53081
+ size: 'medium',
53082
+ color: 'medium',
53083
+ bold: false,
53084
+ }"
53085
+ />
53086
+ <val-button [props]="retryButtonProps()" (click)="load()" />
53087
+ </div>
53088
+ } @else {
53089
+ @if (resolvedConfig().showSystemRoles) {
53090
+ <section class="perm-section">
53091
+ <div class="perm-section__header">
53092
+ <val-title
53093
+ [props]="{
53094
+ size: 'medium',
53095
+ color: 'dark',
53096
+ bold: true,
53097
+ content: tt('systemRolesTitle'),
53098
+ }"
53099
+ />
53100
+ <val-text
53101
+ [props]="{
53102
+ content: tt('systemRolesHint'),
53103
+ size: 'medium',
53104
+ color: 'medium',
53105
+ bold: false,
53106
+ }"
53107
+ />
53108
+ </div>
53109
+ <div class="role-list">
53110
+ @for (role of systemRoles(); track role.id) {
53111
+ <div class="role-card">
53112
+ <div class="role-card__header">
53113
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
53114
+ <val-pill
53115
+ [props]="{
53116
+ label: tt('systemBadge'),
53117
+ color: 'primary',
53118
+ weight: 'bold',
53119
+ }"
53120
+ />
53121
+ </div>
53122
+ @if (role.description) {
53123
+ <span class="role-card__desc">{{ role.description }}</span>
53124
+ }
53125
+ @if (!role.hasPerms) {
53126
+ <div class="perm-chips">
53127
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53128
+ </div>
53129
+ } @else {
53130
+ @for (g of role.groups; track g.key) {
53131
+ <div class="perm-group">
53132
+ <span class="perm-group__label">{{ g.label }}</span>
53133
+ <div class="perm-chips">
53134
+ @for (p of g.perms; track p) {
53135
+ <span class="perm-chip">{{ p }}</span>
53136
+ }
53137
+ </div>
53138
+ </div>
53139
+ }
53140
+ }
53141
+ </div>
53142
+ }
53143
+ </div>
53144
+ </section>
53145
+ }
53146
+
53147
+ @if (resolvedConfig().showCustomRoles) {
53148
+ <section class="perm-section">
53149
+ <div class="perm-section__header">
53150
+ <val-title
53151
+ [props]="{
53152
+ size: 'medium',
53153
+ color: 'dark',
53154
+ bold: true,
53155
+ content: tt('customRolesTitle'),
53156
+ }"
53157
+ />
53158
+ <val-text
53159
+ [props]="{
53160
+ content: tt('customRolesHint'),
53161
+ size: 'medium',
53162
+ color: 'medium',
53163
+ bold: false,
53164
+ }"
53165
+ />
53166
+ </div>
53167
+ @if (customRoles().length === 0) {
53168
+ <val-text
53169
+ [props]="{
53170
+ content: tt('noCustomRoles'),
53171
+ size: 'medium',
53172
+ color: 'medium',
53173
+ bold: false,
53174
+ }"
53175
+ />
53176
+ } @else {
53177
+ <div class="role-list">
53178
+ @for (role of customRoles(); track role.id) {
53179
+ <div class="role-card">
53180
+ <div class="role-card__header">
53181
+ <span class="role-card__name">{{ role.name }}</span>
53182
+ </div>
53183
+ @if (role.description) {
53184
+ <span class="role-card__desc">{{ role.description }}</span>
53185
+ }
53186
+ @if (!role.hasPerms) {
53187
+ <div class="perm-chips">
53188
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53189
+ </div>
53190
+ } @else {
53191
+ @for (g of role.groups; track g.key) {
53192
+ <div class="perm-group">
53193
+ <span class="perm-group__label">{{ g.label }}</span>
53194
+ <div class="perm-chips">
53195
+ @for (p of g.perms; track p) {
53196
+ <span class="perm-chip">{{ p }}</span>
53197
+ }
53198
+ </div>
53199
+ </div>
53200
+ }
53201
+ }
53202
+ </div>
53203
+ }
53204
+ </div>
53205
+ }
53206
+ </section>
53207
+ }
53208
+ }
53209
+ </div>
53210
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
53211
+ }
53212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
53213
+ type: Component,
53214
+ args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
53215
+ <div class="page">
53216
+ <header class="page-header">
53217
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
53218
+ <val-title
53219
+ [props]="{
53220
+ size: 'large',
53221
+ color: 'dark',
53222
+ bold: false,
53223
+ content: tt('pageDescription'),
53224
+ }"
53225
+ />
53226
+ </header>
53227
+
53228
+ @if (loading()) {
53229
+ <div class="spinner-row">
53230
+ <ion-spinner name="crescent" />
53231
+ </div>
53232
+ } @else if (loadError()) {
53233
+ <div class="error-row">
53234
+ <val-text
53235
+ [props]="{
53236
+ content: tt('errorTitle'),
53237
+ size: 'medium',
53238
+ color: 'medium',
53239
+ bold: false,
53240
+ }"
53241
+ />
53242
+ <val-button [props]="retryButtonProps()" (click)="load()" />
53243
+ </div>
53244
+ } @else {
53245
+ @if (resolvedConfig().showSystemRoles) {
53246
+ <section class="perm-section">
53247
+ <div class="perm-section__header">
53248
+ <val-title
53249
+ [props]="{
53250
+ size: 'medium',
53251
+ color: 'dark',
53252
+ bold: true,
53253
+ content: tt('systemRolesTitle'),
53254
+ }"
53255
+ />
53256
+ <val-text
53257
+ [props]="{
53258
+ content: tt('systemRolesHint'),
53259
+ size: 'medium',
53260
+ color: 'medium',
53261
+ bold: false,
53262
+ }"
53263
+ />
53264
+ </div>
53265
+ <div class="role-list">
53266
+ @for (role of systemRoles(); track role.id) {
53267
+ <div class="role-card">
53268
+ <div class="role-card__header">
53269
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
53270
+ <val-pill
53271
+ [props]="{
53272
+ label: tt('systemBadge'),
53273
+ color: 'primary',
53274
+ weight: 'bold',
53275
+ }"
53276
+ />
53277
+ </div>
53278
+ @if (role.description) {
53279
+ <span class="role-card__desc">{{ role.description }}</span>
53280
+ }
53281
+ @if (!role.hasPerms) {
53282
+ <div class="perm-chips">
53283
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53284
+ </div>
53285
+ } @else {
53286
+ @for (g of role.groups; track g.key) {
53287
+ <div class="perm-group">
53288
+ <span class="perm-group__label">{{ g.label }}</span>
53289
+ <div class="perm-chips">
53290
+ @for (p of g.perms; track p) {
53291
+ <span class="perm-chip">{{ p }}</span>
53292
+ }
53293
+ </div>
53294
+ </div>
53295
+ }
53296
+ }
53297
+ </div>
53298
+ }
53299
+ </div>
53300
+ </section>
53301
+ }
53302
+
53303
+ @if (resolvedConfig().showCustomRoles) {
53304
+ <section class="perm-section">
53305
+ <div class="perm-section__header">
53306
+ <val-title
53307
+ [props]="{
53308
+ size: 'medium',
53309
+ color: 'dark',
53310
+ bold: true,
53311
+ content: tt('customRolesTitle'),
53312
+ }"
53313
+ />
53314
+ <val-text
53315
+ [props]="{
53316
+ content: tt('customRolesHint'),
53317
+ size: 'medium',
53318
+ color: 'medium',
53319
+ bold: false,
53320
+ }"
53321
+ />
53322
+ </div>
53323
+ @if (customRoles().length === 0) {
53324
+ <val-text
53325
+ [props]="{
53326
+ content: tt('noCustomRoles'),
53327
+ size: 'medium',
53328
+ color: 'medium',
53329
+ bold: false,
53330
+ }"
53331
+ />
53332
+ } @else {
53333
+ <div class="role-list">
53334
+ @for (role of customRoles(); track role.id) {
53335
+ <div class="role-card">
53336
+ <div class="role-card__header">
53337
+ <span class="role-card__name">{{ role.name }}</span>
53338
+ </div>
53339
+ @if (role.description) {
53340
+ <span class="role-card__desc">{{ role.description }}</span>
53341
+ }
53342
+ @if (!role.hasPerms) {
53343
+ <div class="perm-chips">
53344
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53345
+ </div>
53346
+ } @else {
53347
+ @for (g of role.groups; track g.key) {
53348
+ <div class="perm-group">
53349
+ <span class="perm-group__label">{{ g.label }}</span>
53350
+ <div class="perm-chips">
53351
+ @for (p of g.perms; track p) {
53352
+ <span class="perm-chip">{{ p }}</span>
53353
+ }
53354
+ </div>
53355
+ </div>
53356
+ }
53357
+ }
53358
+ </div>
53359
+ }
53360
+ </div>
53361
+ }
53362
+ </section>
53363
+ }
53364
+ }
53365
+ </div>
53366
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
53367
+ }], ctorParameters: () => [], propDecorators: { config: [{
53368
+ type: Input
53369
+ }] } });
53370
+
53371
+ /**
53372
+ * Helper para montar la vista Permisos (`val-permissions-view`) como ruta en una
53373
+ * app del factory. El `config` se pasa por route `data` (`permissionsConfig`) y el
53374
+ * componente lo lee como fallback de su `@Input() config`.
53375
+ *
53376
+ * @example
53377
+ * ```ts
53378
+ * // settings.routes.ts
53379
+ * export const settingsRoutes: Routes = [
53380
+ * ...provideValtechPermissionsRoutes(), // → /app/settings/permissions
53381
+ * { path: 'preferences', loadComponent: () => ... },
53382
+ * ];
53383
+ *
53384
+ * // con config acotada:
53385
+ * ...provideValtechPermissionsRoutes({ config: { showCustomRoles: false } }),
53386
+ * ...provideValtechPermissionsRoutes({ path: 'roles' }),
53387
+ * ```
53388
+ */
53389
+ function provideValtechPermissionsRoutes(opts) {
53390
+ return [
53391
+ {
53392
+ path: opts?.path ?? 'permissions',
53393
+ component: PermissionsViewComponent,
53394
+ data: { permissionsConfig: opts?.config },
53395
+ },
53396
+ ];
53397
+ }
53398
+
51978
53399
  /** Built-in label sets for the three platform locales. */
51979
53400
  const CALLOUT_LABELS = {
51980
53401
  es: {
@@ -62181,5 +63602,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
62181
63602
  * Generated bundle index. Do not edit.
62182
63603
  */
62183
63604
 
62184
- 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, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyService, ApiKeysModalComponent, 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, CreateOrgModalComponent, 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, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, 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, InviteMemberModalComponent, 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, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, 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, PreferencesViewComponent, 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, SETTINGS_SECTIONS_CATALOG, 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, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, 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, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, 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, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, 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, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
63605
+ 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, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyService, ApiKeysModalComponent, 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, CreateOrgModalComponent, 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, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, 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, InviteMemberModalComponent, 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, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, 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, PermissionCatalogService, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, 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, SETTINGS_SECTIONS_CATALOG, 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, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, 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, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, 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, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, 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, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
62185
63606
  //# sourceMappingURL=valtech-components.mjs.map