valtech-components 4.0.27 → 4.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/esm2022/lib/components/organisms/account-view/account-view.component.mjs +34 -30
  2. package/esm2022/lib/components/organisms/change-password-modal/change-password-modal.component.mjs +3 -3
  3. package/esm2022/lib/components/organisms/create-org-modal/create-org-modal.component.mjs +91 -65
  4. package/esm2022/lib/components/organisms/delete-account-modal/delete-account-modal.component.mjs +106 -84
  5. package/esm2022/lib/components/organisms/login/login.component.mjs +11 -4
  6. package/esm2022/lib/components/organisms/login/types.mjs +23 -1
  7. package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +16 -3
  8. package/esm2022/lib/components/organisms/permissions-view/permissions-modal.component.mjs +70 -0
  9. package/esm2022/lib/components/organisms/permissions-view/permissions-view.component.mjs +16 -5
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +2 -1
  12. package/fesm2022/valtech-components.mjs +1125 -956
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/organisms/account-view/account-view.component.d.ts +3 -0
  15. package/lib/components/organisms/create-org-modal/create-org-modal.component.d.ts +8 -8
  16. package/lib/components/organisms/delete-account-modal/delete-account-modal.component.d.ts +7 -8
  17. package/lib/components/organisms/login/login.component.d.ts +6 -0
  18. package/lib/components/organisms/login/types.d.ts +12 -0
  19. package/lib/components/organisms/organization-view/organization-view.component.d.ts +1 -0
  20. package/lib/components/organisms/permissions-view/permissions-modal.component.d.ts +13 -0
  21. package/lib/components/organisms/permissions-view/permissions-view.component.d.ts +9 -2
  22. package/lib/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/public-api.d.ts +1 -0
  25. package/src/lib/services/firebase/firebase-messaging-sw.js +145 -0
@@ -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.27';
59
+ const VERSION = '4.0.29';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -33564,11 +33564,11 @@ class ChangePasswordModalComponent {
33564
33564
  this.toast.show({ message, duration: 3500 });
33565
33565
  }
33566
33566
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
33567
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
33567
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-modal", inputs: { isOpen: "isOpen" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\" cssClass=\"val-modal-adaptive\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
33568
33568
  }
33569
33569
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
33570
33570
  type: Component,
33571
- args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonModal, IonSpinner, IonToolbar, FormComponent], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
33571
+ args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonModal, IonSpinner, IonToolbar, FormComponent], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\" cssClass=\"val-modal-adaptive\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" color=\"dark\" shape=\"round\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\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 @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
33572
33572
  }], propDecorators: { isOpen: [{
33573
33573
  type: Input
33574
33574
  }], changed: [{
@@ -40218,6 +40218,28 @@ const LOGIN_DEFAULTS = {
40218
40218
  showForgotPassword: true,
40219
40219
  showCard: false,
40220
40220
  };
40221
+ /**
40222
+ * Logo por defecto de la card de login cuando el consumer no pasa `logo`.
40223
+ *
40224
+ * Usa el token CSS `--main-logo` (resuelto por `val-image` vía `var(--main-logo)`),
40225
+ * que cada app del factory define en su `theme/variables.scss` con su logo de
40226
+ * marca (incluye variante dark). Así el login muestra SIEMPRE el logo de la app
40227
+ * sin acoplar la lib a una marca concreta — es el estándar del factory.
40228
+ *
40229
+ * Una app puede override pasando su propio `logo` en `LoginMetadata` (ej.
40230
+ * showcase usa un mark propio `terminal.svg`).
40231
+ */
40232
+ const DEFAULT_LOGIN_LOGO = {
40233
+ src: '--main-logo',
40234
+ alt: '',
40235
+ mode: 'box',
40236
+ shaded: false,
40237
+ bordered: false,
40238
+ size: 'xlarge',
40239
+ alignment: 'center',
40240
+ flex: true,
40241
+ width: 8,
40242
+ };
40221
40243
 
40222
40244
  class LoginComponent {
40223
40245
  constructor() {
@@ -40569,6 +40591,13 @@ class LoginComponent {
40569
40591
  get config() {
40570
40592
  return { ...LOGIN_DEFAULTS, ...this.props };
40571
40593
  }
40594
+ /**
40595
+ * Logo a renderizar arriba del form. Si el consumer no pasa `logo`, cae al
40596
+ * default de marca del factory (`--main-logo` por app). Ver DEFAULT_LOGIN_LOGO.
40597
+ */
40598
+ get resolvedLogo() {
40599
+ return this.props.logo ?? DEFAULT_LOGIN_LOGO;
40600
+ }
40572
40601
  // ==========================================
40573
40602
  // HANDLERS
40574
40603
  // ==========================================
@@ -40930,7 +40959,7 @@ class LoginComponent {
40930
40959
  this.stopResetResendCooldown();
40931
40960
  }
40932
40961
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
40933
- 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 -->\n @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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"] }] }); }
40962
+ 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"] }] }); }
40934
40963
  }
40935
40964
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginComponent, decorators: [{
40936
40965
  type: Component,
@@ -40946,7 +40975,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40946
40975
  IonToolbar,
40947
40976
  FormComponent,
40948
40977
  ImageComponent,
40949
- ], template: "<div class=\"val-login\" [class.val-login--card]=\"config.showCard\">\n <!-- Logo -->\n @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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 @if (props.logo) {\n <div class=\"logo-container\">\n <val-image [props]=\"props.logo\" />\n </div>\n }\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"] }]
40978
+ ], 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"] }]
40950
40979
  }], propDecorators: { props: [{
40951
40980
  type: Input
40952
40981
  }], onSuccess: [{
@@ -46678,6 +46707,16 @@ const DEFAULT_NAMESPACE$h = 'CreateOrgModal';
46678
46707
  * `CreateOrgModal`.
46679
46708
  */
46680
46709
  class CreateOrgModalComponent {
46710
+ set isOpen(value) {
46711
+ const opening = value && !this._isOpen;
46712
+ this._isOpen = value;
46713
+ if (opening) {
46714
+ this._busy.set(false);
46715
+ }
46716
+ }
46717
+ get isOpen() {
46718
+ return this._isOpen;
46719
+ }
46681
46720
  constructor() {
46682
46721
  this.i18n = inject(I18nService);
46683
46722
  this.orgService = inject(OrgService);
@@ -46686,6 +46725,9 @@ class CreateOrgModalComponent {
46686
46725
  this.errors = inject(ValtechErrorService);
46687
46726
  /** Namespace i18n con que la vista resuelve sus textos. */
46688
46727
  this.i18nNamespace = DEFAULT_NAMESPACE$h;
46728
+ this._isOpen = false;
46729
+ this.dismissed = new EventEmitter();
46730
+ this.created = new EventEmitter();
46689
46731
  this._busy = signal(false);
46690
46732
  if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$h)) {
46691
46733
  this.i18n.registerContent(DEFAULT_NAMESPACE$h, CREATE_ORG_MODAL_I18N);
@@ -46731,8 +46773,8 @@ class CreateOrgModalComponent {
46731
46773
  duration: 3500,
46732
46774
  });
46733
46775
  await this.orgSwitch.switchTo(newOrg.id);
46734
- this.onSuccess?.(newOrg);
46735
- this._modalRef?.dismiss(newOrg, 'confirm');
46776
+ this.created.emit(newOrg);
46777
+ this.dismissed.emit();
46736
46778
  }
46737
46779
  catch (err) {
46738
46780
  this.errors.handle(err, { i18nNamespace: this.i18nNamespace });
@@ -46742,7 +46784,10 @@ class CreateOrgModalComponent {
46742
46784
  }
46743
46785
  }
46744
46786
  dismiss() {
46745
- this._modalRef?.dismiss(null, 'cancel');
46787
+ this.dismissed.emit();
46788
+ }
46789
+ close() {
46790
+ this.dismissed.emit();
46746
46791
  }
46747
46792
  buildFormMeta() {
46748
46793
  const nameField = {
@@ -46822,71 +46867,81 @@ class CreateOrgModalComponent {
46822
46867
  return this.i18n.t(key, this.i18nNamespace);
46823
46868
  }
46824
46869
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CreateOrgModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46825
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CreateOrgModalComponent, isStandalone: true, selector: "val-create-org-modal", inputs: { _modalRef: "_modalRef", onSuccess: "onSuccess", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
46826
- <ion-header>
46827
- <ion-toolbar>
46828
- <ion-buttons slot="end">
46829
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
46830
- <strong>{{ t('close') }}</strong>
46831
- </ion-button>
46832
- </ion-buttons>
46833
- </ion-toolbar>
46834
- </ion-header>
46835
- <ion-content class="ion-padding">
46836
- <val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
46837
- <val-title
46838
- [props]="{
46839
- content: t('pageDescription'),
46840
- size: 'large',
46841
- color: '',
46842
- bold: false,
46843
- }"
46844
- />
46845
- <val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
46846
- <div class="modal-close-bottom">
46847
- <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
46848
- {{ t('close') }}
46849
- </ion-button>
46850
- </div>
46851
- </ion-content>
46852
- `, isInline: true, styles: [".modal-close-bottom{margin-top:16px}\n"], dependencies: [{ kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
46870
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CreateOrgModalComponent, isStandalone: true, selector: "val-create-org-modal", inputs: { i18nNamespace: "i18nNamespace", isOpen: "isOpen" }, outputs: { dismissed: "dismissed", created: "created" }, ngImport: i0, template: `
46871
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
46872
+ <ng-template>
46873
+ <ion-header>
46874
+ <ion-toolbar>
46875
+ <ion-buttons slot="end">
46876
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
46877
+ <strong>{{ t('close') }}</strong>
46878
+ </ion-button>
46879
+ </ion-buttons>
46880
+ </ion-toolbar>
46881
+ </ion-header>
46882
+ <ion-content class="ion-padding">
46883
+ <val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
46884
+ <val-title
46885
+ [props]="{
46886
+ content: t('pageDescription'),
46887
+ size: 'large',
46888
+ color: '',
46889
+ bold: false,
46890
+ }"
46891
+ />
46892
+ <val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
46893
+ <div class="modal-close-bottom">
46894
+ <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
46895
+ {{ t('close') }}
46896
+ </ion-button>
46897
+ </div>
46898
+ </ion-content>
46899
+ </ng-template>
46900
+ </ion-modal>
46901
+ `, isInline: true, styles: [".modal-close-bottom{margin-top:16px}\n"], dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
46853
46902
  }
46854
46903
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CreateOrgModalComponent, decorators: [{
46855
46904
  type: Component,
46856
- args: [{ selector: 'val-create-org-modal', standalone: true, imports: [IonHeader, IonToolbar, IonButtons, IonButton, IonContent, FormComponent, DisplayComponent, TitleComponent], template: `
46857
- <ion-header>
46858
- <ion-toolbar>
46859
- <ion-buttons slot="end">
46860
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
46861
- <strong>{{ t('close') }}</strong>
46862
- </ion-button>
46863
- </ion-buttons>
46864
- </ion-toolbar>
46865
- </ion-header>
46866
- <ion-content class="ion-padding">
46867
- <val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
46868
- <val-title
46869
- [props]="{
46870
- content: t('pageDescription'),
46871
- size: 'large',
46872
- color: '',
46873
- bold: false,
46874
- }"
46875
- />
46876
- <val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
46877
- <div class="modal-close-bottom">
46878
- <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
46879
- {{ t('close') }}
46880
- </ion-button>
46881
- </div>
46882
- </ion-content>
46905
+ args: [{ selector: 'val-create-org-modal', standalone: true, imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, FormComponent, DisplayComponent, TitleComponent], template: `
46906
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
46907
+ <ng-template>
46908
+ <ion-header>
46909
+ <ion-toolbar>
46910
+ <ion-buttons slot="end">
46911
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
46912
+ <strong>{{ t('close') }}</strong>
46913
+ </ion-button>
46914
+ </ion-buttons>
46915
+ </ion-toolbar>
46916
+ </ion-header>
46917
+ <ion-content class="ion-padding">
46918
+ <val-display [props]="{ content: t('pageTitle'), size: 'small', color: 'dark' }" />
46919
+ <val-title
46920
+ [props]="{
46921
+ content: t('pageDescription'),
46922
+ size: 'large',
46923
+ color: '',
46924
+ bold: false,
46925
+ }"
46926
+ />
46927
+ <val-form [props]="formMeta()" (onSubmit)="onSubmit($event)" />
46928
+ <div class="modal-close-bottom">
46929
+ <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
46930
+ {{ t('close') }}
46931
+ </ion-button>
46932
+ </div>
46933
+ </ion-content>
46934
+ </ng-template>
46935
+ </ion-modal>
46883
46936
  `, styles: [".modal-close-bottom{margin-top:16px}\n"] }]
46884
- }], ctorParameters: () => [], propDecorators: { _modalRef: [{
46885
- type: Input
46886
- }], onSuccess: [{
46937
+ }], ctorParameters: () => [], propDecorators: { i18nNamespace: [{
46887
46938
  type: Input
46888
- }], i18nNamespace: [{
46939
+ }], isOpen: [{
46889
46940
  type: Input
46941
+ }], dismissed: [{
46942
+ type: Output
46943
+ }], created: [{
46944
+ type: Output
46890
46945
  }] } });
