valtech-components 2.0.797 → 2.0.799

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.
@@ -52,7 +52,7 @@ import 'prismjs/components/prism-json';
52
52
  * Current version of valtech-components.
53
53
  * This is automatically updated during the publish process.
54
54
  */
55
- const VERSION = '2.0.797';
55
+ const VERSION = '2.0.799';
56
56
 
57
57
  /**
58
58
  * Servicio para gestionar presets de componentes.
@@ -22598,6 +22598,15 @@ class MessagingService {
22598
22598
  });
22599
22599
  /** Key para localStorage de mensajes FCM (debugging) */
22600
22600
  this.DEBUG_STORAGE_KEY = 'fcm_debug_messages';
22601
+ /**
22602
+ * Key de localStorage donde se persiste el token FCM.
22603
+ *
22604
+ * El token vive en un `BehaviorSubject` en memoria y se pierde en cada
22605
+ * recarga / suspensión de la PWA (iOS reinicia PWAs con frecuencia). Persistir
22606
+ * el token permite hidratar el estado en cold start y que la UI no parpadee.
22607
+ * Es un *optimistic hint* — la verdad la confirma el siguiente `getToken()`.
22608
+ */
22609
+ this.TOKEN_STORAGE_KEY = 'valtech_fcm_token';
22601
22610
  this.debugPersistence = this.config?.debugMessagePersistence ?? false;
22602
22611
  this.initializeMessaging();
22603
22612
  }
