valtech-components 4.0.56 → 4.0.57

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.56';
59
+ const VERSION = '4.0.57';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -7341,6 +7341,20 @@ const VALTECH_DEFAULT_CONTENT = {
7341
7341
  mfaTOTP: 'Ingresa el código de tu app de autenticación',
7342
7342
  mfaEmail: 'Ingresa el código enviado a tu correo',
7343
7343
  mfaSMS: 'Ingresa el código enviado a tu teléfono',
7344
+ mfaBackupCode: 'Ingresa uno de tus códigos de respaldo',
7345
+ mfaUseBackupLink: '¿Perdiste tu dispositivo?',
7346
+ mfaUseBackupAction: 'Usa un código de respaldo',
7347
+ mfaUseTotpAction: 'Volver al código de la app',
7348
+ backupCodeMinLength: 'El código de respaldo tiene 8 caracteres',
7349
+ mfaLostBackupLink: '¿Sin acceso a tu segundo factor?',
7350
+ mfaResetAction: 'Restablecer MFA por correo',
7351
+ mfaResetTitle: 'Restablecer MFA',
7352
+ mfaResetRequestDescription: 'Te enviaremos un código a tu correo para deshabilitar la verificación en dos pasos.',
7353
+ mfaResetConfirmDescription: 'Ingresa el código que enviamos a tu correo.',
7354
+ mfaResetSubmit: 'Enviar código',
7355
+ mfaResetConfirmSubmit: 'Deshabilitar MFA',
7356
+ mfaResetCodeSent: 'Si la cuenta existe, enviamos un código a tu correo.',
7357
+ mfaResetDone: 'MFA deshabilitado. Inicia sesión nuevamente.',
7344
7358
  // Forgot/Reset password
7345
7359
  forgotTitle: 'Recuperar contraseña',
7346
7360
  forgotDescription: 'Ingresa tu correo electrónico y te enviaremos un código.',
@@ -7526,6 +7540,20 @@ const VALTECH_DEFAULT_CONTENT = {
7526
7540
  mfaTOTP: 'Enter the code from your authenticator app',
7527
7541
  mfaEmail: 'Enter the code sent to your email',
7528
7542
  mfaSMS: 'Enter the code sent to your phone',
7543
+ mfaBackupCode: 'Enter one of your backup codes',
7544
+ mfaUseBackupLink: 'Lost your device?',
7545
+ mfaUseBackupAction: 'Use a backup code',
7546
+ mfaUseTotpAction: 'Back to authenticator code',
7547
+ backupCodeMinLength: 'The backup code is 8 characters',
7548
+ mfaLostBackupLink: 'No access to your second factor?',
7549
+ mfaResetAction: 'Reset MFA by email',
7550
+ mfaResetTitle: 'Reset MFA',
7551
+ mfaResetRequestDescription: 'We will email you a code to disable two-step verification.',
7552
+ mfaResetConfirmDescription: 'Enter the code we sent to your email.',
7553
+ mfaResetSubmit: 'Send code',
7554
+ mfaResetConfirmSubmit: 'Disable MFA',
7555
+ mfaResetCodeSent: 'If the account exists, we sent a code to your email.',
7556
+ mfaResetDone: 'MFA disabled. Please sign in again.',
7529
7557
  // Forgot/Reset password
7530
7558
  forgotTitle: 'Recover password',
7531
7559
  forgotDescription: 'Enter your email and we will send you a code.',
@@ -8762,6 +8790,25 @@ class AuthService {
8762
8790
  .post(`${this.baseUrl}/reset-password`, request)
8763
8791
  .pipe(catchError(error => this.handleAuthError(error)));
8764
8792
  }
8793
+ /**
8794
+ * Inicia el reset de MFA por email cuando el user perdió su 2º factor
8795
+ * (TOTP + backup codes). Envía un código de 6 dígitos al email verificado.
8796
+ * Response siempre genérica (anti-enumeración).
8797
+ */
8798
+ requestMFAReset(request) {
8799
+ return this.http
8800
+ .post(`${this.baseUrl}/mfa/reset-request`, request)
8801
+ .pipe(catchError(error => this.handleAuthError(error)));
8802
+ }
8803
+ /**
8804
+ * Confirma el reset de MFA con el código recibido por email → deshabilita MFA.
8805
+ * Tras esto el user inicia sesión normalmente (sin 2º factor) y re-enrola.
8806
+ */
8807
+ confirmMFAReset(request) {
8808
+ return this.http
8809
+ .post(`${this.baseUrl}/mfa/reset`, request)
8810
+ .pipe(catchError(error => this.handleAuthError(error)));
8811
+ }
8765
8812
  /**
8766
8813
  * Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
8767
8814
  * la contraseña con el token del enlace de email. En éxito el backend devuelve
@@ -40691,6 +40738,16 @@ class LoginComponent {
40691
40738
  this._verifyFormSectionName = signal('');
40692
40739
  this._mfaVerifyFormState = signal(ComponentStates.ENABLED);
40693
40740
  this._mfaMethod = signal('TOTP');
40741
+ /** Método MFA pendiente — expuesto al template para gatear el link de backup (solo TOTP). */
40742
+ this.mfaMethod = this._mfaMethod.asReadonly();
40743
+ /** A: el user togglea a "código de respaldo" (8 chars alfanuméricos) cuando perdió el 2º factor. */
40744
+ this.mfaUseBackupCode = signal(false);
40745
+ // B: reset de MFA por email (cuando el user no tiene backup codes).
40746
+ this.isMFAResetModalOpen = false;
40747
+ this._mfaResetStep = signal('request');
40748
+ this.mfaResetStep = this._mfaResetStep.asReadonly();
40749
+ this._mfaResetFormState = signal(ComponentStates.ENABLED);
40750
+ this._mfaResetEmail = signal('');
40694
40751
  this._forgotPasswordFormState = signal(ComponentStates.ENABLED);
40695
40752
  this._resetPasswordFormState = signal(ComponentStates.ENABLED);
40696
40753
  this._resetFormSectionName = signal('');
@@ -40857,7 +40914,51 @@ class LoginComponent {
40857
40914
  // ==========================================
40858
40915
  this.mfaVerifyFormProps = computed(() => {
40859
40916
  const method = this._mfaMethod();
40860
- const sectionName = method === 'TOTP' ? this.t('mfaTOTP') : method === 'EMAIL' ? this.t('mfaEmail') : this.t('mfaSMS');
40917
+ const useBackup = this.mfaUseBackupCode();
40918
+ const sectionName = useBackup
40919
+ ? this.t('mfaBackupCode')
40920
+ : method === 'TOTP'
40921
+ ? this.t('mfaTOTP')
40922
+ : method === 'EMAIL'
40923
+ ? this.t('mfaEmail')
40924
+ : this.t('mfaSMS');
40925
+ // A: backup code = 8 chars alfanuméricos → input de texto (el PIN de 6
40926
+ // celdas numéricas no puede capturarlo). El backend rutea por longitud.
40927
+ const codeField = useBackup
40928
+ ? {
40929
+ type: InputType.TEXT,
40930
+ label: '',
40931
+ name: 'code',
40932
+ token: 'mfa-backup',
40933
+ hint: '',
40934
+ placeholder: '',
40935
+ errorKeys: {
40936
+ required: 'codeRequired',
40937
+ minlength: 'backupCodeMinLength',
40938
+ },
40939
+ validators: [Validators.required, Validators.minLength(8)],
40940
+ order: 0,
40941
+ state: ComponentStates.ENABLED,
40942
+ autoFocus: true,
40943
+ }
40944
+ : {
40945
+ type: InputType.PIN_CODE,
40946
+ label: '',
40947
+ name: 'code',
40948
+ token: 'mfa-pin',
40949
+ hint: '',
40950
+ placeholder: '',
40951
+ errorKeys: {
40952
+ required: 'codeRequired',
40953
+ minlength: 'codeMinLength',
40954
+ },
40955
+ validators: [Validators.required, Validators.minLength(6)],
40956
+ order: 0,
40957
+ state: ComponentStates.ENABLED,
40958
+ length: 6,
40959
+ allowNumbersOnly: true,
40960
+ autoFocus: true,
40961
+ };
40861
40962
  return this.i18nHelper.resolveForm({
40862
40963
  nameKey: 'mfaTitle',
40863
40964
  i18nNamespace: '_auth',
@@ -40865,26 +40966,7 @@ class LoginComponent {
40865
40966
  {
40866
40967
  name: sectionName,
40867
40968
  order: 0,
40868
- fields: [
40869
- {
40870
- type: InputType.PIN_CODE,
40871
- label: '',
40872
- name: 'code',
40873
- token: 'mfa-pin',
40874
- hint: '',
40875
- placeholder: '',
40876
- errorKeys: {
40877
- required: 'codeRequired',
40878
- minlength: 'codeMinLength',
40879
- },
40880
- validators: [Validators.required, Validators.minLength(6)],
40881
- order: 0,
40882
- state: ComponentStates.ENABLED,
40883
- length: 6,
40884
- allowNumbersOnly: true,
40885
- autoFocus: true,
40886
- },
40887
- ],
40969
+ fields: [codeField],
40888
40970
  },
40889
40971
  ],
40890
40972
  actions: {
@@ -40896,6 +40978,79 @@ class LoginComponent {
40896
40978
  });
40897
40979
  });
40898
40980
  // ==========================================
40981
+ // MFA RESET FORM (computed) — B: recovery por email
40982
+ // ==========================================
40983
+ this.mfaResetRequestFormProps = computed(() => this.i18nHelper.resolveForm({
40984
+ nameKey: 'mfaResetTitle',
40985
+ i18nNamespace: '_auth',
40986
+ sections: [
40987
+ {
40988
+ name: this.t('mfaResetRequestDescription'),
40989
+ order: 0,
40990
+ fields: [
40991
+ {
40992
+ type: InputType.EMAIL,
40993
+ label: '',
40994
+ name: 'email',
40995
+ token: 'mfa-reset-email',
40996
+ hint: '',
40997
+ placeholderKey: 'emailPlaceholder',
40998
+ value: this._mfaResetEmail(),
40999
+ errorKeys: {
41000
+ required: 'emailRequired',
41001
+ email: 'emailInvalid',
41002
+ },
41003
+ validators: [Validators.required, Validators.email],
41004
+ order: 0,
41005
+ state: ComponentStates.ENABLED,
41006
+ },
41007
+ ],
41008
+ },
41009
+ ],
41010
+ actions: {
41011
+ ...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
41012
+ token: 'mfa-reset-request-submit',
41013
+ textKey: 'mfaResetSubmit',
41014
+ },
41015
+ state: this._mfaResetFormState(),
41016
+ }));
41017
+ this.mfaResetConfirmFormProps = computed(() => this.i18nHelper.resolveForm({
41018
+ nameKey: 'mfaResetTitle',
41019
+ i18nNamespace: '_auth',
41020
+ sections: [
41021
+ {
41022
+ name: this.t('mfaResetConfirmDescription'),
41023
+ order: 0,
41024
+ fields: [
41025
+ {
41026
+ type: InputType.PIN_CODE,
41027
+ label: '',
41028
+ name: 'code',
41029
+ token: 'mfa-reset-pin',
41030
+ hint: '',
41031
+ placeholder: '',
41032
+ errorKeys: {
41033
+ required: 'codeRequired',
41034
+ minlength: 'codeMinLength',
41035
+ },
41036
+ validators: [Validators.required, Validators.minLength(6)],
41037
+ order: 0,
41038
+ state: ComponentStates.ENABLED,
41039
+ length: 6,
41040
+ allowNumbersOnly: true,
41041
+ autoFocus: true,
41042
+ },
41043
+ ],
41044
+ },
41045
+ ],
41046
+ actions: {
41047
+ ...button({ text: '', type: 'submit', fill: 'solid', expand: 'block' }),
41048
+ token: 'mfa-reset-confirm-submit',
41049
+ textKey: 'mfaResetConfirmSubmit',
41050
+ },
41051
+ state: this._mfaResetFormState(),
41052
+ }));
41053
+ // ==========================================
40899
41054
  // FORGOT PASSWORD FORM (computed)
40900
41055
  // ==========================================
40901
41056
  this.forgotPasswordFormProps = computed(() => this.i18nHelper.resolveForm({
@@ -41026,6 +41181,7 @@ class LoginComponent {
41026
41181
  next: () => {
41027
41182
  this._loginFormState.set(ComponentStates.ENABLED);
41028
41183
  if (this.authService.mfaPending().required) {
41184
+ this._mfaResetEmail.set(email); // B: prefill del reset por email
41029
41185
  this.openMFAVerifyModal();
41030
41186
  return;
41031
41187
  }
@@ -41154,6 +41310,7 @@ class LoginComponent {
41154
41310
  openMFAVerifyModal() {
41155
41311
  const method = this.authService.mfaPending().method;
41156
41312
  this._mfaMethod.set(method);
41313
+ this.mfaUseBackupCode.set(false);
41157
41314
  this._mfaVerifyFormState.set(ComponentStates.ENABLED);
41158
41315
  this.isMFAVerifyModalOpen = true;
41159
41316
  this.onMFARequired.emit({ method });
@@ -41161,6 +41318,60 @@ class LoginComponent {
41161
41318
  closeMFAVerifyModal() {
41162
41319
  this.isMFAVerifyModalOpen = false;
41163
41320
  }
41321
+ /** A: alterna entre el código TOTP (6 díg) y un código de respaldo (8 chars). */
41322
+ toggleMFABackupCode() {
41323
+ this.mfaUseBackupCode.update(v => !v);
41324
+ }
41325
+ // ==========================================
41326
+ // MFA RESET HANDLERS — B: recovery por email
41327
+ // ==========================================
41328
+ openMFAResetModal() {
41329
+ this._mfaResetStep.set('request');
41330
+ this._mfaResetFormState.set(ComponentStates.ENABLED);
41331
+ this.isMFAResetModalOpen = true;
41332
+ }
41333
+ closeMFAResetModal() {
41334
+ this.isMFAResetModalOpen = false;
41335
+ this._mfaResetStep.set('request');
41336
+ }
41337
+ mfaResetRequestHandler(event) {
41338
+ const email = event.fields['email'];
41339
+ if (!email) {
41340
+ this.showToast(this.t('enterEmail'));
41341
+ return;
41342
+ }
41343
+ this._mfaResetFormState.set(ComponentStates.WORKING);
41344
+ this._mfaResetEmail.set(email);
41345
+ this.authService.requestMFAReset({ email }).subscribe({
41346
+ next: () => {
41347
+ this._mfaResetFormState.set(ComponentStates.ENABLED);
41348
+ this._mfaResetStep.set('confirm');
41349
+ this.showToast(this.t('mfaResetCodeSent'));
41350
+ },
41351
+ error: err => {
41352
+ this._mfaResetFormState.set(ComponentStates.ENABLED);
41353
+ this.handleError(err, 'mfa');
41354
+ },
41355
+ });
41356
+ }
41357
+ mfaResetConfirmHandler(event) {
41358
+ const code = event.fields['code'];
41359
+ this._mfaResetFormState.set(ComponentStates.WORKING);
41360
+ this.authService.confirmMFAReset({ email: this._mfaResetEmail(), code }).subscribe({
41361
+ next: () => {
41362
+ this._mfaResetFormState.set(ComponentStates.ENABLED);
41363
+ // MFA deshabilitado → cerramos ambos modales; el user reinicia el login
41364
+ // (ahora sin 2º factor) y re-enrola TOTP desde ajustes.
41365
+ this.closeMFAResetModal();
41366
+ this.closeMFAVerifyModal();
41367
+ this.showToast(this.t('mfaResetDone'));
41368
+ },
41369
+ error: err => {
41370
+ this._mfaResetFormState.set(ComponentStates.ENABLED);
41371
+ this.handleError(err, 'mfa');
41372
+ },
41373
+ });
41374
+ }
41164
41375
  verifyMFAHandler(event) {
41165
41376
  const code = event.fields['code'];
41166
41377
  this._mfaVerifyFormState.set(ComponentStates.WORKING);
@@ -41377,7 +41588,7 @@ class LoginComponent {
41377
41588
  this.stopResetResendCooldown();
41378
41589
  }
41379
41590
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
41380
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
41591
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginComponent, isStandalone: true, selector: "val-login", inputs: { props: "props" }, outputs: { onSuccess: "onSuccess", onError: "onError", onMFARequired: "onMFARequired" }, ngImport: i0, template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: ImageComponent, selector: "val-image", inputs: ["props"] }] }); }
41381
41592
  }
41382
41593
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, decorators: [{
41383
41594
  type: Component,
@@ -41393,7 +41604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
41393
41604
  IonToolbar,
41394
41605
  FormComponent,
41395
41606
  ImageComponent,
41396
- ], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
41607
+ ], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo \u2014 default de marca (--main-logo) si el consumer no pasa logo -->\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n\n <!-- Login Form -->\n <val-form [props]=\"loginFormProps()\" (onSubmit)=\"loginHandler($event)\" />\n\n <!-- OAuth Section -->\n @if (config.showOAuth && config.oauthProviders.length > 0) {\n <div class=\"oauth-separator\">\n <span>{{ t('orContinueWith') }}</span>\n </div>\n\n <div class=\"oauth-buttons\">\n @for (provider of config.oauthProviders; track provider) {\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n color=\"dark\"\n (click)=\"loginWithOAuth(provider)\"\n [disabled]=\"isOAuthLoading\"\n >\n @switch (provider) { @case ('google') {\n <ion-icon slot=\"start\" name=\"logo-google\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithGoogle') }} } @case ('apple') {\n <ion-icon slot=\"start\" name=\"logo-apple\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithApple') }} } @case ('microsoft') {\n <ion-icon slot=\"start\" name=\"logo-microsoft\" aria-hidden=\"true\"></ion-icon>\n {{ isOAuthLoading ? t('connecting') : t('continueWithMicrosoft') }} } }\n </ion-button>\n }\n </div>\n }\n\n <!-- Register Link -->\n @if (config.showRegister) {\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('noAccount') }}\n <a (click)=\"openRegisterModal()\">{{ t('register') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Forgot Password Link -->\n @if (config.showForgotPassword) {\n <div class=\"auth-link forgot-password\">\n <ion-text color=\"dark\">\n {{ t('forgotLink') }}\n <a (click)=\"openForgotPasswordModal()\">{{ t('recoverPassword') }}</a>\n </ion-text>\n </div>\n }\n\n <!-- Legal Notice -->\n @if (props.legal) {\n <div class=\"legal-notice\">\n <ion-text color=\"dark\">\n <p>\n {{ t('legalPrefix') }} @if (resolvedCompanyLink) {\n <a [href]=\"resolvedCompanyLink\" target=\"_blank\" rel=\"noopener noreferrer\"\n ><strong>{{ props.legal.companyName }}</strong></a\n >\n } @else {\n <strong>{{ props.legal.companyName }}</strong>\n } {{ t('legalSuffix') }} @if (props.legal.termsLink) {\n <a [href]=\"props.legal.termsLink\">{{ t('termsAndConditions') }}</a>\n } @else {\n <span>{{ t('termsAndConditions') }}</span>\n } {{ t('and') }} @if (props.legal.privacyLink) {\n <a [href]=\"props.legal.privacyLink\">{{ t('privacyPolicy') }}</a>\n } @else {\n <span>{{ t('privacyPolicy') }}</span>\n }.\n </p>\n </ion-text>\n </div>\n }\n</div>\n\n<!-- Register Modal -->\n<ion-modal [isOpen]=\"isRegisterModalOpen\" (didDismiss)=\"closeRegisterModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeRegisterModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"registerFormProps()\" (onSubmit)=\"registerHandler($event)\" />\n <div class=\"auth-link\">\n <ion-text color=\"dark\">\n {{ t('hasAccount') }}\n <a (click)=\"closeRegisterModal()\">{{ t('signIn') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Verify Email Modal -->\n<ion-modal [isOpen]=\"isVerifyModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"verifyFormProps()\" (onSubmit)=\"verifyHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Forgot Password Modal -->\n<ion-modal [isOpen]=\"isForgotPasswordModalOpen\" (didDismiss)=\"closeForgotPasswordModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeForgotPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"forgotPasswordFormProps()\" (onSubmit)=\"forgotPasswordHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- Reset Password Modal -->\n<ion-modal [isOpen]=\"isResetPasswordModalOpen\" [backdropDismiss]=\"false\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeResetPasswordModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"resetPasswordFormProps()\" (onSubmit)=\"resetPasswordHandler($event)\" />\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('noCodeReceived') }} @if (resetResendCooldown > 0) {\n <span class=\"cooldown\">{{ t('resendIn', { seconds: resetResendCooldown.toString() }) }}</span>\n } @else {\n <a (click)=\"resendResetCode()\">{{ t('resend') }}</a>\n }\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Verify Modal -->\n<ion-modal [isOpen]=\"isMFAVerifyModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"onMFAVerifyDismissed()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAVerifyModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n <val-form [props]=\"mfaVerifyFormProps()\" (onSubmit)=\"verifyMFAHandler($event)\" />\n @if (mfaMethod() === 'TOTP') {\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n @if (mfaUseBackupCode()) {\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseTotpAction') }}</a>\n } @else { {{ t('mfaUseBackupLink') }}\n <a (click)=\"toggleMFABackupCode()\">{{ t('mfaUseBackupAction') }}</a>\n }\n </ion-text>\n </div>\n }\n <div class=\"resend-link\">\n <ion-text color=\"dark\">\n {{ t('mfaLostBackupLink') }}\n <a (click)=\"openMFAResetModal()\">{{ t('mfaResetAction') }}</a>\n </ion-text>\n </div>\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n\n<!-- MFA Reset Modal (B) \u2014 recovery por email cuando no hay backup codes -->\n<ion-modal [isOpen]=\"isMFAResetModalOpen\" [backdropDismiss]=\"false\" (didDismiss)=\"closeMFAResetModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" [attr.aria-label]=\"t('close')\" (click)=\"closeMFAResetModal()\">\n <ion-icon name=\"close-outline\" aria-hidden=\"true\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <div class=\"logo-container\">\n <val-image [props]=\"resolvedLogo\" />\n </div>\n @if (mfaResetStep() === 'request') {\n <val-form [props]=\"mfaResetRequestFormProps()\" (onSubmit)=\"mfaResetRequestHandler($event)\" />\n } @else {\n <val-form [props]=\"mfaResetConfirmFormProps()\" (onSubmit)=\"mfaResetConfirmHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".val-login{width:100%}.val-login--card{padding:1rem .75rem;background:var(--ion-color-light);border-radius:16px;box-shadow:0 2px 8px #00000014}.logo-container{max-width:130px;margin-bottom:1.5rem}.auth-link{text-align:center;margin-top:1rem;font-size:.9rem}.auth-link a{color:var(--ion-color-primary);text-decoration:none;font-weight:500;cursor:pointer}.auth-link a:hover{text-decoration:underline}.oauth-separator{display:flex;align-items:center;margin:1.5rem 0}.oauth-separator:before,.oauth-separator:after{content:\"\";flex:1;height:1px;background:var(--ion-color-medium-tint)}.oauth-separator span{padding:0 1rem;color:var(--ion-color-dark);font-size:.85rem}.oauth-buttons{margin-bottom:1rem}.oauth-buttons ion-button{--border-radius: 8px;--border-width: 1px}.oauth-buttons ion-icon{font-size:1.2rem;margin-right:.5rem}.legal-notice{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--ion-color-medium-tint)}.legal-notice p{font-size:.75rem;line-height:1.5;text-align:center;margin:0}.legal-notice a{color:var(--ion-color-primary);text-decoration:none}.legal-notice a:hover{text-decoration:underline}.resend-link{text-align:center;margin-top:1rem;font-size:.9rem}.resend-link a{color:var(--ion-color-primary);cursor:pointer;font-weight:500}.resend-link a:hover{text-decoration:underline}.resend-link .cooldown{color:var(--ion-color-medium)}\n"] }]
41397
41608
  }], propDecorators: { props: [{
41398
41609
  type: Input
41399
41610
  }], onSuccess: [{
@@ -52178,7 +52389,7 @@ const ORGANIZATION_VIEW_I18N = {
52178
52389
  inviteSuccess: 'Invitación enviada.',
52179
52390
  inviteError: 'No se pudo enviar la invitación.',
52180
52391
  membersError: 'No se pudieron cargar los miembros.',
52181
- leaveTitle: 'Salir de la organización',
52392
+ leaveTitle: '⚠️ Salir de la organización',
52182
52393
  leaveHint: 'Dejarás de tener acceso a esta organización.',
52183
52394
  leaveCta: 'Salir',
52184
52395
  leaveOwnerHint: 'Eres el propietario. Transfiere la propiedad antes de salir.',
@@ -52194,7 +52405,7 @@ const ORGANIZATION_VIEW_I18N = {
52194
52405
  retry: 'Reintentar',
52195
52406
  saveError: 'No se pudo guardar. Intenta de nuevo.',
52196
52407
  leaveError: 'No se pudo salir. Intenta de nuevo.',
52197
- transferTitle: 'Transferir propiedad',
52408
+ transferTitle: '⚠️ Transferir propiedad',
52198
52409
  transferHint: 'Elige un miembro para transferirle la propiedad de la organización.',
52199
52410
  transferSelect: 'Seleccionar nuevo propietario',
52200
52411
  transferCta: 'Transferir',
@@ -52245,7 +52456,10 @@ const ORGANIZATION_VIEW_I18N = {
52245
52456
  permDelete: 'Eliminar',
52246
52457
  permManage: 'Gestionar',
52247
52458
  membersShowMore: 'Ver más',
52248
- deleteTitle: 'Eliminar organización',
52459
+ orgsNewQuestion: '¿Necesitas otra organización?',
52460
+ orgsNewHint: 'Crea un espacio separado para otro equipo, empresa o proyecto.',
52461
+ orgsNewCta: 'Nueva organización',
52462
+ deleteTitle: '⚠️ Eliminar organización',
52249
52463
  deleteHint: 'Esta acción es permanente e irreversible. Todos los miembros perderán el acceso.',
52250
52464
  deleteCta: 'Eliminar organización',
52251
52465
  deleteConfirm: '¿Estás seguro? Esta acción eliminará permanentemente la organización y no se puede deshacer.',
@@ -52289,7 +52503,7 @@ const ORGANIZATION_VIEW_I18N = {
52289
52503
  retry: 'Retry',
52290
52504
  saveError: 'Could not save. Please try again.',
52291
52505
  leaveError: 'Could not leave. Please try again.',
52292
- transferTitle: 'Transfer ownership',
52506
+ transferTitle: '⚠️ Transfer ownership',
52293
52507
  transferHint: 'Choose a member to transfer ownership of this organization.',
52294
52508
  transferSelect: 'Select new owner',
52295
52509
  transferCta: 'Transfer',
@@ -52340,7 +52554,10 @@ const ORGANIZATION_VIEW_I18N = {
52340
52554
  permDelete: 'Delete',
52341
52555
  permManage: 'Manage',
52342
52556
  membersShowMore: 'Show more',
52343
- deleteTitle: 'Delete organization',
52557
+ orgsNewQuestion: 'Need another organization?',
52558
+ orgsNewHint: 'Create a separate space for another team, company or project.',
52559
+ orgsNewCta: 'New organization',
52560
+ deleteTitle: '⚠️ Delete organization',
52344
52561
  deleteHint: 'This action is permanent and irreversible. All members will lose access.',
52345
52562
  deleteCta: 'Delete organization',
52346
52563
  deleteConfirm: 'Are you sure? This action will permanently delete the organization and cannot be undone.',
@@ -52406,6 +52623,7 @@ class OrganizationViewComponent {
52406
52623
  showMembers: merged.showMembers ?? true,
52407
52624
  showRoles: merged.showRoles ?? true,
52408
52625
  showInvite: merged.showInvite ?? true,
52626
+ showNewOrgCta: merged.showNewOrgCta ?? false,
52409
52627
  showTransferOwnership: merged.showTransferOwnership ?? true,
52410
52628
  showLeave: merged.showLeave ?? true,
52411
52629
  showDeleteOrg: merged.showDeleteOrg ?? true,
@@ -52418,6 +52636,7 @@ class OrganizationViewComponent {
52418
52636
  onOwnershipTransferred: merged.onOwnershipTransferred,
52419
52637
  onLeftOrg: merged.onLeftOrg,
52420
52638
  onOrgDeleted: merged.onOrgDeleted,
52639
+ onOrgCreated: merged.onOrgCreated,
52421
52640
  };
52422
52641
  });
52423
52642
  this.org = signal(null);
@@ -52426,6 +52645,7 @@ class OrganizationViewComponent {
52426
52645
  this.loading = signal(false);
52427
52646
  this.leaving = signal(false);
52428
52647
  this.permissionsOpen = signal(false);
52648
+ this.createOrgModalOpen = signal(false);
52429
52649
  this.orgLoadError = signal(null);
52430
52650
  this.orgErrorState = computed(() => {
52431
52651
  this.i18n.lang();
@@ -52571,6 +52791,28 @@ class OrganizationViewComponent {
52571
52791
  type: 'button',
52572
52792
  state: this.deleting() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
52573
52793
  }));
52794
+ this.newOrgCtaProps = computed(() => ({
52795
+ title: this.tt('orgsNewQuestion'),
52796
+ description: this.tt('orgsNewHint'),
52797
+ padding: '16px',
52798
+ borderRadius: '14px',
52799
+ actions: {
52800
+ position: 'right',
52801
+ columned: false,
52802
+ buttons: [
52803
+ {
52804
+ text: this.tt('orgsNewCta'),
52805
+ color: 'primary',
52806
+ fill: 'solid',
52807
+ shape: 'round',
52808
+ size: 'default',
52809
+ type: 'button',
52810
+ state: 'ENABLED',
52811
+ token: 'org-new',
52812
+ },
52813
+ ],
52814
+ },
52815
+ }));
52574
52816
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
52575
52817
  this.lastLoadedOrgId = null;
52576
52818
  const ns = this.ns;
@@ -52680,6 +52922,13 @@ class OrganizationViewComponent {
52680
52922
  this.deleting.set(false);
52681
52923
  }
52682
52924
  }
52925
+ onNewOrg() {
52926
+ this.createOrgModalOpen.set(true);
52927
+ }
52928
+ onOrgCreated(newOrg) {
52929
+ this.createOrgModalOpen.set(false);
52930
+ this.resolvedConfig().onOrgCreated?.(newOrg);
52931
+ }
52683
52932
  onOpenInvite() {
52684
52933
  void this.modalService.openAdaptive({
52685
52934
  component: InviteMemberModalComponent,
@@ -53124,6 +53373,13 @@ class OrganizationViewComponent {
53124
53373
  </section>
53125
53374
  }
53126
53375
 
53376
+ @if (resolvedConfig().showNewOrgCta) {
53377
+ <!-- Nueva org CTA -->
53378
+ <section class="settings-section">
53379
+ <val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
53380
+ </section>
53381
+ }
53382
+
53127
53383
  @if (resolvedConfig().showInvite) {
53128
53384
  <!-- Invite: solo admin+ (gating RBAC interno) -->
53129
53385
  <section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
@@ -53272,12 +53528,19 @@ class OrganizationViewComponent {
53272
53528
  [config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
53273
53529
  (dismissed)="permissionsOpen.set(false)"
53274
53530
  />
53531
+ <val-create-org-modal
53532
+ [isOpen]="createOrgModalOpen()"
53533
+ (dismissed)="createOrgModalOpen.set(false)"
53534
+ (created)="onOrgCreated($event)"
53535
+ />
53275
53536
  </div>
53276
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { 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"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
53537
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"], dependencies: [{ kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { 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"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: PermissionsModalComponent, selector: "val-permissions-modal", inputs: ["isOpen", "config"], outputs: ["dismissed"] }] }); }
53277
53538
  }
53278
53539
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
53279
53540
  type: Component,
53280
53541
  args: [{ selector: 'val-organization-view', standalone: true, imports: [
53542
+ CreateOrgModalComponent,
53543
+ CtaCardComponent,
53281
53544
  DisplayComponent,
53282
53545
  EmptyStateComponent,
53283
53546
  HasPermissionDirective,
@@ -53448,6 +53711,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
53448
53711
  </section>
53449
53712
  }
53450
53713
 
53714
+ @if (resolvedConfig().showNewOrgCta) {
53715
+ <!-- Nueva org CTA -->
53716
+ <section class="settings-section">
53717
+ <val-cta-card [props]="newOrgCtaProps()" (onAction)="onNewOrg()" />
53718
+ </section>
53719
+ }
53720
+
53451
53721
  @if (resolvedConfig().showInvite) {
53452
53722
  <!-- Invite: solo admin+ (gating RBAC interno) -->
53453
53723
  <section *valHasPermission="'users:*'" class="settings-section" data-testid="org-invite-section">
@@ -53596,6 +53866,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
53596
53866
  [config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
53597
53867
  (dismissed)="permissionsOpen.set(false)"
53598
53868
  />
53869
+ <val-create-org-modal
53870
+ [isOpen]="createOrgModalOpen()"
53871
+ (dismissed)="createOrgModalOpen.set(false)"
53872
+ (created)="onOrgCreated($event)"
53873
+ />
53599
53874
  </div>
53600
53875
  `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;flex-direction:column;gap:16px}.invite-cta__text{display:flex;flex-direction:column;gap:4px}.invite-cta__actions{display:flex;gap:12px;flex-wrap:wrap}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}.org-info-logo{display:flex;justify-content:center;padding:8px 0 16px}.org-logo-img{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid var(--ion-color-light)}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field:last-child{border-bottom:none}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}.section-title-danger{display:flex;align-items:center;gap:8px}.section-title-danger val-title{display:block}.danger-icon{display:block;font-size:1.125rem;color:var(--ion-color-warning-shade, #d4a017);flex-shrink:0}\n"] }]
53601
53876
  }], ctorParameters: () => [], propDecorators: { config: [{