46891
46946
 
46892
46947
  /**
@@ -46949,6 +47004,21 @@ const DEFAULT_NAMESPACE$g = 'Settings.DeleteAccount';
46949
47004
  * proveyó el namespace `Settings.DeleteAccount`.
46950
47005
  */
46951
47006
  class DeleteAccountModalComponent {
47007
+ set isOpen(value) {
47008
+ const opening = value && !this._isOpen;
47009
+ this._isOpen = value;
47010
+ if (opening) {
47011
+ this.hasPassword.set(null);
47012
+ this.codeSent.set(false);
47013
+ this.formState.set(ComponentStates.ENABLED);
47014
+ firstValueFrom(this.auth.getProfile())
47015
+ .then(profile => this.hasPassword.set(profile.hasPassword ?? true))
47016
+ .catch(() => this.hasPassword.set(true));
47017
+ }
47018
+ }
47019
+ get isOpen() {
47020
+ return this._isOpen;
47021
+ }
46952
47022
  constructor() {
46953
47023
  this.auth = inject(AuthService);
46954
47024
  this.errors = inject(ValtechErrorService);
@@ -46956,6 +47026,8 @@ class DeleteAccountModalComponent {
46956
47026
  this.toast = inject(ToastService);
46957
47027
  /** Namespace i18n con que la vista resuelve sus textos. */
46958
47028
  this.i18nNamespace = DEFAULT_NAMESPACE$g;
47029
+ this._isOpen = false;
47030
+ this.dismissed = new EventEmitter();
46959
47031
  this.hasPassword = signal(null);
46960
47032
  this.codeSent = signal(false);
46961
47033
  this.formState = signal(ComponentStates.ENABLED);
@@ -47026,11 +47098,6 @@ class DeleteAccountModalComponent {
47026
47098
  this.i18n.registerContent(DEFAULT_NAMESPACE$g, DELETE_ACCOUNT_MODAL_I18N);
47027
47099
  }
47028
47100
  }
47029
- ngOnInit() {
47030
- firstValueFrom(this.auth.getProfile())
47031
- .then(profile => this.hasPassword.set(profile.hasPassword ?? true))
47032
- .catch(() => this.hasPassword.set(true));
47033
- }
47034
47101
  submitBtn(token, textKey) {
47035
47102
  return {
47036
47103
  text: this.t(textKey),
@@ -47045,7 +47112,7 @@ class DeleteAccountModalComponent {
47045
47112
  };
47046
47113
  }
47047
47114
  close() {
47048
- this._modalRef?.dismiss(undefined, 'cancel');
47115
+ this.dismissed.emit();
47049
47116
  }
47050
47117
  async onPasswordSubmit(event) {
47051
47118
  const password = event.fields['password'];
@@ -47105,89 +47172,99 @@ class DeleteAccountModalComponent {
47105
47172
  }
47106
47173
  }
47107
47174
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeleteAccountModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
47108
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DeleteAccountModalComponent, isStandalone: true, selector: "val-delete-account-modal", inputs: { _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
47109
- <ion-header>
47110
- <ion-toolbar>
47111
- <ion-buttons slot="end">
47112
- <ion-button fill="clear" color="dark" shape="round" (click)="close()">
47113
- <strong>{{ t('close') }}</strong>
47114
- </ion-button>
47115
- </ion-buttons>
47116
- </ion-toolbar>
47117
- </ion-header>
47175
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DeleteAccountModalComponent, isStandalone: true, selector: "val-delete-account-modal", inputs: { i18nNamespace: "i18nNamespace", isOpen: "isOpen" }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
47176
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
47177
+ <ng-template>
47178
+ <ion-header>
47179
+ <ion-toolbar>
47180
+ <ion-buttons slot="end">
47181
+ <ion-button fill="clear" color="dark" shape="round" (click)="close()">
47182
+ <strong>{{ t('close') }}</strong>
47183
+ </ion-button>
47184
+ </ion-buttons>
47185
+ </ion-toolbar>
47186
+ </ion-header>
47118
47187
 
47119
- <ion-content class="ion-padding">
47120
- <section class="modal-form-section">
47121
- @if (hasPassword() === null) {
47122
- <div class="spinner-center">
47123
- <ion-spinner name="crescent" />
47124
- </div>
47125
- } @else if (hasPassword()) {
47126
- <val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
47127
- } @else if (!codeSent()) {
47128
- <val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
47129
- } @else {
47130
- <val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
47131
- <div class="resend-row">
47132
- <ion-button
47133
- fill="clear"
47134
- color="medium"
47135
- size="small"
47136
- [disabled]="formState() === 'WORKING'"
47137
- (click)="onResendCode()"
47138
- >
47139
- {{ t('resend') }}
47140
- </ion-button>
47141
- </div>
47142
- }
47143
- </section>
47144
- </ion-content>
47145
- `, isInline: true, styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.resend-row{display:flex;justify-content:center;margin-top:-8px}.spinner-center{display:flex;justify-content:center;padding-top:32px}\n"], dependencies: [{ kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
47188
+ <ion-content class="ion-padding">
47189
+ <section class="modal-form-section">
47190
+ @if (hasPassword() === null) {
47191
+ <div class="spinner-center">
47192
+ <ion-spinner name="crescent" />
47193
+ </div>
47194
+ } @else if (hasPassword()) {
47195
+ <val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
47196
+ } @else if (!codeSent()) {
47197
+ <val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
47198
+ } @else {
47199
+ <val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
47200
+ <div class="resend-row">
47201
+ <ion-button
47202
+ fill="clear"
47203
+ color="medium"
47204
+ size="small"
47205
+ [disabled]="formState() === 'WORKING'"
47206
+ (click)="onResendCode()"
47207
+ >
47208
+ {{ t('resend') }}
47209
+ </ion-button>
47210
+ </div>
47211
+ }
47212
+ </section>
47213
+ </ion-content>
47214
+ </ng-template>
47215
+ </ion-modal>
47216
+ `, isInline: true, styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.resend-row{display:flex;justify-content:center;margin-top:-8px}.spinner-center{display:flex;justify-content:center;padding-top:32px}\n"], dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }] }); }
47146
47217
  }
47147
47218
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DeleteAccountModalComponent, decorators: [{
47148
47219
  type: Component,
47149
- args: [{ selector: 'val-delete-account-modal', standalone: true, imports: [IonHeader, IonToolbar, IonButtons, IonButton, IonContent, IonSpinner, FormComponent], template: `
47150
- <ion-header>
47151
- <ion-toolbar>
47152
- <ion-buttons slot="end">
47153
- <ion-button fill="clear" color="dark" shape="round" (click)="close()">
47154
- <strong>{{ t('close') }}</strong>
47155
- </ion-button>
47156
- </ion-buttons>
47157
- </ion-toolbar>
47158
- </ion-header>
47220
+ args: [{ selector: 'val-delete-account-modal', standalone: true, imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, IonSpinner, FormComponent], template: `
47221
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
47222
+ <ng-template>
47223
+ <ion-header>
47224
+ <ion-toolbar>
47225
+ <ion-buttons slot="end">
47226
+ <ion-button fill="clear" color="dark" shape="round" (click)="close()">
47227
+ <strong>{{ t('close') }}</strong>
47228
+ </ion-button>
47229
+ </ion-buttons>
47230
+ </ion-toolbar>
47231
+ </ion-header>
47159
47232
 
47160
- <ion-content class="ion-padding">
47161
- <section class="modal-form-section">
47162
- @if (hasPassword() === null) {
47163
- <div class="spinner-center">
47164
- <ion-spinner name="crescent" />
47165
- </div>
47166
- } @else if (hasPassword()) {
47167
- <val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
47168
- } @else if (!codeSent()) {
47169
- <val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
47170
- } @else {
47171
- <val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
47172
- <div class="resend-row">
47173
- <ion-button
47174
- fill="clear"
47175
- color="medium"
47176
- size="small"
47177
- [disabled]="formState() === 'WORKING'"
47178
- (click)="onResendCode()"
47179
- >
47180
- {{ t('resend') }}
47181
- </ion-button>
47182
- </div>
47183
- }
47184
- </section>
47185
- </ion-content>
47233
+ <ion-content class="ion-padding">
47234
+ <section class="modal-form-section">
47235
+ @if (hasPassword() === null) {
47236
+ <div class="spinner-center">
47237
+ <ion-spinner name="crescent" />
47238
+ </div>
47239
+ } @else if (hasPassword()) {
47240
+ <val-form [props]="passwordFormProps()" (onSubmit)="onPasswordSubmit($event)" />
47241
+ } @else if (!codeSent()) {
47242
+ <val-form [props]="sendCodeFormProps()" (onSubmit)="onSendCode()" />
47243
+ } @else {
47244
+ <val-form [props]="codeFormProps()" (onSubmit)="onCodeSubmit($event)" />
47245
+ <div class="resend-row">
47246
+ <ion-button
47247
+ fill="clear"
47248
+ color="medium"
47249
+ size="small"
47250
+ [disabled]="formState() === 'WORKING'"
47251
+ (click)="onResendCode()"
47252
+ >
47253
+ {{ t('resend') }}
47254
+ </ion-button>
47255
+ </div>
47256
+ }
47257
+ </section>
47258
+ </ion-content>
47259
+ </ng-template>
47260
+ </ion-modal>
47186
47261
  `, styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.resend-row{display:flex;justify-content:center;margin-top:-8px}.spinner-center{display:flex;justify-content:center;padding-top:32px}\n"] }]
47187
- }], ctorParameters: () => [], propDecorators: { _modalRef: [{
47262
+ }], ctorParameters: () => [], propDecorators: { i18nNamespace: [{
47188
47263
  type: Input
47189
- }], i18nNamespace: [{
47264
+ }], isOpen: [{
47190
47265
  type: Input
47266
+ }], dismissed: [{
47267
+ type: Output
47191
47268
  }] } });
47192
47269
 
47193
47270
  /**
@@ -47878,6 +47955,8 @@ class AccountViewComponent {
47878
47955
  retrying: this.orgsLoading(),
47879
47956
  });
47880
47957
  });
47958
+ this.createOrgOpen = signal(false);
47959
+ this.deleteAccountOpen = signal(false);
47881
47960
  this.pendingInvites = signal([]);
47882
47961
  this.pendingInvitesLoading = signal(false);
47883
47962
  this.inviteAccepting = signal(null);
@@ -47991,40 +48070,18 @@ class AccountViewComponent {
47991
48070
  });
47992
48071
  }
47993
48072
  onDeleteAccount() {
47994
- void this.modalService.open({
47995
- component: DeleteAccountModalComponent,
47996
- componentProps: {
47997
- i18nNamespace: 'Settings.DeleteAccount',
47998
- },
47999
- breakpoints: {
48000
- breakpoints: [0, 0.85],
48001
- initialBreakpoint: 0.85,
48002
- showHandle: true,
48003
- },
48004
- backdropDismiss: false,
48005
- });
48073
+ this.deleteAccountOpen.set(true);
48006
48074
  this.resolvedConfig().onAccountDeleted?.();
48007
48075
  }
48008
48076
  onManageOrg() {
48009
48077
  this.nav.navigateByUrl(this.resolvedConfig().manageOrgRoute);
48010
48078
  }
48011
48079
  onNewOrg() {
48012
- void this.modalService.open({
48013
- component: CreateOrgModalComponent,
48014
- componentProps: {
48015
- i18nNamespace: 'CreateOrgModal',
48016
- onSuccess: (newOrg) => {
48017
- this.loadOrgs();
48018
- this.resolvedConfig().onOrgCreated?.(newOrg);
48019
- },
48020
- },
48021
- breakpoints: {
48022
- breakpoints: [0, 0.92],
48023
- initialBreakpoint: 0.92,
48024
- showHandle: true,
48025
- },
48026
- backdropDismiss: true,
48027
- });
48080
+ this.createOrgOpen.set(true);
48081
+ }
48082
+ onOrgCreated(newOrg) {
48083
+ this.loadOrgs();
48084
+ this.resolvedConfig().onOrgCreated?.(newOrg);
48028
48085
  }
48029
48086
  onMoreInfo() {
48030
48087
  void this.modalService.open({
@@ -48314,8 +48371,19 @@ class AccountViewComponent {
48314
48371
  </div>
48315
48372
  </section>
48316
48373
  }
48374
+ <val-create-org-modal
48375
+ [isOpen]="createOrgOpen()"
48376
+ [i18nNamespace]="'CreateOrgModal'"
48377
+ (dismissed)="createOrgOpen.set(false)"
48378
+ (created)="onOrgCreated($event)"
48379
+ />
48380
+ <val-delete-account-modal
48381
+ [isOpen]="deleteAccountOpen()"
48382
+ [i18nNamespace]="'Settings.DeleteAccount'"
48383
+ (dismissed)="deleteAccountOpen.set(false)"
48384
+ />
48317
48385
  </div>
48318
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;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: "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: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: InvitationCardComponent, selector: "val-invitation-card", inputs: ["props"], outputs: ["onAccept", "onDecline"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { 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: "pipe", type: TitleCasePipe, name: "titlecase" }] }); }
48386
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;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: "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: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: InvitationCardComponent, selector: "val-invitation-card", inputs: ["props"], outputs: ["onAccept", "onDecline"] }, { kind: "component", type: CreateOrgModalComponent, selector: "val-create-org-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed", "created"] }, { kind: "component", type: DeleteAccountModalComponent, selector: "val-delete-account-modal", inputs: ["i18nNamespace", "isOpen"], outputs: ["dismissed"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { 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: "pipe", type: TitleCasePipe, name: "titlecase" }] }); }
48319
48387
  }
48320
48388
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AccountViewComponent, decorators: [{
48321
48389
  type: Component,
@@ -48328,6 +48396,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48328
48396
  ButtonComponent,
48329
48397
  CtaCardComponent,
48330
48398
  InvitationCardComponent,
48399
+ CreateOrgModalComponent,
48400
+ DeleteAccountModalComponent,
48331
48401
  IonIcon,
48332
48402
  IonButton,
48333
48403
  TitleCasePipe,
@@ -48501,6 +48571,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48501
48571
  </div>
48502
48572
  </section>
48503
48573
  }
48574
+ <val-create-org-modal
48575
+ [isOpen]="createOrgOpen()"
48576
+ [i18nNamespace]="'CreateOrgModal'"
48577
+ (dismissed)="createOrgOpen.set(false)"
48578
+ (created)="onOrgCreated($event)"
48579
+ />
48580
+ <val-delete-account-modal
48581
+ [isOpen]="deleteAccountOpen()"
48582
+ [i18nNamespace]="'Settings.DeleteAccount'"
48583
+ (dismissed)="deleteAccountOpen.set(false)"
48584
+ />
48504
48585
  </div>
48505
48586
  `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.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))}.settings-section val-title{display:block;margin-bottom:12px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;flex-wrap:wrap;gap:8px}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.org-new-cta-card{display:block;margin-top:12px}.orgs-empty-card{background:var(--ion-color-secondary-tint, rgba(var(--ion-color-secondary-rgb, 130, 101, 208), .2));border-radius:18px;padding:18px 16px 12px;display:flex;flex-direction:column;gap:8px}.orgs-empty-card__main{display:flex;align-items:center;gap:10px}.orgs-empty-card__icon{font-size:20px;flex-shrink:0;color:var(--ion-color-dark)}.orgs-empty-card__text{font-size:.875rem;color:var(--ion-color-dark)}.orgs-empty-card__link{background:none;border:none;padding:0;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);text-align:left;text-decoration:underline;text-underline-offset:2px;font-family:inherit}.orgs-list{display:flex;flex-direction:column;gap:8px}.org-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1.5px solid transparent;cursor:pointer;transition:background .15s,border-color .15s}.org-card:active{opacity:.75}.org-card--active{border-color:var(--ion-color-primary);background:color-mix(in srgb,var(--ion-color-primary) 8%,transparent)}:host-context(body.dark) .org-card,:host-context(html.ion-palette-dark) .org-card,:host-context([data-theme=\"dark\"]) .org-card{background:#ffffff0d}:host-context(body.dark) .org-card--active,:host-context(html.ion-palette-dark) .org-card--active,:host-context([data-theme=\"dark\"]) .org-card--active{background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent)}.org-card__icon{width:40px;height:40px;border-radius:50%;background:color-mix(in srgb,var(--ion-color-primary) 15%,transparent);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:20px;color:var(--ion-color-primary)}.org-card__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.org-card__name{font-weight:600;font-size:.95rem;color:var(--ion-color-dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.org-card__meta{display:flex;flex-direction:column;gap:1px;margin-top:3px}.org-card__meta-item{font-size:.78rem;color:var(--ion-color-dark)}.org-card__meta-label{font-weight:600;color:var(--ion-color-dark);margin-right:4px}.org-card__end{display:flex;align-items:center;gap:8px;flex-shrink:0}\n"] }]
48506
48587
  }], ctorParameters: () => [], propDecorators: { config: [{
@@ -50004,188 +50085,880 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50004
50085
  }] } });
