valtech-components 2.0.903 → 2.0.905

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, Injectable, makeEnvironmentProviders, APP_INITIALIZER, inject, EventEmitter, Component, Input, Output, input, computed, ChangeDetectionStrategy, HostBinding, PLATFORM_ID, Inject, HostListener, Pipe, effect, ViewChild, ChangeDetectorRef, ContentChild, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, output, Injector, TemplateRef, ViewContainerRef, isSignal, Directive, ElementRef, ViewEncapsulation } from '@angular/core';
2
+ import { signal, Injectable, makeEnvironmentProviders, APP_INITIALIZER, inject, EventEmitter, Component, Input, Output, input, computed, ChangeDetectionStrategy, HostBinding, PLATFORM_ID, Inject, HostListener, Pipe, effect, ViewChild, ChangeDetectorRef, ContentChild, ErrorHandler, DestroyRef, InjectionToken, isDevMode, Optional, runInInjectionContext, output, Injector, TemplateRef, ViewContainerRef, isSignal, Directive, ElementRef, ViewEncapsulation } from '@angular/core';
3
3
  import * as i2$1 from '@ionic/angular/standalone';
4
4
  import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonRippleEffect, AlertController, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
5
5
  import * as i1 from '@angular/common';
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
53
53
  * Current version of valtech-components.
54
54
  * This is automatically updated during the publish process.
55
55
  */