@@ -22637,6 +22646,10 @@ class MessagingService {
22637
22646
  if (!isPlatformBrowser(this.platformId)) {
22638
22647
  return;
22639
22648
  }
22649
+ // Hidratar el token desde localStorage ANTES del check de soporte: el token
22650
+ // cacheado es un hint optimista para la UI tras recargas / suspensiones de
22651
+ // la PWA. La validez real se confirma en el próximo getToken().
22652
+ this.hydrateTokenFromStorage();
22640
22653
  const supported = await this.checkSupport();
22641
22654
  const permission = this.getPermissionState();
22642
22655
  this.stateSubject.next({
@@ -22770,10 +22783,11 @@ class MessagingService {
22770
22783
  return null;
22771
22784
  }
22772
22785
  try {
22773
- // Registrar SW personalizado antes de obtener token
22774
- // Esto es necesario cuando usamos un SW custom (firebase-messaging-sw.js)
22775
- const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');
22776
- console.log('[Messaging] SW registered, waiting ready...');
22786
+ // Reutilizar el SW ya registrado (en config.ts durante el bootstrap) en
22787
+ // lugar de re-registrarlo en cada llamada re-registrar dispara
22788
+ // revalidaciones del SW en iOS PWA. Solo registramos si no existe aún.
22789
+ const registration = await this.resolveServiceWorkerRegistration();
22790
+ console.log('[Messaging] SW resolved, waiting ready...');
22777
22791
  // Esperar a que el SW esté activo
22778
22792
  await navigator.serviceWorker.ready;
22779
22793
  console.log('[Messaging] SW ready, calling Firebase getToken()...');
@@ -22786,13 +22800,68 @@ class MessagingService {
22786
22800
  ...this.stateSubject.value,
22787
22801
  token,
22788
22802
  });
22803
+ // Persistir el token para hidratar el estado en futuros cold starts.
22804
+ this.persistToken(token);
22789
22805
  return token;
22790
22806
  }
22791
22807
  catch (error) {
22792
- console.error('[Messaging] getToken error:', error);
22808
+ // No tragamos la causa: logueamos el detalle para diagnóstico. La firma
22809
+ // sigue siendo Promise<string | null> para no romper callers.
22810
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
22811
+ console.error('[Messaging] getToken error:', reason, error);
22793
22812
  return null;
22794
22813
  }
22795
22814
  }
22815
+ /**
22816
+ * Resuelve el `ServiceWorkerRegistration` del SW de FCM.
22817
+ *
22818
+ * El SW `/firebase-messaging-sw.js` ya se registra una vez en el bootstrap de
22819
+ * la app (`config.ts`). Reutilizamos ese registro en lugar de re-registrarlo
22820
+ * en cada `getToken()` — re-registrar repetidamente dispara revalidaciones del
22821
+ * SW en iOS PWA. Solo registramos como fallback si todavía no existe.
22822
+ */
22823
+ async resolveServiceWorkerRegistration() {
22824
+ const existing = await navigator.serviceWorker.getRegistration('/firebase-messaging-sw.js');
22825
+ if (existing) {
22826
+ return existing;
22827
+ }
22828
+ console.warn('[Messaging] SW not registered yet, registering as fallback');
22829
+ return navigator.serviceWorker.register('/firebase-messaging-sw.js');
22830
+ }
22831
+ /**
22832
+ * Persiste el token FCM en localStorage (o lo limpia si es null/empty).
22833
+ */
22834
+ persistToken(token) {
22835
+ if (!isPlatformBrowser(this.platformId))
22836
+ return;
22837
+ try {
22838
+ if (token) {
22839
+ localStorage.setItem(this.TOKEN_STORAGE_KEY, token);
22840
+ }
22841
+ else {
22842
+ localStorage.removeItem(this.TOKEN_STORAGE_KEY);
22843
+ }
22844
+ }
22845
+ catch (e) {
22846
+ console.warn('[Messaging] token persistence failed:', e);
22847
+ }
22848
+ }
22849
+ /**
22850
+ * Hidrata el estado del token desde localStorage en el arranque del servicio.
22851
+ */
22852
+ hydrateTokenFromStorage() {
22853
+ if (!isPlatformBrowser(this.platformId))
22854
+ return;
22855
+ try {
22856
+ const cached = localStorage.getItem(this.TOKEN_STORAGE_KEY);
22857
+ if (cached) {
22858
+ this.setTokenFromStorage(cached);
22859
+ }
22860
+ }
22861
+ catch (e) {
22862
+ console.warn('[Messaging] token hydration failed:', e);
22863
+ }
22864
+ }
22796
22865
  /**
22797
22866
  * Elimina el token FCM actual (unsubscribe de notificaciones).
22798
22867
  *
@@ -22813,6 +22882,8 @@ class MessagingService {
22813
22882
  ...this.stateSubject.value,
22814
22883
  token: null,
22815
22884
  });
22885
+ // Limpiar el token persistido — ya no estamos suscritos.
22886
+ this.persistToken(null);
22816
22887
  // Limpiar listener de mensajes
22817
22888
  if (this.unsubscribeOnMessage) {
22818
22889
  this.unsubscribeOnMessage();
@@ -34857,6 +34928,435 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
34857
34928
  type: Output
34858
34929
  }] } });
34859
34930
 
34931
+ /** Defaults de `rows` por preset cuando el consumer no lo especifica. */
34932
+ const SKELETON_LAYOUT_DEFAULT_ROWS = {
34933
+ form: 3,
34934
+ list: 5,
34935
+ article: 3,
34936
+ cards: 4,
34937
+ detail: 4,
34938
+ hero: 3,
34939
+ };
34940
+
34941
+ /**
34942
+ * `val-skeleton-layout`
34943
+ *
34944
+ * Skeleton de página **preset-driven**. En vez de que cada vista componga
34945
+ * átomos `val-skeleton` a mano, declara una shape y este organism arma el
34946
+ * placeholder completo. Estándar único para loading states en todo el factory.
34947
+ *
34948
+ * Presets: `form` · `list` · `article` · `cards` · `detail` · `hero`.
34949
+ *
34950
+ * Extender = agregar un valor a `SkeletonLayoutPreset` + un `@case` acá.
34951
+ * Los consumers no cambian.
34952
+ *
34953
+ * @example
34954
+ * ```html
34955
+ * @if (loading()) {
34956
+ * <val-skeleton-layout [props]="{ preset: 'form', rows: 3, showAvatar: true }" />
34957
+ * } @else {
34958
+ * ...contenido real...
34959
+ * }
34960
+ * ```
34961
+ */
34962
+ class SkeletonLayoutComponent {
34963
+ constructor() {
34964
+ this.props_ = signal({ preset: 'list' });
34965
+ /** Props con defaults aplicados. */
34966
+ this.resolved = computed(() => this.props_());
34967
+ /** Shimmer on/off — default on. */
34968
+ this.anim = computed(() => this.resolved().animated !== false);
34969
+ /** Array `[0..rows)` para los `@for`. rows del prop o default del preset. */
34970
+ this.rowsArray = computed(() => {
34971
+ const p = this.resolved();
34972
+ const preset = p.preset;
34973
+ const count = p.rows && p.rows > 0 ? Math.floor(p.rows) : SKELETON_LAYOUT_DEFAULT_ROWS[preset];
34974
+ return Array.from({ length: count }, (_, i) => i);
34975
+ });
34976
+ /** `grid-template-columns` para el preset cards. */
34977
+ this.gridColumns = computed(() => {
34978
+ const cols = this.resolved().columns ?? 2;
34979
+ return `repeat(${Math.max(1, cols)}, 1fr)`;
34980
+ });
34981
+ }
34982
+ set props(value) {
34983
+ if (value)
34984
+ this.props_.set(value);
34985
+ }
34986
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SkeletonLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
34987
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SkeletonLayoutComponent, isStandalone: true, selector: "val-skeleton-layout", inputs: { props: "props" }, ngImport: i0, template: `
34988
+ <div class="val-skeleton-layout" [class]="resolved().cssClass || ''" aria-busy="true" aria-live="polite">
34989
+ @switch (resolved().preset) {
34990
+ <!-- FORM — avatar opcional + N fields (label+input) + botón -->
34991
+ @case ('form') {
34992
+ @if (resolved().showAvatar) {
34993
+ <div class="sk-form__avatar">
34994
+ <val-skeleton
34995
+ [props]="{
34996
+ type: 'avatar',
34997
+ width: '96px',
34998
+ height: '96px',
34999
+ circle: true,
35000
+ animated: anim(),
35001
+ }"
35002
+ />
35003
+ <div class="sk-form__avatar-meta">
35004
+ <val-skeleton
35005
+ [props]="{
35006
+ type: 'text',
35007
+ width: '150px',
35008
+ height: '18px',
35009
+ animated: anim(),
35010
+ }"
35011
+ />
35012
+ <val-skeleton
35013
+ [props]="{
35014
+ type: 'text',
35015
+ width: '190px',
35016
+ height: '13px',
35017
+ animated: anim(),
35018
+ }"
35019
+ />
35020
+ <val-skeleton
35021
+ [props]="{
35022
+ type: 'text',
35023
+ width: '110px',
35024
+ height: '13px',
35025
+ animated: anim(),
35026
+ }"
35027
+ />
35028
+ </div>
35029
+ </div>
35030
+ }
35031
+ <div class="sk-form__fields">
35032
+ @for (i of rowsArray(); track i) {
35033
+ <div class="sk-form__field">
35034
+ <val-skeleton
35035
+ [props]="{
35036
+ type: 'text',
35037
+ width: '90px',
35038
+ height: '13px',
35039
+ animated: anim(),
35040
+ }"
35041
+ />
35042
+ <val-skeleton
35043
+ [props]="{
35044
+ type: 'custom',
35045
+ width: '100%',
35046
+ height: '48px',
35047
+ borderRadius: '12px',
35048
+ animated: anim(),
35049
+ }"
35050
+ />
35051
+ </div>
35052
+ }
35053
+ @if (resolved().showButton !== false) {
35054
+ <val-skeleton
35055
+ [props]="{
35056
+ type: 'custom',
35057
+ width: '100%',
35058
+ height: '48px',
35059
+ borderRadius: '24px',
35060
+ animated: anim(),
35061
+ }"
35062
+ />
35063
+ }
35064
+ </div>
35065
+ }
35066
+
35067
+ <!-- LIST — N filas list-item (el átomo ya trae icono + 2 líneas) -->
35068
+ @case ('list') {
35069
+ <div class="sk-list">
35070
+ @for (i of rowsArray(); track i) {
35071
+ <val-skeleton [props]="{ type: 'list-item', animated: anim() }" />
35072
+ }
35073
+ </div>
35074
+ }
35075
+
35076
+ <!-- ARTICLE — título + meta + párrafos -->
35077
+ @case ('article') {
35078
+ <div class="sk-article">
35079
+ @if (resolved().showTitle !== false) {
35080
+ <val-skeleton
35081
+ [props]="{
35082
+ type: 'text',
35083
+ width: '70%',
35084
+ height: '30px',
35085
+ animated: anim(),
35086
+ }"
35087
+ />
35088
+ <val-skeleton
35089
+ [props]="{
35090
+ type: 'text',
35091
+ width: '40%',
35092
+ height: '14px',
35093
+ animated: anim(),
35094
+ }"
35095
+ />
35096
+ }
35097
+ @for (i of rowsArray(); track i) {
35098
+ <val-skeleton [props]="{ type: 'paragraph', lines: 4, animated: anim() }" />
35099
+ }
35100
+ </div>
35101
+ }
35102
+
35103
+ <!-- CARDS — grid de N card blocks -->
35104
+ @case ('cards') {
35105
+ <div class="sk-cards" [style.grid-template-columns]="gridColumns()">
35106
+ @for (i of rowsArray(); track i) {
35107
+ <val-skeleton [props]="{ type: 'card', animated: anim() }" />
35108
+ }
35109
+ </div>
35110
+ }
35111
+
35112
+ <!-- DETAIL — N filas key-value -->
35113
+ @case ('detail') {
35114
+ <div class="sk-detail">
35115
+ @for (i of rowsArray(); track i) {
35116
+ <div class="sk-detail__row">
35117
+ <val-skeleton
35118
+ [props]="{
35119
+ type: 'text',
35120
+ width: '110px',
35121
+ height: '14px',
35122
+ animated: anim(),
35123
+ }"
35124
+ />
35125
+ <val-skeleton
35126
+ [props]="{
35127
+ type: 'text',
35128
+ width: '160px',
35129
+ height: '14px',
35130
+ animated: anim(),
35131
+ }"
35132
+ />
35133
+ </div>
35134
+ }
35135
+ </div>
35136
+ }
35137
+
35138
+ <!-- HERO — banner grande + filas de contenido -->
35139
+ @case ('hero') {
35140
+ <div class="sk-hero">
35141
+ @if (resolved().showTitle !== false) {
35142
+ <val-skeleton
35143
+ [props]="{
35144
+ type: 'custom',
35145
+ width: '100%',
35146
+ height: '140px',
35147
+ borderRadius: '20px',
35148
+ animated: anim(),
35149
+ }"
35150
+ />
35151
+ }
35152
+ <div class="sk-hero__rows">
35153
+ @for (i of rowsArray(); track i) {
35154
+ <val-skeleton
35155
+ [props]="{
35156
+ type: 'custom',
35157
+ width: '100%',
35158
+ height: '72px',
35159
+ borderRadius: '14px',
35160
+ animated: anim(),
35161
+ }"
35162
+ />
35163
+ }
35164
+ </div>
35165
+ </div>
35166
+ }
35167
+ }
35168
+ </div>
35169
+ `, isInline: true, styles: [":host{display:block;width:100%}.val-skeleton-layout{width:100%}.sk-form__avatar{display:flex;align-items:center;gap:16px;padding:16px 0}.sk-form__avatar-meta{display:flex;flex-direction:column;gap:8px}.sk-form__fields{padding:16px 0;display:flex;flex-direction:column;gap:18px}.sk-form__field{display:flex;flex-direction:column;gap:8px}.sk-list{display:flex;flex-direction:column;gap:10px;padding:8px 0}.sk-article{display:flex;flex-direction:column;gap:16px;padding:8px 0}.sk-cards{display:grid;gap:12px;padding:8px 0}.sk-detail{display:flex;flex-direction:column;gap:14px;padding:8px 0}.sk-detail__row{display:flex;align-items:center;justify-content:space-between;gap:16px}.sk-hero{display:flex;flex-direction:column;gap:16px;padding:8px 0}.sk-hero__rows{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "component", type: SkeletonComponent, selector: "val-skeleton", inputs: ["props"] }] }); }
35170
+ }
35171
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SkeletonLayoutComponent, decorators: [{
35172
+ type: Component,
35173
+ args: [{ selector: 'val-skeleton-layout', standalone: true, imports: [SkeletonComponent], template: `
35174
+ <div class="val-skeleton-layout" [class]="resolved().cssClass || ''" aria-busy="true" aria-live="polite">
35175
+ @switch (resolved().preset) {
35176
+ <!-- FORM — avatar opcional + N fields (label+input) + botón -->
35177
+ @case ('form') {
35178
+ @if (resolved().showAvatar) {
35179
+ <div class="sk-form__avatar">
35180
+ <val-skeleton
35181
+ [props]="{
35182
+ type: 'avatar',
35183
+ width: '96px',
35184
+ height: '96px',
35185
+ circle: true,
35186
+ animated: anim(),
35187
+ }"
35188
+ />
35189
+ <div class="sk-form__avatar-meta">
35190
+ <val-skeleton
35191
+ [props]="{
35192
+ type: 'text',
35193
+ width: '150px',
35194
+ height: '18px',
35195
+ animated: anim(),
35196
+ }"
35197
+ />
35198
+ <val-skeleton
35199
+ [props]="{
35200
+ type: 'text',
35201
+ width: '190px',
35202
+ height: '13px',
35203
+ animated: anim(),
35204
+ }"
35205
+ />
35206
+ <val-skeleton
35207
+ [props]="{
35208
+ type: 'text',
35209
+ width: '110px',
35210
+ height: '13px',
35211
+ animated: anim(),
35212
+ }"
35213
+ />
35214
+ </div>
35215
+ </div>
35216
+ }
35217
+ <div class="sk-form__fields">
35218
+ @for (i of rowsArray(); track i) {
35219
+ <div class="sk-form__field">
35220
+ <val-skeleton
35221
+ [props]="{
35222
+ type: 'text',
35223
+ width: '90px',
35224
+ height: '13px',
35225
+ animated: anim(),
35226
+ }"
35227
+ />
35228
+ <val-skeleton
35229
+ [props]="{
35230
+ type: 'custom',
35231
+ width: '100%',
35232
+ height: '48px',
35233
+ borderRadius: '12px',
35234
+ animated: anim(),
35235
+ }"
35236
+ />
35237
+ </div>
35238
+ }
35239
+ @if (resolved().showButton !== false) {
35240
+ <val-skeleton
35241
+ [props]="{
35242
+ type: 'custom',
35243
+ width: '100%',
35244
+ height: '48px',
35245
+ borderRadius: '24px',
35246
+ animated: anim(),
35247
+ }"
35248
+ />
35249
+ }
35250
+ </div>
35251
+ }
35252
+
35253
+ <!-- LIST — N filas list-item (el átomo ya trae icono + 2 líneas) -->
35254
+ @case ('list') {
35255
+ <div class="sk-list">
35256
+ @for (i of rowsArray(); track i) {
35257
+ <val-skeleton [props]="{ type: 'list-item', animated: anim() }" />
35258
+ }
35259
+ </div>
35260
+ }
35261
+
35262
+ <!-- ARTICLE — título + meta + párrafos -->
35263
+ @case ('article') {
35264
+ <div class="sk-article">
35265
+ @if (resolved().showTitle !== false) {
35266
+ <val-skeleton
35267
+ [props]="{
35268
+ type: 'text',
35269
+ width: '70%',
35270
+ height: '30px',
35271
+ animated: anim(),
35272
+ }"
35273
+ />
35274
+ <val-skeleton
35275
+ [props]="{
35276
+ type: 'text',
35277
+ width: '40%',
35278
+ height: '14px',
35279
+ animated: anim(),
35280
+ }"
35281
+ />
35282
+ }
35283
+ @for (i of rowsArray(); track i) {
35284
+ <val-skeleton [props]="{ type: 'paragraph', lines: 4, animated: anim() }" />
35285
+ }
35286
+ </div>
35287
+ }
35288
+
35289
+ <!-- CARDS — grid de N card blocks -->
35290
+ @case ('cards') {
35291
+ <div class="sk-cards" [style.grid-template-columns]="gridColumns()">
35292
+ @for (i of rowsArray(); track i) {
35293
+ <val-skeleton [props]="{ type: 'card', animated: anim() }" />
35294
+ }
35295
+ </div>
35296
+ }
35297
+
35298
+ <!-- DETAIL — N filas key-value -->
35299
+ @case ('detail') {
35300
+ <div class="sk-detail">
35301
+ @for (i of rowsArray(); track i) {
35302
+ <div class="sk-detail__row">
35303
+ <val-skeleton
35304
+ [props]="{
35305
+ type: 'text',
35306
+ width: '110px',
35307
+ height: '14px',
35308
+ animated: anim(),
35309
+ }"
35310
+ />
35311
+ <val-skeleton
35312
+ [props]="{
35313
+ type: 'text',
35314
+ width: '160px',
35315
+ height: '14px',
35316
+ animated: anim(),
35317
+ }"
35318
+ />
35319
+ </div>
35320
+ }
35321
+ </div>
35322
+ }
35323
+
35324
+ <!-- HERO — banner grande + filas de contenido -->
35325
+ @case ('hero') {
35326
+ <div class="sk-hero">
35327
+ @if (resolved().showTitle !== false) {
35328
+ <val-skeleton
35329
+ [props]="{
35330
+ type: 'custom',
35331
+ width: '100%',
35332
+ height: '140px',
35333
+ borderRadius: '20px',
35334
+ animated: anim(),
35335
+ }"
35336
+ />
35337
+ }
35338
+ <div class="sk-hero__rows">
35339
+ @for (i of rowsArray(); track i) {
35340
+ <val-skeleton
35341
+ [props]="{
35342
+ type: 'custom',
35343
+ width: '100%',
35344
+ height: '72px',
35345
+ borderRadius: '14px',
35346
+ animated: anim(),
35347
+ }"
35348
+ />
35349
+ }
35350
+ </div>
35351
+ </div>
35352
+ }
35353
+ }
35354
+ </div>
35355
+ `, styles: [":host{display:block;width:100%}.val-skeleton-layout{width:100%}.sk-form__avatar{display:flex;align-items:center;gap:16px;padding:16px 0}.sk-form__avatar-meta{display:flex;flex-direction:column;gap:8px}.sk-form__fields{padding:16px 0;display:flex;flex-direction:column;gap:18px}.sk-form__field{display:flex;flex-direction:column;gap:8px}.sk-list{display:flex;flex-direction:column;gap:10px;padding:8px 0}.sk-article{display:flex;flex-direction:column;gap:16px;padding:8px 0}.sk-cards{display:grid;gap:12px;padding:8px 0}.sk-detail{display:flex;flex-direction:column;gap:14px;padding:8px 0}.sk-detail__row{display:flex;align-items:center;justify-content:space-between;gap:16px}.sk-hero{display:flex;flex-direction:column;gap:16px;padding:8px 0}.sk-hero__rows{display:flex;flex-direction:column;gap:12px}\n"] }]
35356
+ }], propDecorators: { props: [{
35357
+ type: Input
35358
+ }] } });
35359
+
34860
35360
  class SimpleComponent {
34861
35361
  constructor() {
34862
35362
  this.onClick = new EventEmitter();
@@ -36854,6 +37354,16 @@ class PreferencesService {
36854
37354
  this.language = this._language.asReadonly();
36855
37355
  this.notificationsMaster = this._notificationsMaster.asReadonly();
36856
37356
  this.synced = this._synced.asReadonly();
37357
+ /**
37358
+ * `true` mientras un `update()` está en vuelo (PUT al backend).
37359
+ *
37360
+ * El listener Firestore (`bindToUser`) es real-time y puede emitir un snapshot
37361
+ * *stale* — anterior al sync del backend — que pisaría el valor optimista de
37362
+ * `_notificationsMaster` y revertiría el toggle. Mientras este flag esté `true`
37363
+ * el listener ignora las emisiones; el `update()` ya alinea las señales con la
37364
+ * respuesta del backend al terminar.
37365
+ */
37366
+ this._updateInFlight = false;
36857
37367
  const destroyRef = inject(DestroyRef);
36858
37368
  destroyRef.onDestroy(() => this.unbind());
36859
37369
  // Auto-bind al user actual. effect() corre en injection context (constructor OK).
@@ -36906,6 +37416,9 @@ class PreferencesService {
36906
37416
  if (partial.notifications && typeof partial.notifications.master === 'boolean') {
36907
37417
  this._notificationsMaster.set(partial.notifications.master);
36908
37418
  }
37419
+ // Bloquear el listener Firestore mientras el PUT está en vuelo: un snapshot
37420
+ // stale no debe pisar el valor optimista recién aplicado.
37421
+ this._updateInFlight = true;
36909
37422
  try {
36910
37423
  const res = await firstValueFrom(this.http.put(url, partial));
36911
37424
  // El listener Firestore es la fuente final, pero alinear ya con la
@@ -36927,6 +37440,9 @@ class PreferencesService {
36927
37440
  this._notificationsMaster.set(prev.master);
36928
37441
  throw err;
36929
37442
  }
37443
+ finally {
37444
+ this._updateInFlight = false;
37445
+ }
36930
37446
  }
36931
37447
  setTheme(theme) {
36932
37448
  return this.update({ theme });
@@ -36953,6 +37469,12 @@ class PreferencesService {
36953
37469
  // pisan el theme/lang locales con valores arbitrarios.
36954
37470
  return;
36955
37471
  }
37472
+ // Ignorar emisiones del listener mientras hay un update() en vuelo: un
37473
+ // snapshot stale (anterior al sync del backend) revertiría el valor
37474
+ // optimista. El update() ya alinea las señales con la respuesta real.
37475
+ if (this._updateInFlight) {
37476
+ return;
37477
+ }
36956
37478
  if (doc.theme)
36957
37479
  this._theme.set(doc.theme);
36958
37480
  if (doc.language)
@@ -44145,5 +44667,5 @@ function buildFooterLinks(links, t, resolver) {
44145
44667
  * Generated bundle index. Do not edit.
44146
44668
  */
44147
44669
 
44148
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
44670
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
44149
44671
  //# sourceMappingURL=valtech-components.mjs.map