50005
50086
 
50006
50087
  /**
50007
- * Defaults i18n (es/en) embebidos en `val-transfer-ownership-modal`.
50008
- * Auto-registrados en el constructor del componente si el consumer no proveyó el
50009
- * namespace (`Settings.TransferModal` por default). SOLO incluye las keys que el
50010
- * modal usa.
50088
+ * Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
50089
+ * el constructor del componente si el consumer no proveyó el namespace
50090
+ * (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
50091
+ *
50092
+ * Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
50011
50093
  */
50012
- const TRANSFER_OWNERSHIP_MODAL_I18N = {
50094
+ const PERMISSIONS_VIEW_I18N = {
50013
50095
  es: {
50014
- title: 'Transferir propiedad',
50015
- hint: 'Selecciona el nuevo propietario de la organización.',
50016
- searchPlaceholder: 'Buscar miembro...',
50017
- empty: 'No hay otros miembros disponibles.',
50018
- noResults: 'Sin resultados para',
50019
- confirmCta: 'Confirmar transferencia',
50020
- close: 'Cancelar',
50096
+ pageTitle: 'Permisos',
50097
+ pageDescription: 'Roles y permisos de esta app en la organización activa',
50098
+ systemRolesTitle: 'Roles del sistema',
50099
+ systemRolesHint: 'Predefinidos por la plataforma. No editables.',
50100
+ customRolesTitle: 'Roles personalizados',
50101
+ customRolesHint: 'Definidos por tu organización.',
50102
+ systemBadge: 'Sistema',
50103
+ noCustomRoles: 'Esta organización no tiene roles personalizados.',
50104
+ noPermissions: 'Sin permisos asignados.',
50105
+ // Grupos de alcance
50106
+ groupApp: 'Permisos de la app',
50107
+ groupOrg: 'Permisos de la organización',
50108
+ groupPlatform: 'Permisos de plataforma',
50109
+ groupOther: 'Otras apps',
50110
+ // Roles del sistema
50111
+ roleViewer: 'Visor',
50112
+ roleEditor: 'Editor',
50113
+ roleAdmin: 'Administrador',
50114
+ roleSuperAdmin: 'Super Administrador',
50115
+ roleBingoAdmin: 'Bingo · Administrador',
50116
+ roleBingoTesorero: 'Bingo · Tesorero',
50117
+ roleBingoTaquilla: 'Bingo · Taquilla',
50118
+ roleBingoOperator: 'Bingo · Operador',
50119
+ // Apps
50120
+ app_showcase: 'Showcase',
50121
+ app_bingo: 'Bingo',
50122
+ // Recursos
50123
+ permUsers: 'Usuarios',
50124
+ permRoles: 'Roles',
50125
+ permRbac: 'Control de acceso',
50126
+ permApps: 'Apps',
50127
+ 'permApi-keys': 'API keys',
50128
+ 'permEmail-logs': 'Logs de correo',
50129
+ 'permSms-logs': 'Logs de SMS',
50130
+ permDocuments: 'Documentos',
50131
+ permMedia: 'Media',
50132
+ permTemplates: 'Plantillas',
50133
+ permRequests: 'Solicitudes',
50134
+ permBingo: 'Bingo',
50135
+ // Acciones
50136
+ permRead: 'Leer',
50137
+ permWrite: 'Escribir',
50138
+ permCreate: 'Crear',
50139
+ permDelete: 'Eliminar',
50140
+ permManage: 'Gestionar',
50141
+ permEdit: 'Editar',
50142
+ permSell: 'Vender',
50143
+ permCheckin: 'Canjear',
50144
+ permSettle: 'Rendir',
50145
+ permExport: 'Exportar',
50146
+ permOperate: 'Operar',
50147
+ permAll: 'Todo',
50148
+ // Loading / error
50149
+ errorTitle: 'Error al cargar permisos',
50150
+ retry: 'Reintentar',
50021
50151
  },
50022
50152
  en: {
50023
- title: 'Transfer ownership',
50024
- hint: 'Select the new owner of the organization.',
50025
- searchPlaceholder: 'Search member...',
50026
- empty: 'No other members available.',
50027
- noResults: 'No results for',
50028
- confirmCta: 'Confirm transfer',
50029
- close: 'Cancel',
50153
+ pageTitle: 'Permissions',
50154
+ pageDescription: 'Roles and permissions of this app in the active organization',
50155
+ systemRolesTitle: 'System roles',
50156
+ systemRolesHint: 'Defined by the platform. Not editable.',
50157
+ customRolesTitle: 'Custom roles',
50158
+ customRolesHint: 'Defined by your organization.',
50159
+ systemBadge: 'System',
50160
+ noCustomRoles: 'This organization has no custom roles.',
50161
+ noPermissions: 'No permissions assigned.',
50162
+ groupApp: 'App permissions',
50163
+ groupOrg: 'Organization permissions',
50164
+ groupPlatform: 'Platform permissions',
50165
+ groupOther: 'Other apps',
50166
+ roleViewer: 'Viewer',
50167
+ roleEditor: 'Editor',
50168
+ roleAdmin: 'Administrator',
50169
+ roleSuperAdmin: 'Super Administrator',
50170
+ roleBingoAdmin: 'Bingo · Administrator',
50171
+ roleBingoTesorero: 'Bingo · Treasurer',
50172
+ roleBingoTaquilla: 'Bingo · Box office',
50173
+ roleBingoOperator: 'Bingo · Operator',
50174
+ app_showcase: 'Showcase',
50175
+ app_bingo: 'Bingo',
50176
+ permUsers: 'Users',
50177
+ permRoles: 'Roles',
50178
+ permRbac: 'Access control',
50179
+ permApps: 'Apps',
50180
+ 'permApi-keys': 'API keys',
50181
+ 'permEmail-logs': 'Email logs',
50182
+ 'permSms-logs': 'SMS logs',
50183
+ permDocuments: 'Documents',
50184
+ permMedia: 'Media',
50185
+ permTemplates: 'Templates',
50186
+ permRequests: 'Requests',
50187
+ permBingo: 'Bingo',
50188
+ permRead: 'Read',
50189
+ permWrite: 'Write',
50190
+ permCreate: 'Create',
50191
+ permDelete: 'Delete',
50192
+ permManage: 'Manage',
50193
+ permEdit: 'Edit',
50194
+ permSell: 'Sell',
50195
+ permCheckin: 'Check-in',
50196
+ permSettle: 'Settle',
50197
+ permExport: 'Export',
50198
+ permOperate: 'Operate',
50199
+ permAll: 'All',
50200
+ errorTitle: 'Error loading permissions',
50201
+ retry: 'Retry',
50030
50202
  },
50031
50203
  };
50032
50204
 
50033
- const DEFAULT_NAMESPACE$9 = 'Settings.TransferModal';
50205
+ const DEFAULT_NAMESPACE$9 = 'Settings.Permissions';
50034
50206
  /**
50035
- * `val-transfer-ownership-modal` — selector de miembro para transferir la
50036
- * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
50037
- * ADR-021.
50207
+ * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
50208
+ * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
50209
+ * roles de plataforma) y los roles personalizados de la org activa, con el
50210
+ * catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
50211
+ * Promovida desde `showcase` bajo el proceso de ADR-021.
50038
50212
  *
50039
- * Recibe la lista de candidatos por `members` (todos excepto el owner actual),
50040
- * permite filtrarlos con `val-searchbar` y, al confirmar, ejecuta el callback
50041
- * `onConfirm(newOwnerId)`. La ejecución real de la transferencia (vía
50042
- * `OrgService.transferOwnership`) queda en el consumer.
50213
+ * **RBAC interno** (como `organization-view`): lee la org activa vía
50214
+ * `AuthService.user().activeOrgId`. La vista NO muta nada el control de quién
50215
+ * puede verla es responsabilidad del guard de ruta del consumer; no hay gating
50216
+ * fino interno ni se expone como config.
50043
50217
  *
50044
- * Abierto vía `ModalService` recibe `_modalRef` por `componentProps`. Header
50045
- * canónico (Regla #5): sin `ion-title`, botón Cerrar texto en `slot=end`,
50046
- * título/subtítulo en el body con `val-display`/`val-title`.
50218
+ * Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
50219
+ * `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
50220
+ * catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
50047
50221
  *
50048
- * Auto-registra sus defaults i18n (es/en) si el consumer no proveyó el namespace
50049
- * `Settings.TransferModal`.
50222
+ * NO renderiza ion-content vive dentro de val-page-wrapper. `ActivatedRoute` se
50223
+ * inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
50224
+ * sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
50225
+ * `Settings.Permissions`).
50050
50226
  */
50051
- class TransferOwnershipModalComponent {
50227
+ class PermissionsViewComponent {
50228
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
50229
+ get ns() {
50230
+ return this.resolvedConfig().i18nNamespace;
50231
+ }
50052
50232
  constructor() {
50053
50233
  this.i18n = inject(I18nService);
50054
- /** Lista de miembros transferibles (todos excepto el owner actual). */
50055
- this.members = [];
50056
- /** Namespace i18n con que la vista resuelve sus textos. */
50057
- this.i18nNamespace = DEFAULT_NAMESPACE$9;
50058
- this.query = signal('');
50059
- this.selectedId = signal('');
50060
- this.filtered = computed(() => {
50061
- const q = this.query().trim().toLowerCase();
50062
- if (!q)
50063
- return this.members;
50064
- return this.members.filter(m => m.name?.toLowerCase().includes(q) || m.email?.toLowerCase().includes(q) || m.userId.toLowerCase().includes(q));
50234
+ this.orgService = inject(OrgService);
50235
+ this.catalog = inject(PermissionCatalogService);
50236
+ this.auth = inject(AuthService);
50237
+ this.nav = inject(NavigationService);
50238
+ this.route = inject(ActivatedRoute, { optional: true });
50239
+ /**
50240
+ * Cuando `true`, la vista está embebida en un modal — suprime el back-header
50241
+ * (lo gestiona el modal padre). Default `false` (uso como ruta).
50242
+ */
50243
+ this.isModal = false;
50244
+ this.resolvedConfig = computed(() => {
50245
+ const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
50246
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
50247
+ return {
50248
+ showSystemRoles: merged.showSystemRoles ?? true,
50249
+ showCustomRoles: merged.showCustomRoles ?? true,
50250
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$9,
50251
+ };
50065
50252
  });
50066
- this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
50067
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
50068
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, TRANSFER_OWNERSHIP_MODAL_I18N);
50253
+ this.loading = signal(false);
50254
+ this.loadError = signal(null);
50255
+ this.roles = signal([]);
50256
+ this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
50257
+ this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
50258
+ this.activeOrgId = computed(() => {
50259
+ const u = this.auth.user();
50260
+ return u?.activeOrgId ?? u?.activeOrg ?? '';
50261
+ });
50262
+ this.retryButtonProps = computed(() => ({
50263
+ text: this.tt('retry'),
50264
+ color: 'dark',
50265
+ fill: 'outline',
50266
+ size: 'default',
50267
+ type: 'button',
50268
+ state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
50269
+ }));
50270
+ const ns = this.ns;
50271
+ if (!this.i18n.hasNamespace(ns)) {
50272
+ this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
50069
50273
  }
50274
+ this.load();
50275
+ connectPageRefresh(() => this.load());
50070
50276
  }
50071
50277
  ngOnInit() {
50072
- if (!this.i18n.hasNamespace(this.i18nNamespace)) {
50073
- this.i18n.registerContent(this.i18nNamespace, TRANSFER_OWNERSHIP_MODAL_I18N);
50278
+ if (!this.isModal) {
50279
+ this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
50074
50280
  }
50075
50281
  }
50076
- confirm() {
50077
- const id = this.selectedId();
50078
- if (!id)
50282
+ load() {
50283
+ const orgId = this.activeOrgId();
50284
+ if (!orgId)
50079
50285
  return;
50080
- this.onConfirm?.(id);
50081
- this.dismiss();
50286
+ this.loading.set(true);
50287
+ this.loadError.set(null);
50288
+ forkJoin({
50289
+ roles: this.orgService.listOrgRoles(orgId),
50290
+ catalog: this.catalog.getCatalog(orgId),
50291
+ }).subscribe({
50292
+ next: ({ roles: rawRoles, catalog }) => {
50293
+ const byResource = new Map();
50294
+ for (const r of catalog.permissions)
50295
+ byResource.set(r.resource, r);
50296
+ const currentAppId = catalog.appId;
50297
+ // Labeler con la SSOT del backend (labels del catálogo) — reemplaza el
50298
+ // mapa hardcodeado `perm*`. Locale del momento de carga (igual que antes:
50299
+ // los labels se hornean en el RoleView; cambio de idioma requiere reload).
50300
+ const labeler = createPermissionLabeler(catalog, this.i18n.lang(), {
50301
+ allLabel: this.tt('permAll'),
50302
+ });
50303
+ this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
50304
+ id: r.id,
50305
+ name: r.name,
50306
+ description: r.description,
50307
+ permissions: r.permissions ?? [],
50308
+ isSystem: r.isSystem ?? false,
50309
+ }, byResource, currentAppId, labeler)));
50310
+ this.loading.set(false);
50311
+ },
50312
+ error: err => {
50313
+ this.loadError.set(err);
50314
+ this.loading.set(false);
50315
+ },
50316
+ });
50082
50317
  }
50083
- dismiss() {
50084
- this._modalRef?.dismiss(null, 'cancel');
50318
+ buildRoleView(role, byResource, currentAppId, labeler) {
50319
+ const appPerms = [];
50320
+ const orgPerms = [];
50321
+ const otherByApp = new Map();
50322
+ for (const perm of role.permissions) {
50323
+ const [resource] = perm.split(':');
50324
+ const entry = byResource.get(resource);
50325
+ // Sin entrada en el catálogo (el backend omite los recursos internal para
50326
+ // no-platform-admins) o entrada internal → NUNCA se muestra a nivel de
50327
+ // producto. Defensa en profundidad junto al filtro del backend (ADR-024).
50328
+ // Nota: el rol puede traer el permiso aunque el recurso esté filtrado del
50329
+ // catálogo (ej. admin tiene templates:*); por eso se descarta lo no resuelto.
50330
+ if (!entry || entry.scope === 'internal') {
50331
+ continue;
50332
+ }
50333
+ const label = labeler(perm);
50334
+ if (entry.scope === 'app') {
50335
+ if (entry.appId === currentAppId) {
50336
+ appPerms.push(label);
50337
+ }
50338
+ else {
50339
+ const appId = entry.appId ?? '?';
50340
+ const arr = otherByApp.get(appId) ?? [];
50341
+ arr.push(label);
50342
+ otherByApp.set(appId, arr);
50343
+ }
50344
+ }
50345
+ else {
50346
+ // scope === 'org' → gestión de la organización del cliente
50347
+ orgPerms.push(label);
50348
+ }
50349
+ }
50350
+ const groups = [];
50351
+ if (appPerms.length) {
50352
+ groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
50353
+ }
50354
+ if (orgPerms.length) {
50355
+ groups.push({
50356
+ key: 'org',
50357
+ label: this.tt('groupOrg'),
50358
+ perms: orgPerms,
50359
+ });
50360
+ }
50361
+ for (const [appId, perms] of otherByApp) {
50362
+ groups.push({
50363
+ key: `other:${appId}`,
50364
+ label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
50365
+ perms,
50366
+ });
50367
+ }
50368
+ return {
50369
+ id: role.id,
50370
+ name: role.name,
50371
+ description: role.description,
50372
+ isSystem: role.isSystem,
50373
+ groups,
50374
+ hasPerms: groups.length > 0,
50375
+ };
50085
50376
  }
50086
- t(key) {
50087
- return this.i18n.t(key, this.i18nNamespace);
50377
+ tt(key) {
50378
+ return this.i18n.t(key, this.ns);
50088
50379
  }
50089
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
50090
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TransferOwnershipModalComponent, isStandalone: true, selector: "val-transfer-ownership-modal", inputs: { members: "members", onConfirm: "onConfirm", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
50091
- <ion-header>
50092
- <ion-toolbar>
50093
- <ion-buttons slot="end">
50094
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
50095
- <strong>{{ t('close') }}</strong>
50096
- </ion-button>
50097
- </ion-buttons>
50098
- </ion-toolbar>
50099
- </ion-header>
50100
-
50101
- <ion-content class="ion-padding">
50102
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
50103
- <val-title
50104
- [props]="{
50105
- content: t('hint'),
50106
- size: 'large',
50107
- color: '',
50108
- bold: false,
50109
- }"
50110
- />
50111
-
50112
- <val-searchbar
50113
- [props]="{ placeholder: t('searchPlaceholder'), debounce: 150 }"
50114
- (filterEvent)="query.set($event)"
50115
- />
50116
-
50117
- @if (members.length === 0) {
50118
- <val-text
50119
- [props]="{
50120
- content: t('empty'),
50121
- size: 'medium',
50122
- color: 'medium',
50123
- bold: false,
50124
- }"
50125
- />
50126
- } @else if (filtered().length === 0) {
50127
- <val-text
50380
+ appLabel(appId) {
50381
+ const key = `app_${appId}`;
50382
+ const label = this.tt(key);
50383
+ return label === key ? appId : label;
50384
+ }
50385
+ roleLabel(name) {
50386
+ const map = {
50387
+ viewer: 'roleViewer',
50388
+ editor: 'roleEditor',
50389
+ admin: 'roleAdmin',
50390
+ super_admin: 'roleSuperAdmin',
50391
+ bingo_admin: 'roleBingoAdmin',
50392
+ bingo_tesorero: 'roleBingoTesorero',
50393
+ bingo_taquilla: 'roleBingoTaquilla',
50394
+ bingo_operator: 'roleBingoOperator',
50395
+ };
50396
+ return this.tt(map[name] ?? name);
50397
+ }
50398
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
50399
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config", isModal: "isModal" }, ngImport: i0, template: `
50400
+ <div class="page">
50401
+ <header class="page-header">
50402
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
50403
+ <val-title
50128
50404
  [props]="{
50129
- content: noResultsLabel(),
50130
- size: 'medium',
50131
- color: 'medium',
50405
+ size: 'large',
50406
+ color: 'dark',
50132
50407
  bold: false,
50408
+ content: tt('pageDescription'),
50133
50409
  }"
50134
50410
  />
50411
+ </header>
50412
+
50413
+ @if (loading()) {
50414
+ <div class="spinner-row">
50415
+ <ion-spinner name="crescent" />
50416
+ </div>
50417
+ } @else if (loadError()) {
50418
+ <div class="error-row">
50419
+ <val-text
50420
+ [props]="{
50421
+ content: tt('errorTitle'),
50422
+ size: 'medium',
50423
+ color: 'medium',
50424
+ bold: false,
50425
+ }"
50426
+ />
50427
+ <val-button [props]="retryButtonProps()" (click)="load()" />
50428
+ </div>
50135
50429
  } @else {
50136
- <div class="transfer-member-list">
50137
- @for (m of filtered(); track m.userId) {
50138
- <button
50139
- class="transfer-member-row"
50140
- [class.transfer-member-row--selected]="selectedId() === m.userId"
50141
- (click)="selectedId.set(m.userId)"
50142
- type="button"
50143
- >
50144
- <span class="transfer-member-name">{{ m.name || m.email || m.userId }}</span>
50145
- @if (selectedId() === m.userId) {
50146
- <span class="transfer-member-check">&#10003;</span>
50430
+ @if (resolvedConfig().showSystemRoles) {
50431
+ <section class="perm-section">
50432
+ <div class="perm-section__header">
50433
+ <val-title
50434
+ [props]="{
50435
+ size: 'medium',
50436
+ color: 'dark',
50437
+ bold: true,
50438
+ content: tt('systemRolesTitle'),
50439
+ }"
50440
+ />
50441
+ <val-text
50442
+ [props]="{
50443
+ content: tt('systemRolesHint'),
50444
+ size: 'medium',
50445
+ color: 'medium',
50446
+ bold: false,
50447
+ }"
50448
+ />
50449
+ </div>
50450
+ <div class="role-list">
50451
+ @for (role of systemRoles(); track role.id) {
50452
+ <div class="role-card">
50453
+ <div class="role-card__header">
50454
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
50455
+ <val-pill
50456
+ [props]="{
50457
+ label: tt('systemBadge'),
50458
+ color: 'primary',
50459
+ weight: 'bold',
50460
+ }"
50461
+ />
50462
+ </div>
50463
+ @if (role.description) {
50464
+ <span class="role-card__desc">{{ role.description }}</span>
50465
+ }
50466
+ @if (!role.hasPerms) {
50467
+ <div class="perm-chips">
50468
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
50469
+ </div>
50470
+ } @else {
50471
+ @for (g of role.groups; track g.key) {
50472
+ <div class="perm-group">
50473
+ <span class="perm-group__label">{{ g.label }}</span>
50474
+ <div class="perm-chips">
50475
+ @for (p of g.perms; track p) {
50476
+ <span class="perm-chip">{{ p }}</span>
50477
+ }
50478
+ </div>
50479
+ </div>
50480
+ }
50481
+ }
50482
+ </div>
50147
50483
  }
50148
- </button>
50149
- }
50150
- </div>
50151
- }
50152
-
50153
- <div class="transfer-confirm-row">
50154
- <ion-button expand="block" color="dark" shape="round" [disabled]="!selectedId()" (click)="confirm()">
50155
- {{ t('confirmCta') }}
50156
- </ion-button>
50157
- </div>
50158
-
50159
- <div class="modal-close-bottom">
50160
- <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
50161
- {{ t('close') }}
50162
- </ion-button>
50163
- </div>
50164
- </ion-content>
50165
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:16px}val-searchbar{display:block;margin-bottom:12px}.transfer-member-list{display:flex;flex-direction:column;gap:4px}.transfer-member-row{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-radius:10px;border:1.5px solid var(--ion-color-light);background:transparent;cursor:pointer;width:100%;text-align:left;font-size:15px;color:var(--ion-color-dark);transition:border-color .15s}.transfer-member-row--selected{border-color:var(--ion-color-dark);background:var(--ion-color-light)}.transfer-member-check{font-size:17px;color:var(--ion-color-dark)}.transfer-confirm-row{margin-top:24px}.modal-close-bottom{margin-top:12px}\n"], dependencies: [{ kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
50166
- }
50167
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, decorators: [{
50168
- type: Component,
50169
- args: [{ selector: 'val-transfer-ownership-modal', standalone: true, imports: [
50170
- IonHeader,
50171
- IonToolbar,
50172
- IonButtons,
50173
- IonButton,
50174
- IonContent,
50175
- SearchbarComponent,
50176
- DisplayComponent,
50177
- TextComponent,
50178
- TitleComponent,
50179
- ], template: `
50180
- <ion-header>
50181
- <ion-toolbar>
50182
- <ion-buttons slot="end">
50183
- <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
50184
- <strong>{{ t('close') }}</strong>
50185
- </ion-button>
50186
- </ion-buttons>
50187
- </ion-toolbar>
50188
- </ion-header>
50484
+ </div>
50485
+ </section>
50486
+ }
50487
+
50488
+ @if (resolvedConfig().showCustomRoles) {
50489
+ <section class="perm-section">
50490
+ <div class="perm-section__header">
50491
+ <val-title
50492
+ [props]="{
50493
+ size: 'medium',
50494
+ color: 'dark',
50495
+ bold: true,
50496
+ content: tt('customRolesTitle'),
50497
+ }"
50498
+ />
50499
+ <val-text
50500
+ [props]="{
50501
+ content: tt('customRolesHint'),
50502
+ size: 'medium',
50503
+ color: 'medium',
50504
+ bold: false,
50505
+ }"
50506
+ />
50507
+ </div>
50508
+ @if (customRoles().length === 0) {
50509
+ <val-text
50510
+ [props]="{
50511
+ content: tt('noCustomRoles'),
50512
+ size: 'medium',
50513
+ color: 'medium',
50514
+ bold: false,
50515
+ }"
50516
+ />
50517
+ } @else {
50518
+ <div class="role-list">
50519
+ @for (role of customRoles(); track role.id) {
50520
+ <div class="role-card">
50521
+ <div class="role-card__header">
50522
+ <span class="role-card__name">{{ role.name }}</span>
50523
+ </div>
50524
+ @if (role.description) {
50525
+ <span class="role-card__desc">{{ role.description }}</span>
50526
+ }
50527
+ @if (!role.hasPerms) {
50528
+ <div class="perm-chips">
50529
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
50530
+ </div>
50531
+ } @else {
50532
+ @for (g of role.groups; track g.key) {
50533
+ <div class="perm-group">
50534
+ <span class="perm-group__label">{{ g.label }}</span>
50535
+ <div class="perm-chips">
50536
+ @for (p of g.perms; track p) {
50537
+ <span class="perm-chip">{{ p }}</span>
50538
+ }
50539
+ </div>
50540
+ </div>
50541
+ }
50542
+ }
50543
+ </div>
50544
+ }
50545
+ </div>
50546
+ }
50547
+ </section>
50548
+ }
50549
+ }
50550
+ </div>
50551
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:.9375rem;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:.8125rem;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:.75rem;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
50552
+ }
50553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
50554
+ type: Component,
50555
+ args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
50556
+ <div class="page">
50557
+ <header class="page-header">
50558
+ <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
50559
+ <val-title
50560
+ [props]="{
50561
+ size: 'large',
50562
+ color: 'dark',
50563
+ bold: false,
50564
+ content: tt('pageDescription'),
50565
+ }"
50566
+ />
50567
+ </header>
50568
+
50569
+ @if (loading()) {
50570
+ <div class="spinner-row">
50571
+ <ion-spinner name="crescent" />
50572
+ </div>
50573
+ } @else if (loadError()) {
50574
+ <div class="error-row">
50575
+ <val-text
50576
+ [props]="{
50577
+ content: tt('errorTitle'),
50578
+ size: 'medium',
50579
+ color: 'medium',
50580
+ bold: false,
50581
+ }"
50582
+ />
50583
+ <val-button [props]="retryButtonProps()" (click)="load()" />
50584
+ </div>
50585
+ } @else {
50586
+ @if (resolvedConfig().showSystemRoles) {
50587
+ <section class="perm-section">
50588
+ <div class="perm-section__header">
50589
+ <val-title
50590
+ [props]="{
50591
+ size: 'medium',
50592
+ color: 'dark',
50593
+ bold: true,
50594
+ content: tt('systemRolesTitle'),
50595
+ }"
50596
+ />
50597
+ <val-text
50598
+ [props]="{
50599
+ content: tt('systemRolesHint'),
50600
+ size: 'medium',
50601
+ color: 'medium',
50602
+ bold: false,
50603
+ }"
50604
+ />
50605
+ </div>
50606
+ <div class="role-list">
50607
+ @for (role of systemRoles(); track role.id) {
50608
+ <div class="role-card">
50609
+ <div class="role-card__header">
50610
+ <span class="role-card__name">{{ roleLabel(role.name) }}</span>
50611
+ <val-pill
50612
+ [props]="{
50613
+ label: tt('systemBadge'),
50614
+ color: 'primary',
50615
+ weight: 'bold',
50616
+ }"
50617
+ />
50618
+ </div>
50619
+ @if (role.description) {
50620
+ <span class="role-card__desc">{{ role.description }}</span>
50621
+ }
50622
+ @if (!role.hasPerms) {
50623
+ <div class="perm-chips">
50624
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
50625
+ </div>
50626
+ } @else {
50627
+ @for (g of role.groups; track g.key) {
50628
+ <div class="perm-group">
50629
+ <span class="perm-group__label">{{ g.label }}</span>
50630
+ <div class="perm-chips">
50631
+ @for (p of g.perms; track p) {
50632
+ <span class="perm-chip">{{ p }}</span>
50633
+ }
50634
+ </div>
50635
+ </div>
50636
+ }
50637
+ }
50638
+ </div>
50639
+ }
50640
+ </div>
50641
+ </section>
50642
+ }
50643
+
50644
+ @if (resolvedConfig().showCustomRoles) {
50645
+ <section class="perm-section">
50646
+ <div class="perm-section__header">
50647
+ <val-title
50648
+ [props]="{
50649
+ size: 'medium',
50650
+ color: 'dark',
50651
+ bold: true,
50652
+ content: tt('customRolesTitle'),
50653
+ }"
50654
+ />
50655
+ <val-text
50656
+ [props]="{
50657
+ content: tt('customRolesHint'),
50658
+ size: 'medium',
50659
+ color: 'medium',
50660
+ bold: false,
50661
+ }"
50662
+ />
50663
+ </div>
50664
+ @if (customRoles().length === 0) {
50665
+ <val-text
50666
+ [props]="{
50667
+ content: tt('noCustomRoles'),
50668
+ size: 'medium',
50669
+ color: 'medium',
50670
+ bold: false,
50671
+ }"
50672
+ />
50673
+ } @else {
50674
+ <div class="role-list">
50675
+ @for (role of customRoles(); track role.id) {
50676
+ <div class="role-card">
50677
+ <div class="role-card__header">
50678
+ <span class="role-card__name">{{ role.name }}</span>
50679
+ </div>
50680
+ @if (role.description) {
50681
+ <span class="role-card__desc">{{ role.description }}</span>
50682
+ }
50683
+ @if (!role.hasPerms) {
50684
+ <div class="perm-chips">
50685
+ <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
50686
+ </div>
50687
+ } @else {
50688
+ @for (g of role.groups; track g.key) {
50689
+ <div class="perm-group">
50690
+ <span class="perm-group__label">{{ g.label }}</span>
50691
+ <div class="perm-chips">
50692
+ @for (p of g.perms; track p) {
50693
+ <span class="perm-chip">{{ p }}</span>
50694
+ }
50695
+ </div>
50696
+ </div>
50697
+ }
50698
+ }
50699
+ </div>
50700
+ }
50701
+ </div>
50702
+ }
50703
+ </section>
50704
+ }
50705
+ }
50706
+ </div>
50707
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:.9375rem;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:.8125rem;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:.75rem;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
50708
+ }], ctorParameters: () => [], propDecorators: { config: [{
50709
+ type: Input
50710
+ }], isModal: [{
50711
+ type: Input
50712
+ }] } });
50713
+
50714
+ class PermissionsModalComponent {
50715
+ constructor() {
50716
+ this.i18n = inject(I18nService);
50717
+ this.isOpen = false;
50718
+ this.dismissed = new EventEmitter();
50719
+ }
50720
+ close() {
50721
+ this.dismissed.emit();
50722
+ }
50723
+ t(key) {
50724
+ return this.i18n.t(key, '_global');
50725
+ }
50726
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
50727
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: PermissionsModalComponent, isStandalone: true, selector: "val-permissions-modal", inputs: { isOpen: "isOpen", config: "config" }, outputs: { dismissed: "dismissed" }, ngImport: i0, template: `
50728
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
50729
+ <ng-template>
50730
+ <ion-header>
50731
+ <ion-toolbar>
50732
+ <ion-buttons slot="end">
50733
+ <ion-button fill="clear" color="dark" shape="round" (click)="close()">
50734
+ <strong>{{ t('close') }}</strong>
50735
+ </ion-button>
50736
+ </ion-buttons>
50737
+ </ion-toolbar>
50738
+ </ion-header>
50739
+ <ion-content class="ion-padding">
50740
+ <val-permissions-view [config]="config" [isModal]="true" />
50741
+ </ion-content>
50742
+ </ng-template>
50743
+ </ion-modal>
50744
+ `, isInline: true, dependencies: [{ kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: PermissionsViewComponent, selector: "val-permissions-view", inputs: ["config", "isModal"] }] }); }
50745
+ }
50746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsModalComponent, decorators: [{
50747
+ type: Component,
50748
+ args: [{
50749
+ selector: 'val-permissions-modal',
50750
+ standalone: true,
50751
+ imports: [IonModal, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, PermissionsViewComponent],
50752
+ template: `
50753
+ <ion-modal [isOpen]="isOpen" (didDismiss)="close()" cssClass="val-modal-adaptive">
50754
+ <ng-template>
50755
+ <ion-header>
50756
+ <ion-toolbar>
50757
+ <ion-buttons slot="end">
50758
+ <ion-button fill="clear" color="dark" shape="round" (click)="close()">
50759
+ <strong>{{ t('close') }}</strong>
50760
+ </ion-button>
50761
+ </ion-buttons>
50762
+ </ion-toolbar>
50763
+ </ion-header>
50764
+ <ion-content class="ion-padding">
50765
+ <val-permissions-view [config]="config" [isModal]="true" />
50766
+ </ion-content>
50767
+ </ng-template>
50768
+ </ion-modal>
50769
+ `,
50770
+ }]
50771
+ }], propDecorators: { isOpen: [{
50772
+ type: Input
50773
+ }], config: [{
50774
+ type: Input
50775
+ }], dismissed: [{
50776
+ type: Output
50777
+ }] } });
50778
+
50779
+ /**
50780
+ * Defaults i18n (es/en) embebidos en `val-transfer-ownership-modal`.
50781
+ * Auto-registrados en el constructor del componente si el consumer no proveyó el
50782
+ * namespace (`Settings.TransferModal` por default). SOLO incluye las keys que el
50783
+ * modal usa.
50784
+ */
50785
+ const TRANSFER_OWNERSHIP_MODAL_I18N = {
50786
+ es: {
50787
+ title: 'Transferir propiedad',
50788
+ hint: 'Selecciona el nuevo propietario de la organización.',
50789
+ searchPlaceholder: 'Buscar miembro...',
50790
+ empty: 'No hay otros miembros disponibles.',
50791
+ noResults: 'Sin resultados para',
50792
+ confirmCta: 'Confirmar transferencia',
50793
+ close: 'Cancelar',
50794
+ },
50795
+ en: {
50796
+ title: 'Transfer ownership',
50797
+ hint: 'Select the new owner of the organization.',
50798
+ searchPlaceholder: 'Search member...',
50799
+ empty: 'No other members available.',
50800
+ noResults: 'No results for',
50801
+ confirmCta: 'Confirm transfer',
50802
+ close: 'Cancel',
50803
+ },
50804
+ };
50805
+
50806
+ const DEFAULT_NAMESPACE$8 = 'Settings.TransferModal';
50807
+ /**
50808
+ * `val-transfer-ownership-modal` — selector de miembro para transferir la
50809
+ * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
50810
+ * ADR-021.
50811
+ *
50812
+ * Recibe la lista de candidatos por `members` (todos excepto el owner actual),
50813
+ * permite filtrarlos con `val-searchbar` y, al confirmar, ejecuta el callback
50814
+ * `onConfirm(newOwnerId)`. La ejecución real de la transferencia (vía
50815
+ * `OrgService.transferOwnership`) queda en el consumer.
50816
+ *
50817
+ * Abierto vía `ModalService` — recibe `_modalRef` por `componentProps`. Header
50818
+ * canónico (Regla #5): sin `ion-title`, botón Cerrar texto en `slot=end`,
50819
+ * título/subtítulo en el body con `val-display`/`val-title`.
50820
+ *
50821
+ * Auto-registra sus defaults i18n (es/en) si el consumer no proveyó el namespace
50822
+ * `Settings.TransferModal`.
50823
+ */
50824
+ class TransferOwnershipModalComponent {
50825
+ constructor() {
50826
+ this.i18n = inject(I18nService);
50827
+ /** Lista de miembros transferibles (todos excepto el owner actual). */
50828
+ this.members = [];
50829
+ /** Namespace i18n con que la vista resuelve sus textos. */
50830
+ this.i18nNamespace = DEFAULT_NAMESPACE$8;
50831
+ this.query = signal('');
50832
+ this.selectedId = signal('');
50833
+ this.filtered = computed(() => {
50834
+ const q = this.query().trim().toLowerCase();
50835
+ if (!q)
50836
+ return this.members;
50837
+ return this.members.filter(m => m.name?.toLowerCase().includes(q) || m.email?.toLowerCase().includes(q) || m.userId.toLowerCase().includes(q));
50838
+ });
50839
+ this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
50840
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
50841
+ this.i18n.registerContent(DEFAULT_NAMESPACE$8, TRANSFER_OWNERSHIP_MODAL_I18N);
50842
+ }
50843
+ }
50844
+ ngOnInit() {
50845
+ if (!this.i18n.hasNamespace(this.i18nNamespace)) {
50846
+ this.i18n.registerContent(this.i18nNamespace, TRANSFER_OWNERSHIP_MODAL_I18N);
50847
+ }
50848
+ }
50849
+ confirm() {
50850
+ const id = this.selectedId();
50851
+ if (!id)
50852
+ return;
50853
+ this.onConfirm?.(id);
50854
+ this.dismiss();
50855
+ }
50856
+ dismiss() {
50857
+ this._modalRef?.dismiss(null, 'cancel');
50858
+ }
50859
+ t(key) {
50860
+ return this.i18n.t(key, this.i18nNamespace);
50861
+ }
50862
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
50863
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TransferOwnershipModalComponent, isStandalone: true, selector: "val-transfer-ownership-modal", inputs: { members: "members", onConfirm: "onConfirm", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
50864
+ <ion-header>
50865
+ <ion-toolbar>
50866
+ <ion-buttons slot="end">
50867
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
50868
+ <strong>{{ t('close') }}</strong>
50869
+ </ion-button>
50870
+ </ion-buttons>
50871
+ </ion-toolbar>
50872
+ </ion-header>
50873
+
50874
+ <ion-content class="ion-padding">
50875
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
50876
+ <val-title
50877
+ [props]="{
50878
+ content: t('hint'),
50879
+ size: 'large',
50880
+ color: '',
50881
+ bold: false,
50882
+ }"
50883
+ />
50884
+
50885
+ <val-searchbar
50886
+ [props]="{ placeholder: t('searchPlaceholder'), debounce: 150 }"
50887
+ (filterEvent)="query.set($event)"
50888
+ />
50889
+
50890
+ @if (members.length === 0) {
50891
+ <val-text
50892
+ [props]="{
50893
+ content: t('empty'),
50894
+ size: 'medium',
50895
+ color: 'medium',
50896
+ bold: false,
50897
+ }"
50898
+ />
50899
+ } @else if (filtered().length === 0) {
50900
+ <val-text
50901
+ [props]="{
50902
+ content: noResultsLabel(),
50903
+ size: 'medium',
50904
+ color: 'medium',
50905
+ bold: false,
50906
+ }"
50907
+ />
50908
+ } @else {
50909
+ <div class="transfer-member-list">
50910
+ @for (m of filtered(); track m.userId) {
50911
+ <button
50912
+ class="transfer-member-row"
50913
+ [class.transfer-member-row--selected]="selectedId() === m.userId"
50914
+ (click)="selectedId.set(m.userId)"
50915
+ type="button"
50916
+ >
50917
+ <span class="transfer-member-name">{{ m.name || m.email || m.userId }}</span>
50918
+ @if (selectedId() === m.userId) {
50919
+ <span class="transfer-member-check">&#10003;</span>
50920
+ }
50921
+ </button>
50922
+ }
50923
+ </div>
50924
+ }
50925
+
50926
+ <div class="transfer-confirm-row">
50927
+ <ion-button expand="block" color="dark" shape="round" [disabled]="!selectedId()" (click)="confirm()">
50928
+ {{ t('confirmCta') }}
50929
+ </ion-button>
50930
+ </div>
50931
+
50932
+ <div class="modal-close-bottom">
50933
+ <ion-button expand="block" fill="outline" color="dark" shape="round" (click)="dismiss()">
50934
+ {{ t('close') }}
50935
+ </ion-button>
50936
+ </div>
50937
+ </ion-content>
50938
+ `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:16px}val-searchbar{display:block;margin-bottom:12px}.transfer-member-list{display:flex;flex-direction:column;gap:4px}.transfer-member-row{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-radius:10px;border:1.5px solid var(--ion-color-light);background:transparent;cursor:pointer;width:100%;text-align:left;font-size:15px;color:var(--ion-color-dark);transition:border-color .15s}.transfer-member-row--selected{border-color:var(--ion-color-dark);background:var(--ion-color-light)}.transfer-member-check{font-size:17px;color:var(--ion-color-dark)}.transfer-confirm-row{margin-top:24px}.modal-close-bottom{margin-top:12px}\n"], dependencies: [{ kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { 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: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }] }); }
50939
+ }
50940
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TransferOwnershipModalComponent, decorators: [{
50941
+ type: Component,
50942
+ args: [{ selector: 'val-transfer-ownership-modal', standalone: true, imports: [
50943
+ IonHeader,
50944
+ IonToolbar,
50945
+ IonButtons,
50946
+ IonButton,
50947
+ IonContent,
50948
+ SearchbarComponent,
50949
+ DisplayComponent,
50950
+ TextComponent,
50951
+ TitleComponent,
50952
+ ], template: `
50953
+ <ion-header>
50954
+ <ion-toolbar>
50955
+ <ion-buttons slot="end">
50956
+ <ion-button fill="clear" color="dark" shape="round" (click)="dismiss()">
50957
+ <strong>{{ t('close') }}</strong>
50958
+ </ion-button>
50959
+ </ion-buttons>
50960
+ </ion-toolbar>
50961
+ </ion-header>
50189
50962
 
50190
50963
  <ion-content class="ion-padding">
50191
50964
  <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
@@ -50341,7 +51114,7 @@ const MEMBER_IMPORT_MODAL_I18N = {
50341
51114
  },
50342
51115
  };
50343
51116
 
50344
- const DEFAULT_NAMESPACE$8 = 'Settings.ImportModal';
51117
+ const DEFAULT_NAMESPACE$7 = 'Settings.ImportModal';
50345
51118
  addIcons({ cloudUploadOutline, documentTextOutline });
50346
51119
  /**
50347
51120
  * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
@@ -50361,7 +51134,7 @@ class MemberImportModalComponent {
50361
51134
  /** Nombres de roles disponibles (para el hint). Opcional. */
50362
51135
  this.availableRoles = [];
50363
51136
  /** Namespace i18n. */
50364
- this.i18nNamespace = DEFAULT_NAMESPACE$8;
51137
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
50365
51138
  this.csv = '';
50366
51139
  this.importMode = signal('file');
50367
51140
  this.onConflictControl = new FormControl('assignRole');
@@ -50403,8 +51176,8 @@ class MemberImportModalComponent {
50403
51176
  return this.t('summary');
50404
51177
  return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
50405
51178
  });
50406
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
50407
- this.i18n.registerContent(DEFAULT_NAMESPACE$8, MEMBER_IMPORT_MODAL_I18N);
51179
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
51180
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, MEMBER_IMPORT_MODAL_I18N);
50408
51181
  }
50409
51182
  }
50410
51183
  ngOnInit() {
@@ -50996,7 +51769,7 @@ const ORGANIZATION_VIEW_I18N = {
50996
51769
  },
50997
51770
  };
50998
51771
 
50999
- const DEFAULT_NAMESPACE$7 = 'Settings.Organization';
51772
+ const DEFAULT_NAMESPACE$6 = 'Settings.Organization';
51000
51773
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
51001
51774
  const DEFAULT_API_KEYS_ROUTE = '/app/settings/api-keys';
51002
51775
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
@@ -51054,7 +51827,7 @@ class OrganizationViewComponent {
51054
51827
  showInvite: merged.showInvite ?? true,
51055
51828
  showTransferOwnership: merged.showTransferOwnership ?? true,
51056
51829
  showLeave: merged.showLeave ?? true,
51057
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$7,
51830
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
51058
51831
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
51059
51832
  apiKeysRoute: merged.apiKeysRoute ?? DEFAULT_API_KEYS_ROUTE,
51060
51833
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
@@ -51067,6 +51840,7 @@ class OrganizationViewComponent {
51067
51840
  this.org = signal(null);
51068
51841
  this.loading = signal(false);
51069
51842
  this.leaving = signal(false);
51843
+ this.permissionsOpen = signal(false);
51070
51844
  this.orgLoadError = signal(null);
51071
51845
  this.orgErrorState = computed(() => {
51072
51846
  this.i18n.lang();
@@ -51365,7 +52139,7 @@ class OrganizationViewComponent {
51365
52139
  });
51366
52140
  }
51367
52141
  onViewPermissions() {
51368
- this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
52142
+ this.permissionsOpen.set(true);
51369
52143
  }
51370
52144
  onOpenTransfer() {
51371
52145
  void this.modalService.openAdaptive({
@@ -51838,8 +52612,13 @@ class OrganizationViewComponent {
51838
52612
  </div>
51839
52613
  </section>
51840
52614
  }
52615
+ <val-permissions-modal
52616
+ [isOpen]="permissionsOpen()"
52617
+ [config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
52618
+ (dismissed)="permissionsOpen.set(false)"
52619
+ />
51841
52620
  </div>
51842
- `, 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}: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)}\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: 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"] }] }); }
52621
+ `, 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}: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)}\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: 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"] }] }); }
51843
52622
  }
51844
52623
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
51845
52624
  type: Component,
@@ -51852,6 +52631,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
51852
52631
  TextComponent,
51853
52632
  ButtonComponent,
51854
52633
  MemberCardComponent,
52634
+ PermissionsModalComponent,
51855
52635
  TransferOwnershipModalComponent,
51856
52636
  MemberImportModalComponent,
51857
52637
  OrgInfoSheetComponent,
@@ -52142,6 +52922,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
52142
52922
  </div>
52143
52923
  </section>
52144
52924
  }
52925
+ <val-permissions-modal
52926
+ [isOpen]="permissionsOpen()"
52927
+ [config]="{ i18nNamespace: resolvedConfig().i18nNamespace }"
52928
+ (dismissed)="permissionsOpen.set(false)"
52929
+ />
52145
52930
  </div>
52146
52931
  `, 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}: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)}\n"] }]