56
- const VERSION = '2.0.903';
56
+ const VERSION = '2.0.905';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -21912,7 +21912,10 @@ class AuthStorageService {
21912
21912
  constructor(config, platformId) {
21913
21913
  this.config = config;
21914
21914
  this.isBrowser = isPlatformBrowser(platformId);
21915
- const prefix = this.config.storagePrefix || 'valtech_auth_';
21915
+ if (!this.config && isDevMode()) {
21916
+ console.warn('[valtech-components] AuthStorageService: provideValtechAuth() no está en main.ts.');
21917
+ }
21918
+ const prefix = this.config?.storagePrefix || 'valtech_auth_';
21916
21919
  this.keys = {
21917
21920
  ACCESS_TOKEN: `${prefix}access_token`,
21918
21921
  REFRESH_TOKEN: `${prefix}refresh_token`,
@@ -22049,7 +22052,7 @@ class AuthStorageService {
22049
22052
  if (!this.isBrowser)
22050
22053
  return;
22051
22054
  try {
22052
- Object.values(this.keys).forEach((key) => localStorage.removeItem(key));
22055
+ Object.values(this.keys).forEach(key => localStorage.removeItem(key));
22053
22056
  }
22054
22057
  catch (e) {
22055
22058
  console.warn('[ValtechAuth] Error limpiando storage:', e);
@@ -22092,7 +22095,10 @@ class AuthSyncService {
22092
22095
  /** Observable de eventos de sincronización */
22093
22096
  this.onEvent$ = this.eventSubject.asObservable();
22094
22097
  this.isBrowser = isPlatformBrowser(platformId);
22095
- const prefix = this.config.storagePrefix || 'valtech_auth_';
22098
+ if (!this.config && isDevMode()) {
22099
+ console.warn('[valtech-components] AuthSyncService: provideValtechAuth() no está en main.ts.');
22100
+ }
22101
+ const prefix = this.config?.storagePrefix || 'valtech_auth_';
22096
22102
  this.channelName = `${prefix}sync_channel`;
22097
22103
  }
22098
22104
  /**
@@ -22100,7 +22106,7 @@ class AuthSyncService {
22100
22106
  * SSR-noop — sync de pestañas solo aplica en browser.
22101
22107
  */
22102
22108
  start() {
22103
- if (!this.config.enableTabSync || !this.isBrowser) {
22109
+ if (!this.config?.enableTabSync || !this.isBrowser) {
22104
22110
  return;
22105
22111
  }
22106
22112
  // Intentar usar BroadcastChannel API (mejor rendimiento)
@@ -22129,7 +22135,7 @@ class AuthSyncService {
22129
22135
  * Envía un evento a otras pestañas.
22130
22136
  */
22131
22137
  broadcast(event) {
22132
- if (!this.config.enableTabSync || !this.isBrowser) {
22138
+ if (!this.config?.enableTabSync || !this.isBrowser) {
22133
22139
  return;
22134
22140
  }
22135
22141
  const fullEvent = {
@@ -22173,7 +22179,7 @@ class AuthSyncService {
22173
22179
  * Inicializa fallback con storage events.
22174
22180
  */
22175
22181
  initStorageEvents() {
22176
- const storageKey = `${this.config.storagePrefix}sync_event`;
22182
+ const storageKey = `${this.config?.storagePrefix}sync_event`;
22177
22183
  this.storageListener = (event) => {
22178
22184
  if (event.key === storageKey && event.newValue) {
22179
22185
  try {
@@ -22191,7 +22197,7 @@ class AuthSyncService {
22191
22197
  * Envía evento via localStorage (fallback).
22192
22198
  */
22193
22199
  broadcastViaStorage(event) {
22194
- const storageKey = `${this.config.storagePrefix}sync_event`;
22200
+ const storageKey = `${this.config?.storagePrefix}sync_event`;
22195
22201
  try {
22196
22202
  // Escribir y luego limpiar para permitir múltiples eventos del mismo tipo
22197
22203
  localStorage.setItem(storageKey, JSON.stringify(event));
@@ -25273,7 +25279,7 @@ class OAuthService {
25273
25279
  return new Observable(observer => {
25274
25280
  // Construir URL de inicio
25275
25281
  const redirectUri = `${window.location.origin}/auth/oauth/callback`;
25276
- const startUrl = `${this.config.apiUrl}/v2/auth/oauth/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
25282
+ const startUrl = `${this.config?.apiUrl}/v2/auth/oauth/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
25277
25283
  // Abrir popup centrado
25278
25284
  const width = 500;
25279
25285
  const height = 600;
@@ -25378,7 +25384,7 @@ class OAuthService {
25378
25384
  startLinkFlow(provider) {
25379
25385
  return new Observable(observer => {
25380
25386
  const redirectUri = `${window.location.origin}/auth/oauth/callback`;
25381
- const startUrl = `${this.config.apiUrl}/v2/auth/oauth/link/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
25387
+ const startUrl = `${this.config?.apiUrl}/v2/auth/oauth/link/${provider}/start?redirect_uri=${encodeURIComponent(redirectUri)}`;
25382
25388
  const width = 500;
25383
25389
  const height = 600;
25384
25390
  const left = window.screenX + (window.outerWidth - width) / 2;
@@ -25429,7 +25435,7 @@ class OAuthService {
25429
25435
  */
25430
25436
  getLinkedProviders() {
25431
25437
  return this.http
25432
- .get(`${this.config.apiUrl}/v2/auth/oauth/providers`)
25438
+ .get(`${this.config?.apiUrl}/v2/auth/oauth/providers`)
25433
25439
  .pipe(catchError(error => throwError(() => ({
25434
25440
  code: error.error?.code || 'FETCH_ERROR',
25435
25441
  message: error.error?.message || 'Error al obtener proveedores vinculados',
@@ -25440,7 +25446,7 @@ class OAuthService {
25440
25446
  */
25441
25447
  unlinkProvider(provider) {
25442
25448
  return this.http
25443
- .post(`${this.config.apiUrl}/v2/auth/oauth/unlink`, { provider })
25449
+ .post(`${this.config?.apiUrl}/v2/auth/oauth/unlink`, { provider })
25444
25450
  .pipe(catchError(error => throwError(() => ({
25445
25451
  code: error.error?.code || 'UNLINK_ERROR',
25446
25452
  message: error.error?.message || 'Error al desvincular proveedor',
@@ -25451,7 +25457,7 @@ class OAuthService {
25451
25457
  */
25452
25458
  setPassword(password) {
25453
25459
  return this.http
25454
- .post(`${this.config.apiUrl}/v2/auth/oauth/set-password`, { password })
25460
+ .post(`${this.config?.apiUrl}/v2/auth/oauth/set-password`, { password })
25455
25461
  .pipe(catchError(error => throwError(() => ({
25456
25462
  code: error.error?.code || 'SET_PASSWORD_ERROR',
25457
25463
  message: error.error?.message || 'Error al establecer contraseña',
@@ -25462,7 +25468,7 @@ class OAuthService {
25462
25468
  */
25463
25469
  hasPassword() {
25464
25470
  return this.http
25465
- .get(`${this.config.apiUrl}/v2/auth/oauth/has-password`)
25471
+ .get(`${this.config?.apiUrl}/v2/auth/oauth/has-password`)
25466
25472
  .pipe(catchError(error => throwError(() => ({
25467
25473
  code: error.error?.code || 'CHECK_PASSWORD_ERROR',
25468
25474
  message: error.error?.message || 'Error al verificar contraseña',
@@ -25800,6 +25806,10 @@ class AuthService {
25800
25806
  this.mfaPending = this.stateService.mfaPending;
25801
25807
  /** Error actual */
25802
25808
  this.error = this.stateService.error;
25809
+ if (!this.config && isDevMode()) {
25810
+ console.warn("[valtech-components] AuthService: provideValtechAuth() no está en main.ts. " +
25811
+ "Login, registro y sesión no funcionarán.");
25812
+ }
25803
25813
  }
25804
25814
  // =============================================
25805
25815
  // INICIALIZACIÓN
@@ -25873,7 +25883,7 @@ class AuthService {
25873
25883
  }
25874
25884
  }
25875
25885
  // 5. Iniciar sincronización entre pestañas
25876
- if (this.config.enableTabSync) {
25886
+ if (this.config?.enableTabSync) {
25877
25887
  this.syncService.start();
25878
25888
  this.syncSubscription = this.syncService.onEvent$.subscribe(event => this.handleSyncEvent(event));
25879
25889
  }
@@ -26106,7 +26116,7 @@ class AuthService {
26106
26116
  this.stateService.updateRefreshToken(response.refreshToken); // NUEVO: guardar refresh rotado
26107
26117
  this.storageService.saveAccessToken(response.accessToken, expiresAt);
26108
26118
  this.storageService.saveRefreshToken(response.refreshToken); // NUEVO: persistir refresh rotado
26109
- if (this.config.enableFirebaseIntegration && response.firebaseToken) {
26119
+ if (this.config?.enableFirebaseIntegration && response.firebaseToken) {
26110
26120
  this.signInWithFirebase(response.firebaseToken);
26111
26121
  }
26112
26122
  this.startRefreshTimer();
@@ -26192,7 +26202,7 @@ class AuthService {
26192
26202
  this.signOutFirebase();
26193
26203
  this.clearState();
26194
26204
  this.syncService.broadcast({ type: 'LOGOUT' });
26195
- this.router.navigate([this.config.loginRoute]);
26205
+ this.router.navigate([this.config?.loginRoute]);
26196
26206
  }
26197
26207
  // =============================================
26198
26208
  // MFA SETUP (usuario autenticado)
@@ -26407,7 +26417,7 @@ class AuthService {
26407
26417
  this.signOutFirebase();
26408
26418
  this.clearState();
26409
26419
  this.syncService.broadcast({ type: 'LOGOUT' });
26410
- this.router.navigate([this.config.loginRoute]);
26420
+ this.router.navigate([this.config?.loginRoute]);
26411
26421
  }
26412
26422
  }), catchError(error => this.handleAuthError(error)));
26413
26423
  }
@@ -26484,11 +26494,11 @@ class AuthService {
26484
26494
  // PRIVATE METHODS
26485
26495
  // =============================================
26486
26496
  get baseUrl() {
26487
- return `${this.config.apiUrl}${this.config.authPrefix}`;
26497
+ return `${this.config?.apiUrl}${this.config?.authPrefix}`;
26488
26498
  }
26489
26499
  /** Base for /v2/users endpoints (separate from /v2/auth). */
26490
26500
  get usersBaseUrl() {
26491
- return `${this.config.apiUrl}/v2/users`;
26501
+ return `${this.config?.apiUrl}/v2/users`;
26492
26502
  }
26493
26503
  handleSuccessfulAuth(response) {
26494
26504
  const expiresAt = Date.now() + response.expiresIn * 1000;
@@ -26521,18 +26531,18 @@ class AuthService {
26521
26531
  this.syncService.broadcast({ type: 'LOGIN' });
26522
26532
  // Integración con Firebase
26523
26533
  console.log('[ValtechAuth] handleSuccessfulAuth - Firebase check:', {
26524
- enableFirebaseIntegration: this.config.enableFirebaseIntegration,
26534
+ enableFirebaseIntegration: this.config?.enableFirebaseIntegration,
26525
26535
  hasFirebaseTokenKey: 'firebaseToken' in response,
26526
26536
  firebaseTokenValue: !!response.firebaseToken,
26527
26537
  firebaseTokenLength: response.firebaseToken?.length || 0,
26528
26538
  });
26529
- if (this.config.enableFirebaseIntegration &&
26539
+ if (this.config?.enableFirebaseIntegration &&
26530
26540
  'firebaseToken' in response &&
26531
26541
  response.firebaseToken) {
26532
26542
  console.log('[ValtechAuth] Calling signInWithFirebase with token length:', response.firebaseToken.length);
26533
26543
  this.signInWithFirebase(response.firebaseToken);
26534
26544
  }
26535
- else if (this.config.enableFirebaseIntegration) {
26545
+ else if (this.config?.enableFirebaseIntegration) {
26536
26546
  // El login NO trajo firebaseToken (ej. flujo OAuth que lo perdió). En vez
26537
26547
  // de quedar sin sesión de Firebase, recurrimos al fallback self-healing:
26538
26548
  // /refresh sí devuelve un firebaseToken confiable.
@@ -26543,7 +26553,7 @@ class AuthService {
26543
26553
  console.log('[ValtechAuth] Firebase signin skipped - integración Firebase desactivada');
26544
26554
  }
26545
26555
  // Registro automático de dispositivo para push notifications
26546
- if (this.config.enableDeviceRegistration) {
26556
+ if (this.config?.enableDeviceRegistration) {
26547
26557
  this.registerDeviceIfNeeded(); // fire-and-forget
26548
26558
  }
26549
26559
  // Sincronizar idioma del usuario con i18n
@@ -26595,7 +26605,7 @@ class AuthService {
26595
26605
  const state = this.stateService.state();
26596
26606
  if (!state.expiresAt)
26597
26607
  return;
26598
- const refreshBeforeMs = (this.config.refreshBeforeExpiry || 60) * 1000;
26608
+ const refreshBeforeMs = (this.config?.refreshBeforeExpiry || 60) * 1000;
26599
26609
  const refreshAt = state.expiresAt - refreshBeforeMs;
26600
26610
  const delay = refreshAt - Date.now();
26601
26611
  if (delay > 0) {
@@ -26640,7 +26650,7 @@ class AuthService {
26640
26650
  case 'LOGOUT':
26641
26651
  this.stateService.reset();
26642
26652
  this.stopRefreshTimer();
26643
- this.router.navigate([this.config.loginRoute]);
26653
+ this.router.navigate([this.config?.loginRoute]);
26644
26654
  break;
26645
26655
  case 'PERMISSIONS_UPDATE': {
26646
26656
  const perms = this.storageService.loadPermissions();
@@ -26686,7 +26696,7 @@ class AuthService {
26686
26696
  }
26687
26697
  }
26688
26698
  async signOutFirebase() {
26689
- if (!this.config.enableFirebaseIntegration)
26699
+ if (!this.config?.enableFirebaseIntegration)
26690
26700
  return;
26691
26701
  try {
26692
26702
  if (this.firebaseService) {
@@ -26717,7 +26727,7 @@ class AuthService {
26717
26727
  * Fire-and-forget: se invoca con `void` para no demorar `initialize()`.
26718
26728
  */
26719
26729
  async ensureFirebaseSessionOnBootstrap() {
26720
- if (!this.config.enableFirebaseIntegration || !this.firebaseService)
26730
+ if (!this.config?.enableFirebaseIntegration || !this.firebaseService)
26721
26731
  return;
26722
26732
  // Dar a la persistencia IndexedDB propia de Firebase Auth una oportunidad
26723
26733
  // de auto-restaurarse (navegador normal). En una PWA standalone de iOS
@@ -26743,7 +26753,7 @@ class AuthService {
26743
26753
  * desde el debug-console qué disparó el fallback.
26744
26754
  */
26745
26755
  async reestablishFirebaseViaRefresh(reason) {
26746
- if (!this.config.enableFirebaseIntegration || !this.firebaseService)
26756
+ if (!this.config?.enableFirebaseIntegration || !this.firebaseService)
26747
26757
  return;
26748
26758
  console.log(`[FBAuth] fallback — re-estableciendo Firebase vía /refresh (motivo: ${reason})`);
26749
26759
  try {
@@ -26816,7 +26826,7 @@ class AuthService {
26816
26826
  if (token) {
26817
26827
  // Eliminar del backend
26818
26828
  await firstValueFrom(this.http
26819
- .request('DELETE', `${this.config.apiUrl}/v2/users/me/devices/by-token`, {
26829
+ .request('DELETE', `${this.config?.apiUrl}/v2/users/me/devices/by-token`, {
26820
26830
  body: { token },
26821
26831
  })
26822
26832
  .pipe(catchError(() => of(null))));
@@ -26886,7 +26896,7 @@ class AuthService {
26886
26896
  }
26887
26897
  // Registrar en backend
26888
26898
  const { platform, browser, os } = this.detectPlatformInfo();
26889
- const response = await firstValueFrom(this.http.post(`${this.config.apiUrl}/v2/users/me/devices`, {
26899
+ const response = await firstValueFrom(this.http.post(`${this.config?.apiUrl}/v2/users/me/devices`, {
26890
26900
  token,
26891
26901
  platform,
26892
26902
  browser,
@@ -26909,7 +26919,7 @@ class AuthService {
26909
26919
  */
26910
26920
  async registerDeviceIfNeeded() {
26911
26921
  // Solo si está habilitado y messaging está disponible
26912
- if (!this.config.enableDeviceRegistration || !this.messagingService) {
26922
+ if (!this.config?.enableDeviceRegistration || !this.messagingService) {
26913
26923
  return false;
26914
26924
  }
26915
26925
  try {
@@ -26929,7 +26939,7 @@ class AuthService {
26929
26939
  }
26930
26940
  // Registrar en backend
26931
26941
  const { platform, browser, os } = this.detectPlatformInfo();
26932
- await firstValueFrom(this.http.post(`${this.config.apiUrl}/v2/users/me/devices`, {
26942
+ await firstValueFrom(this.http.post(`${this.config?.apiUrl}/v2/users/me/devices`, {
26933
26943
  token,
26934
26944
  platform,
26935
26945
  browser,
@@ -26948,7 +26958,7 @@ class AuthService {
26948
26958
  * Elimina el dispositivo del backend y borra el token FCM.
26949
26959
  */
26950
26960
  async unregisterDevice() {
26951
- if (!this.config.enableDeviceRegistration || !this.messagingService) {
26961
+ if (!this.config?.enableDeviceRegistration || !this.messagingService) {
26952
26962
  return;
26953
26963
  }
26954
26964
  try {
@@ -26956,7 +26966,7 @@ class AuthService {
26956
26966
  if (token) {
26957
26967
  // Delete from backend (fire and forget)
26958
26968
  this.http
26959
- .request('DELETE', `${this.config.apiUrl}/v2/users/me/devices/by-token`, {
26969
+ .request('DELETE', `${this.config?.apiUrl}/v2/users/me/devices/by-token`, {
26960
26970
  body: { token },
26961
26971
  })
26962
26972
  .pipe(catchError(() => of(null)))
@@ -28123,17 +28133,22 @@ function provideValtechFeedback(config) {
28123
28133
  */
28124
28134
  class FeedbackService {
28125
28135
  constructor() {
28126
- this.config = inject(VALTECH_FEEDBACK_CONFIG);
28136
+ this.config = inject(VALTECH_FEEDBACK_CONFIG, { optional: true });
28127
28137
  this.http = inject(HttpClient);
28128
28138
  this.firestore = inject(FirestoreService, { optional: true });
28129
28139
  this.storage = inject(StorageService, { optional: true });
28130
28140
  this.auth = inject(AuthService, { optional: true });
28141
+ this.isConfigured = signal(!!this.config);
28142
+ if (!this.config && isDevMode()) {
28143
+ console.warn('[valtech-components] FeedbackService: provideValtechFeedback() no está en main.ts. ' +
28144
+ 'Las reacciones y el feedback no funcionarán.');
28145
+ }
28131
28146
  }
28132
28147
  /**
28133
28148
  * URL base para endpoints de feedback.
28134
28149
  */
28135
28150
  get baseUrl() {
28136
- return `${this.config.apiUrl}${this.config.feedbackPrefix}`;
28151
+ return `${this.config?.apiUrl}${this.config?.feedbackPrefix}`;
28137
28152
  }
28138
28153
  /**
28139
28154
  * Captura el contexto del dispositivo automáticamente.
@@ -28167,7 +28182,7 @@ class FeedbackService {
28167
28182
  attachments,
28168
28183
  contentRef,
28169
28184
  deviceContext: this.captureDeviceContext(),
28170
- appId: this.config.appId,
28185
+ appId: this.config?.appId,
28171
28186
  };
28172
28187
  return this.http.post(this.baseUrl, request);
28173
28188
  }
@@ -28197,15 +28212,15 @@ class FeedbackService {
28197
28212
  */
28198
28213
  validateFile(file) {
28199
28214
  // Verificar tamaño
28200
- if (file.size > this.config.maxFileSize) {
28201
- const maxSizeMB = Math.round(this.config.maxFileSize / (1024 * 1024));
28215
+ if (file.size > this.config?.maxFileSize) {
28216
+ const maxSizeMB = Math.round(this.config?.maxFileSize / (1024 * 1024));
28202
28217
  return {
28203
28218
  valid: false,
28204
28219
  error: `El archivo excede el tamaño máximo de ${maxSizeMB}MB`,
28205
28220
  };
28206
28221
  }
28207
28222
  // Verificar tipo
28208
- const allowedTypes = this.config.allowedFileTypes || [];
28223
+ const allowedTypes = this.config?.allowedFileTypes || [];
28209
28224
  const isAllowed = allowedTypes.some(pattern => {
28210
28225
  if (pattern.endsWith('/*')) {
28211
28226
  const baseType = pattern.replace('/*', '');
@@ -28367,7 +28382,7 @@ class FeedbackService {
28367
28382
  }
28368
28383
  // 2. Fallback: llamar API (solo si hay usuario autenticado)
28369
28384
  const params = new URLSearchParams({
28370
- appId: this.config.appId,
28385
+ appId: this.config?.appId,
28371
28386
  entityType,
28372
28387
  entityId,
28373
28388
  });
@@ -28397,7 +28412,7 @@ class FeedbackService {
28397
28412
  reactionValue: value,
28398
28413
  description: comment || '',
28399
28414
  deviceContext: this.captureDeviceContext(),
28400
- appId: this.config.appId,
28415
+ appId: this.config?.appId,
28401
28416
  };
28402
28417
  return firstValueFrom(this.http.post(this.baseUrl, request));
28403
28418
  }
@@ -28426,7 +28441,7 @@ class FeedbackService {
28426
28441
  reactionValue: value,
28427
28442
  description: comment || '',
28428
28443
  deviceContext: this.captureDeviceContext(),
28429
- appId: this.config.appId,
28444
+ appId: this.config?.appId,
28430
28445
  };
28431
28446
  return firstValueFrom(this.http.post(`${this.baseUrl}/anonymous`, request));
28432
28447
  }
@@ -28474,7 +28489,7 @@ class FeedbackService {
28474
28489
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackService, decorators: [{
28475
28490
  type: Injectable,
28476
28491
  args: [{ providedIn: 'root' }]
28477
- }] });
28492
+ }], ctorParameters: () => [] });
28478
28493
 
28479
28494
  /**
28480
28495
  * Configuración por defecto de tipos de feedback.
@@ -30199,7 +30214,7 @@ class DeviceService {
30199
30214
  this.http = http;
30200
30215
  }
30201
30216
  get baseUrl() {
30202
- return `${this.config.apiUrl}/v2/users/me/devices`;
30217
+ return `${this.config?.apiUrl}/v2/users/me/devices`;
30203
30218
  }
30204
30219
  /**
30205
30220
  * Lista todos los dispositivos registrados del usuario.
@@ -30254,7 +30269,7 @@ class DeviceService {
30254
30269
  * ```
30255
30270
  */
30256
30271
  validateAction(token) {
30257
- return this.http.post(`${this.config.apiUrl}/v2/actions/validate`, { token });
30272
+ return this.http.post(`${this.config?.apiUrl}/v2/actions/validate`, { token });
30258
30273
  }
30259
30274
  /**
30260
30275
  * Ejecuta una acción de dispositivo desde un token de email.
@@ -30277,7 +30292,7 @@ class DeviceService {
30277
30292
  */
30278
30293
  executeAction(token) {
30279
30294
  // Usa el endpoint unificado de acciones
30280
- return this.http.post(`${this.config.apiUrl}/v2/actions/execute`, { token }).pipe(map$1(response => ({
30295
+ return this.http.post(`${this.config?.apiUrl}/v2/actions/execute`, { token }).pipe(map$1(response => ({
30281
30296
  operationId: response.operationId,
30282
30297
  success: response.success,
30283
30298
  message: response.message,
@@ -30336,7 +30351,7 @@ class SessionService {
30336
30351
  this.http = http;
30337
30352
  }
30338
30353
  get baseUrl() {
30339
- return `${this.config.apiUrl}/v2/users/me/sessions`;
30354
+ return `${this.config?.apiUrl}/v2/users/me/sessions`;
30340
30355
  }
30341
30356
  /**
30342
30357
  * Lista todas las sesiones activas del usuario.
@@ -30659,7 +30674,7 @@ class HandoffService {
30659
30674
  const tokenParam = options.tokenParam ?? HANDOFF_TOKEN_PARAM;
30660
30675
  const routeParam = options.routeParam ?? HANDOFF_ROUTE_PARAM;
30661
30676
  const defaultRoute = options.defaultRoute ?? '/';
30662
- const errorRoute = options.errorRoute ?? this.config.loginRoute ?? '/login';
30677
+ const errorRoute = options.errorRoute ?? this.config?.loginRoute ?? '/login';
30663
30678
  const params = new URLSearchParams(window.location.search);
30664
30679
  const token = params.get(tokenParam);
30665
30680
  if (!token) {
@@ -30689,7 +30704,7 @@ class HandoffService {
30689
30704
  });
30690
30705
  }
30691
30706
  get baseUrl() {
30692
- return `${this.config.apiUrl}${this.config.authPrefix}`;
30707
+ return `${this.config?.apiUrl}${this.config?.authPrefix}`;
30693
30708
  }
30694
30709
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HandoffService, deps: [{ token: VALTECH_AUTH_CONFIG, optional: true }, { token: i1$8.HttpClient }, { token: AuthService }, { token: i1$1.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
30695
30710
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HandoffService, providedIn: 'root' }); }
@@ -30877,7 +30892,7 @@ class NotificationActionService {
30877
30892
  if (!route) {
30878
30893
  return 'no-action-route';
30879
30894
  }
30880
- const currentApp = this.config.appId;
30895
+ const currentApp = this.config?.appId;
30881
30896
  const targetApp = notif.appId;
30882
30897
  // 2) Cross-app: handoff + full redirect
30883
30898
  //
@@ -30887,7 +30902,7 @@ class NotificationActionService {
30887
30902
  if (targetApp && currentApp && targetApp !== currentApp) {
30888
30903
  try {
30889
30904
  const resp = await firstValueFrom(this.handoff.createHandoff({ targetAppId: targetApp, route }));
30890
- const baseUrl = resp.targetBaseUrl ?? this.config.appUrls?.[targetApp];
30905
+ const baseUrl = resp.targetBaseUrl ?? this.config?.appUrls?.[targetApp];
30891
30906
  if (!baseUrl) {
30892
30907
  console.warn(`[NotificationAction] No baseUrl for app '${targetApp}' — backend did not return targetBaseUrl and no appUrls fallback configured`);
30893
30908
  return 'cross-app-unconfigured';
@@ -46072,6 +46087,7 @@ class ContentReactionComponent {
46072
46087
  this.toast = inject(ToastService);
46073
46088
  this.i18n = inject(I18nService);
46074
46089
  this.auth = inject(AuthService, { optional: true });
46090
+ this.isConfigured = computed(() => this.feedbackService.isConfigured());
46075
46091
  this.props = {};
46076
46092
  this.reactionSubmit = new EventEmitter();
46077
46093
  this.reactionChange = new EventEmitter();
@@ -46249,11 +46265,11 @@ class ContentReactionComponent {
46249
46265
  return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
46250
46266
  }
46251
46267
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46252
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }] }); }
46268
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }] }); }
46253
46269
  }
46254
46270
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
46255
46271
  type: Component,
46256
- args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
46272
+ args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:14px}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
46257
46273
  }], propDecorators: { props: [{
46258
46274
  type: Input
46259
46275
  }], reactionSubmit: [{