52147
52932
  }], ctorParameters: () => [], propDecorators: { config: [{
@@ -52257,7 +53042,7 @@ addIcons({
52257
53042
  notificationsOutline,
52258
53043
  refreshOutline,
52259
53044
  });
52260
- const DEFAULT_NAMESPACE$6 = 'Settings.Notifications';
53045
+ const DEFAULT_NAMESPACE$5 = 'Settings.Notifications';
52261
53046
  /**
52262
53047
  * `val-notification-preferences-view` — **preferencias de notificaciones push**
52263
53048
  * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
@@ -52300,7 +53085,7 @@ class NotificationPreferencesViewComponent {
52300
53085
  const merged = { ...fromRoute, ...(this.config ?? {}) };
52301
53086
  return {
52302
53087
  showDetails: merged.showDetails ?? true,
52303
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
53088
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$5,
52304
53089
  onPushEnabled: merged.onPushEnabled,
52305
53090
  onPushDisabled: merged.onPushDisabled,
52306
53091
  };
@@ -52924,622 +53709,6 @@ function provideValtechNotificationPreferencesRoutes(opts) {
52924
53709
  ];
52925
53710
  }
52926
53711
 
52927
- /**
52928
- * Defaults i18n (es/en) embebidos en `val-permissions-view`. Auto-registrados en
52929
- * el constructor del componente si el consumer no proveyó el namespace
52930
- * (`Settings.Permissions` por default). SOLO incluye las keys que la vista usa.
52931
- *
52932
- * Portado EXACTO de `PERMISSIONS_I18N` (showcase) bajo el proceso de ADR-021.
52933
- */
52934
- const PERMISSIONS_VIEW_I18N = {
52935
- es: {
52936
- pageTitle: 'Permisos',
52937
- pageDescription: 'Roles y permisos de esta app en la organización activa',
52938
- systemRolesTitle: 'Roles del sistema',
52939
- systemRolesHint: 'Predefinidos por la plataforma. No editables.',
52940
- customRolesTitle: 'Roles personalizados',
52941
- customRolesHint: 'Definidos por tu organización.',
52942
- systemBadge: 'Sistema',
52943
- noCustomRoles: 'Esta organización no tiene roles personalizados.',
52944
- noPermissions: 'Sin permisos asignados.',
52945
- // Grupos de alcance
52946
- groupApp: 'Permisos de la app',
52947
- groupOrg: 'Permisos de la organización',
52948
- groupPlatform: 'Permisos de plataforma',
52949
- groupOther: 'Otras apps',
52950
- // Roles del sistema
52951
- roleViewer: 'Visor',
52952
- roleEditor: 'Editor',
52953
- roleAdmin: 'Administrador',
52954
- roleSuperAdmin: 'Super Administrador',
52955
- roleBingoAdmin: 'Bingo · Administrador',
52956
- roleBingoTesorero: 'Bingo · Tesorero',
52957
- roleBingoTaquilla: 'Bingo · Taquilla',
52958
- roleBingoOperator: 'Bingo · Operador',
52959
- // Apps
52960
- app_showcase: 'Showcase',
52961
- app_bingo: 'Bingo',
52962
- // Recursos
52963
- permUsers: 'Usuarios',
52964
- permRoles: 'Roles',
52965
- permRbac: 'Control de acceso',
52966
- permApps: 'Apps',
52967
- 'permApi-keys': 'API keys',
52968
- 'permEmail-logs': 'Logs de correo',
52969
- 'permSms-logs': 'Logs de SMS',
52970
- permDocuments: 'Documentos',
52971
- permMedia: 'Media',
52972
- permTemplates: 'Plantillas',
52973
- permRequests: 'Solicitudes',
52974
- permBingo: 'Bingo',
52975
- // Acciones
52976
- permRead: 'Leer',
52977
- permWrite: 'Escribir',
52978
- permCreate: 'Crear',
52979
- permDelete: 'Eliminar',
52980
- permManage: 'Gestionar',
52981
- permEdit: 'Editar',
52982
- permSell: 'Vender',
52983
- permCheckin: 'Canjear',
52984
- permSettle: 'Rendir',
52985
- permExport: 'Exportar',
52986
- permOperate: 'Operar',
52987
- permAll: 'Todo',
52988
- // Loading / error
52989
- errorTitle: 'Error al cargar permisos',
52990
- retry: 'Reintentar',
52991
- },
52992
- en: {
52993
- pageTitle: 'Permissions',
52994
- pageDescription: 'Roles and permissions of this app in the active organization',
52995
- systemRolesTitle: 'System roles',
52996
- systemRolesHint: 'Defined by the platform. Not editable.',
52997
- customRolesTitle: 'Custom roles',
52998
- customRolesHint: 'Defined by your organization.',
52999
- systemBadge: 'System',
53000
- noCustomRoles: 'This organization has no custom roles.',
53001
- noPermissions: 'No permissions assigned.',
53002
- groupApp: 'App permissions',
53003
- groupOrg: 'Organization permissions',
53004
- groupPlatform: 'Platform permissions',
53005
- groupOther: 'Other apps',
53006
- roleViewer: 'Viewer',
53007
- roleEditor: 'Editor',
53008
- roleAdmin: 'Administrator',
53009
- roleSuperAdmin: 'Super Administrator',
53010
- roleBingoAdmin: 'Bingo · Administrator',
53011
- roleBingoTesorero: 'Bingo · Treasurer',
53012
- roleBingoTaquilla: 'Bingo · Box office',
53013
- roleBingoOperator: 'Bingo · Operator',
53014
- app_showcase: 'Showcase',
53015
- app_bingo: 'Bingo',
53016
- permUsers: 'Users',
53017
- permRoles: 'Roles',
53018
- permRbac: 'Access control',
53019
- permApps: 'Apps',
53020
- 'permApi-keys': 'API keys',
53021
- 'permEmail-logs': 'Email logs',
53022
- 'permSms-logs': 'SMS logs',
53023
- permDocuments: 'Documents',
53024
- permMedia: 'Media',
53025
- permTemplates: 'Templates',
53026
- permRequests: 'Requests',
53027
- permBingo: 'Bingo',
53028
- permRead: 'Read',
53029
- permWrite: 'Write',
53030
- permCreate: 'Create',
53031
- permDelete: 'Delete',
53032
- permManage: 'Manage',
53033
- permEdit: 'Edit',
53034
- permSell: 'Sell',
53035
- permCheckin: 'Check-in',
53036
- permSettle: 'Settle',
53037
- permExport: 'Export',
53038
- permOperate: 'Operate',
53039
- permAll: 'All',
53040
- errorTitle: 'Error loading permissions',
53041
- retry: 'Retry',
53042
- },
53043
- };
53044
-
53045
- const DEFAULT_NAMESPACE$5 = 'Settings.Permissions';
53046
- /**
53047
- * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
53048
- * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
53049
- * roles de plataforma) y los roles personalizados de la org activa, con el
53050
- * catálogo de permisos agrupado por alcance (app actual / plataforma / otras apps).
53051
- * Promovida desde `showcase` bajo el proceso de ADR-021.
53052
- *
53053
- * **RBAC interno** (como `organization-view`): lee la org activa vía
53054
- * `AuthService.user().activeOrgId`. La vista NO muta nada — el control de quién
53055
- * puede verla es responsabilidad del guard de ruta del consumer; no hay gating
53056
- * fino interno ni se expone como config.
53057
- *
53058
- * Datos: `forkJoin` de `OrgService.listOrgRoles(orgId)` (roles de la org) +
53059
- * `PermissionCatalogService.getCatalog(orgId)` (catálogo + appId actual). El
53060
- * catálogo define el alcance (`scope`/`appId`) de cada recurso para agrupar.
53061
- *
53062
- * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute` se
53063
- * inyecta `{ optional: true }` solo para leer el route data de config. Auto-registra
53064
- * sus defaults i18n (es/en) si el consumer no proveyó el namespace (default
53065
- * `Settings.Permissions`).
53066
- */
53067
- class PermissionsViewComponent {
53068
- /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
53069
- get ns() {
53070
- return this.resolvedConfig().i18nNamespace;
53071
- }
53072
- constructor() {
53073
- this.i18n = inject(I18nService);
53074
- this.orgService = inject(OrgService);
53075
- this.catalog = inject(PermissionCatalogService);
53076
- this.auth = inject(AuthService);
53077
- this.nav = inject(NavigationService);
53078
- this.route = inject(ActivatedRoute, { optional: true });
53079
- this.resolvedConfig = computed(() => {
53080
- const fromRoute = (this.route?.snapshot.data['permissionsConfig'] ?? {});
53081
- const merged = { ...fromRoute, ...(this.config ?? {}) };
53082
- return {
53083
- showSystemRoles: merged.showSystemRoles ?? true,
53084
- showCustomRoles: merged.showCustomRoles ?? true,
53085
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$5,
53086
- };
53087
- });
53088
- this.loading = signal(false);
53089
- this.loadError = signal(null);
53090
- this.roles = signal([]);
53091
- this.systemRoles = computed(() => this.roles().filter(r => r.isSystem));
53092
- this.customRoles = computed(() => this.roles().filter(r => !r.isSystem));
53093
- this.activeOrgId = computed(() => {
53094
- const u = this.auth.user();
53095
- return u?.activeOrgId ?? u?.activeOrg ?? '';
53096
- });
53097
- this.retryButtonProps = computed(() => ({
53098
- text: this.tt('retry'),
53099
- color: 'dark',
53100
- fill: 'outline',
53101
- size: 'default',
53102
- type: 'button',
53103
- state: this.loading() ? ComponentStates.DISABLED : ComponentStates.ENABLED,
53104
- }));
53105
- const ns = this.ns;
53106
- if (!this.i18n.hasNamespace(ns)) {
53107
- this.i18n.registerContent(ns, PERMISSIONS_VIEW_I18N);
53108
- }
53109
- this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
53110
- this.load();
53111
- connectPageRefresh(() => this.load());
53112
- }
53113
- load() {
53114
- const orgId = this.activeOrgId();
53115
- if (!orgId)
53116
- return;
53117
- this.loading.set(true);
53118
- this.loadError.set(null);
53119
- forkJoin({
53120
- roles: this.orgService.listOrgRoles(orgId),
53121
- catalog: this.catalog.getCatalog(orgId),
53122
- }).subscribe({
53123
- next: ({ roles: rawRoles, catalog }) => {
53124
- const byResource = new Map();
53125
- for (const r of catalog.permissions)
53126
- byResource.set(r.resource, r);
53127
- const currentAppId = catalog.appId;
53128
- // Labeler con la SSOT del backend (labels del catálogo) — reemplaza el
53129
- // mapa hardcodeado `perm*`. Locale del momento de carga (igual que antes:
53130
- // los labels se hornean en el RoleView; cambio de idioma requiere reload).
53131
- const labeler = createPermissionLabeler(catalog, this.i18n.lang(), {
53132
- allLabel: this.tt('permAll'),
53133
- });
53134
- this.roles.set((rawRoles ?? []).map((r) => this.buildRoleView({
53135
- id: r.id,
53136
- name: r.name,
53137
- description: r.description,
53138
- permissions: r.permissions ?? [],
53139
- isSystem: r.isSystem ?? false,
53140
- }, byResource, currentAppId, labeler)));
53141
- this.loading.set(false);
53142
- },
53143
- error: err => {
53144
- this.loadError.set(err);
53145
- this.loading.set(false);
53146
- },
53147
- });
53148
- }
53149
- buildRoleView(role, byResource, currentAppId, labeler) {
53150
- const appPerms = [];
53151
- const orgPerms = [];
53152
- const otherByApp = new Map();
53153
- for (const perm of role.permissions) {
53154
- const [resource] = perm.split(':');
53155
- const entry = byResource.get(resource);
53156
- // Sin entrada en el catálogo (el backend omite los recursos internal para
53157
- // no-platform-admins) o entrada internal → NUNCA se muestra a nivel de
53158
- // producto. Defensa en profundidad junto al filtro del backend (ADR-024).
53159
- // Nota: el rol puede traer el permiso aunque el recurso esté filtrado del
53160
- // catálogo (ej. admin tiene templates:*); por eso se descarta lo no resuelto.
53161
- if (!entry || entry.scope === 'internal') {
53162
- continue;
53163
- }
53164
- const label = labeler(perm);
53165
- if (entry.scope === 'app') {
53166
- if (entry.appId === currentAppId) {
53167
- appPerms.push(label);
53168
- }
53169
- else {
53170
- const appId = entry.appId ?? '?';
53171
- const arr = otherByApp.get(appId) ?? [];
53172
- arr.push(label);
53173
- otherByApp.set(appId, arr);
53174
- }
53175
- }
53176
- else {
53177
- // scope === 'org' → gestión de la organización del cliente
53178
- orgPerms.push(label);
53179
- }
53180
- }
53181
- const groups = [];
53182
- if (appPerms.length) {
53183
- groups.push({ key: 'app', label: this.tt('groupApp'), perms: appPerms });
53184
- }
53185
- if (orgPerms.length) {
53186
- groups.push({
53187
- key: 'org',
53188
- label: this.tt('groupOrg'),
53189
- perms: orgPerms,
53190
- });
53191
- }
53192
- for (const [appId, perms] of otherByApp) {
53193
- groups.push({
53194
- key: `other:${appId}`,
53195
- label: `${this.tt('groupOther')}: ${this.appLabel(appId)}`,
53196
- perms,
53197
- });
53198
- }
53199
- return {
53200
- id: role.id,
53201
- name: role.name,
53202
- description: role.description,
53203
- isSystem: role.isSystem,
53204
- groups,
53205
- hasPerms: groups.length > 0,
53206
- };
53207
- }
53208
- tt(key) {
53209
- return this.i18n.t(key, this.ns);
53210
- }
53211
- appLabel(appId) {
53212
- const key = `app_${appId}`;
53213
- const label = this.tt(key);
53214
- return label === key ? appId : label;
53215
- }
53216
- roleLabel(name) {
53217
- const map = {
53218
- viewer: 'roleViewer',
53219
- editor: 'roleEditor',
53220
- admin: 'roleAdmin',
53221
- super_admin: 'roleSuperAdmin',
53222
- bingo_admin: 'roleBingoAdmin',
53223
- bingo_tesorero: 'roleBingoTesorero',
53224
- bingo_taquilla: 'roleBingoTaquilla',
53225
- bingo_operator: 'roleBingoOperator',
53226
- };
53227
- return this.tt(map[name] ?? name);
53228
- }
53229
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53230
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PermissionsViewComponent, isStandalone: true, selector: "val-permissions-view", inputs: { config: "config" }, ngImport: i0, template: `
53231
- <div class="page">
53232
- <header class="page-header">
53233
- <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
53234
- <val-title
53235
- [props]="{
53236
- size: 'large',
53237
- color: 'dark',
53238
- bold: false,
53239
- content: tt('pageDescription'),
53240
- }"
53241
- />
53242
- </header>
53243
-
53244
- @if (loading()) {
53245
- <div class="spinner-row">
53246
- <ion-spinner name="crescent" />
53247
- </div>
53248
- } @else if (loadError()) {
53249
- <div class="error-row">
53250
- <val-text
53251
- [props]="{
53252
- content: tt('errorTitle'),
53253
- size: 'medium',
53254
- color: 'medium',
53255
- bold: false,
53256
- }"
53257
- />
53258
- <val-button [props]="retryButtonProps()" (click)="load()" />
53259
- </div>
53260
- } @else {
53261
- @if (resolvedConfig().showSystemRoles) {
53262
- <section class="perm-section">
53263
- <div class="perm-section__header">
53264
- <val-title
53265
- [props]="{
53266
- size: 'medium',
53267
- color: 'dark',
53268
- bold: true,
53269
- content: tt('systemRolesTitle'),
53270
- }"
53271
- />
53272
- <val-text
53273
- [props]="{
53274
- content: tt('systemRolesHint'),
53275
- size: 'medium',
53276
- color: 'medium',
53277
- bold: false,
53278
- }"
53279
- />
53280
- </div>
53281
- <div class="role-list">
53282
- @for (role of systemRoles(); track role.id) {
53283
- <div class="role-card">
53284
- <div class="role-card__header">
53285
- <span class="role-card__name">{{ roleLabel(role.name) }}</span>
53286
- <val-pill
53287
- [props]="{
53288
- label: tt('systemBadge'),
53289
- color: 'primary',
53290
- weight: 'bold',
53291
- }"
53292
- />
53293
- </div>
53294
- @if (role.description) {
53295
- <span class="role-card__desc">{{ role.description }}</span>
53296
- }
53297
- @if (!role.hasPerms) {
53298
- <div class="perm-chips">
53299
- <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53300
- </div>
53301
- } @else {
53302
- @for (g of role.groups; track g.key) {
53303
- <div class="perm-group">
53304
- <span class="perm-group__label">{{ g.label }}</span>
53305
- <div class="perm-chips">
53306
- @for (p of g.perms; track p) {
53307
- <span class="perm-chip">{{ p }}</span>
53308
- }
53309
- </div>
53310
- </div>
53311
- }
53312
- }
53313
- </div>
53314
- }
53315
- </div>
53316
- </section>
53317
- }
53318
-
53319
- @if (resolvedConfig().showCustomRoles) {
53320
- <section class="perm-section">
53321
- <div class="perm-section__header">
53322
- <val-title
53323
- [props]="{
53324
- size: 'medium',
53325
- color: 'dark',
53326
- bold: true,
53327
- content: tt('customRolesTitle'),
53328
- }"
53329
- />
53330
- <val-text
53331
- [props]="{
53332
- content: tt('customRolesHint'),
53333
- size: 'medium',
53334
- color: 'medium',
53335
- bold: false,
53336
- }"
53337
- />
53338
- </div>
53339
- @if (customRoles().length === 0) {
53340
- <val-text
53341
- [props]="{
53342
- content: tt('noCustomRoles'),
53343
- size: 'medium',
53344
- color: 'medium',
53345
- bold: false,
53346
- }"
53347
- />
53348
- } @else {
53349
- <div class="role-list">
53350
- @for (role of customRoles(); track role.id) {
53351
- <div class="role-card">
53352
- <div class="role-card__header">
53353
- <span class="role-card__name">{{ role.name }}</span>
53354
- </div>
53355
- @if (role.description) {
53356
- <span class="role-card__desc">{{ role.description }}</span>
53357
- }
53358
- @if (!role.hasPerms) {
53359
- <div class="perm-chips">
53360
- <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53361
- </div>
53362
- } @else {
53363
- @for (g of role.groups; track g.key) {
53364
- <div class="perm-group">
53365
- <span class="perm-group__label">{{ g.label }}</span>
53366
- <div class="perm-chips">
53367
- @for (p of g.perms; track p) {
53368
- <span class="perm-chip">{{ p }}</span>
53369
- }
53370
- </div>
53371
- </div>
53372
- }
53373
- }
53374
- </div>
53375
- }
53376
- </div>
53377
- }
53378
- </section>
53379
- }
53380
- }
53381
- </div>
53382
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"], dependencies: [{ kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
53383
- }
53384
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PermissionsViewComponent, decorators: [{
53385
- type: Component,
53386
- args: [{ selector: 'val-permissions-view', standalone: true, imports: [IonSpinner, DisplayComponent, PillComponent, TitleComponent, TextComponent, ButtonComponent], template: `
53387
- <div class="page">
53388
- <header class="page-header">
53389
- <val-display [props]="{ size: 'small', color: 'dark', content: tt('pageTitle') }" />
53390
- <val-title
53391
- [props]="{
53392
- size: 'large',
53393
- color: 'dark',
53394
- bold: false,
53395
- content: tt('pageDescription'),
53396
- }"
53397
- />
53398
- </header>
53399
-
53400
- @if (loading()) {
53401
- <div class="spinner-row">
53402
- <ion-spinner name="crescent" />
53403
- </div>
53404
- } @else if (loadError()) {
53405
- <div class="error-row">
53406
- <val-text
53407
- [props]="{
53408
- content: tt('errorTitle'),
53409
- size: 'medium',
53410
- color: 'medium',
53411
- bold: false,
53412
- }"
53413
- />
53414
- <val-button [props]="retryButtonProps()" (click)="load()" />
53415
- </div>
53416
- } @else {
53417
- @if (resolvedConfig().showSystemRoles) {
53418
- <section class="perm-section">
53419
- <div class="perm-section__header">
53420
- <val-title
53421
- [props]="{
53422
- size: 'medium',
53423
- color: 'dark',
53424
- bold: true,
53425
- content: tt('systemRolesTitle'),
53426
- }"
53427
- />
53428
- <val-text
53429
- [props]="{
53430
- content: tt('systemRolesHint'),
53431
- size: 'medium',
53432
- color: 'medium',
53433
- bold: false,
53434
- }"
53435
- />
53436
- </div>
53437
- <div class="role-list">
53438
- @for (role of systemRoles(); track role.id) {
53439
- <div class="role-card">
53440
- <div class="role-card__header">
53441
- <span class="role-card__name">{{ roleLabel(role.name) }}</span>
53442
- <val-pill
53443
- [props]="{
53444
- label: tt('systemBadge'),
53445
- color: 'primary',
53446
- weight: 'bold',
53447
- }"
53448
- />
53449
- </div>
53450
- @if (role.description) {
53451
- <span class="role-card__desc">{{ role.description }}</span>
53452
- }
53453
- @if (!role.hasPerms) {
53454
- <div class="perm-chips">
53455
- <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53456
- </div>
53457
- } @else {
53458
- @for (g of role.groups; track g.key) {
53459
- <div class="perm-group">
53460
- <span class="perm-group__label">{{ g.label }}</span>
53461
- <div class="perm-chips">
53462
- @for (p of g.perms; track p) {
53463
- <span class="perm-chip">{{ p }}</span>
53464
- }
53465
- </div>
53466
- </div>
53467
- }
53468
- }
53469
- </div>
53470
- }
53471
- </div>
53472
- </section>
53473
- }
53474
-
53475
- @if (resolvedConfig().showCustomRoles) {
53476
- <section class="perm-section">
53477
- <div class="perm-section__header">
53478
- <val-title
53479
- [props]="{
53480
- size: 'medium',
53481
- color: 'dark',
53482
- bold: true,
53483
- content: tt('customRolesTitle'),
53484
- }"
53485
- />
53486
- <val-text
53487
- [props]="{
53488
- content: tt('customRolesHint'),
53489
- size: 'medium',
53490
- color: 'medium',
53491
- bold: false,
53492
- }"
53493
- />
53494
- </div>
53495
- @if (customRoles().length === 0) {
53496
- <val-text
53497
- [props]="{
53498
- content: tt('noCustomRoles'),
53499
- size: 'medium',
53500
- color: 'medium',
53501
- bold: false,
53502
- }"
53503
- />
53504
- } @else {
53505
- <div class="role-list">
53506
- @for (role of customRoles(); track role.id) {
53507
- <div class="role-card">
53508
- <div class="role-card__header">
53509
- <span class="role-card__name">{{ role.name }}</span>
53510
- </div>
53511
- @if (role.description) {
53512
- <span class="role-card__desc">{{ role.description }}</span>
53513
- }
53514
- @if (!role.hasPerms) {
53515
- <div class="perm-chips">
53516
- <span class="perm-chip perm-chip--empty">{{ tt('noPermissions') }}</span>
53517
- </div>
53518
- } @else {
53519
- @for (g of role.groups; track g.key) {
53520
- <div class="perm-group">
53521
- <span class="perm-group__label">{{ g.label }}</span>
53522
- <div class="perm-chips">
53523
- @for (p of g.perms; track p) {
53524
- <span class="perm-chip">{{ p }}</span>
53525
- }
53526
- </div>
53527
- </div>
53528
- }
53529
- }
53530
- </div>
53531
- }
53532
- </div>
53533
- }
53534
- </section>
53535
- }
53536
- }
53537
- </div>
53538
- `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:24px}.spinner-row,.error-row{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 0}.perm-section{margin-bottom:32px}.perm-section__header{margin-bottom:12px}.role-list{display:flex;flex-direction:column;gap:10px}.role-card{border:1.5px solid var(--ion-color-light);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}.role-card__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.role-card__name{font-size:15px;font-weight:600;color:var(--ion-color-dark)}.role-card__desc{font-size:13px;color:var(--ion-color-medium);line-height:1.4}.perm-group{display:flex;flex-direction:column;gap:6px}.perm-group__label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ion-color-medium)}.perm-chips{display:flex;flex-wrap:wrap;gap:6px}.perm-chip{font-size:12px;padding:3px 10px;border-radius:20px;background:var(--ion-color-light);color:var(--ion-color-dark)}.perm-chip--empty{color:var(--ion-color-medium);background:transparent;padding-left:0}\n"] }]
53539
- }], ctorParameters: () => [], propDecorators: { config: [{
53540
- type: Input
53541
- }] } });
53542
-
53543
53712
  /**
53544
53713
  * Helper para montar la vista Permisos (`val-permissions-view`) como ruta en una
53545
53714
  * app del factory. El `config` se pasa por route `data` (`permissionsConfig`) y el
@@ -67204,5 +67373,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
67204
67373
  * Generated bundle index. Do not edit.
67205
67374
  */
67206
67375
 
67207
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionSelectorComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketCardComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
67376
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketCardComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
67208
67377
  //# sourceMappingURL=valtech-components.mjs.map