valtech-components 2.0.848 → 2.0.850

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.
@@ -5,7 +5,7 @@ import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal,
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgStyle, NgFor, isPlatformBrowser, DOCUMENT, NgClass } from '@angular/common';
7
7
  import { addIcons } from 'ionicons';
8
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, warningOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, cameraOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
8
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, warningOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, cameraOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
9
9
  import * as i1$1 from '@angular/router';
10
10
  import { RouterLink, Router, NavigationEnd, RouterOutlet, RouterModule } from '@angular/router';
11
11
  import { Browser } from '@capacitor/browser';
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
53
53
  * Current version of valtech-components.
54
54
  * This is automatically updated during the publish process.
55
55
  */
56
- const VERSION = '2.0.848';
56
+ const VERSION = '2.0.850';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -4401,6 +4401,7 @@ const VALTECH_DEFAULT_CONTENT = {
4401
4401
  mfaEnabledOk: '¡MFA habilitado correctamente!',
4402
4402
  mfaDisabledOk: 'MFA deshabilitado correctamente.',
4403
4403
  mfaCodesCopied: 'Códigos copiados al portapapeles.',
4404
+ mfaSecretCopied: 'Secreto copiado al portapapeles.',
4404
4405
  mfaErrorInvalidCode: 'Código incorrecto.',
4405
4406
  mfaErrorExpiredCode: 'El código ha expirado. Solicita uno nuevo.',
4406
4407
  mfaErrorCodeUsed: 'Este código ya fue utilizado.',
@@ -4557,6 +4558,7 @@ const VALTECH_DEFAULT_CONTENT = {
4557
4558
  mfaEnabledOk: 'MFA enabled successfully!',
4558
4559
  mfaDisabledOk: 'MFA disabled successfully.',
4559
4560
  mfaCodesCopied: 'Codes copied to clipboard.',
4561
+ mfaSecretCopied: 'Secret copied to clipboard.',
4560
4562
  mfaErrorInvalidCode: 'Incorrect code.',
4561
4563
  mfaErrorExpiredCode: 'The code has expired. Request a new one.',
4562
4564
  mfaErrorCodeUsed: 'This code was already used.',
@@ -27159,6 +27161,216 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
27159
27161
  type: Input
27160
27162
  }] } });
27161
27163
 
27164
+ // Registra los 3 iconos default (variantes). Si la página pasa un icono custom
27165
+ // en `props.icon`, debe registrarlo ella misma con addIcons() — Ionic
27166
+ // standalone no resuelve nombres global, solo los del registry local.
27167
+ addIcons({
27168
+ alertCircleOutline,
27169
+ cloudOfflineOutline,
27170
+ documentOutline,
27171
+ });
27172
+ const DEFAULT_ICON_BY_VARIANT = {
27173
+ empty: 'document-outline',
27174
+ error: 'alert-circle-outline',
27175
+ offline: 'cloud-offline-outline',
27176
+ };
27177
+ /**
27178
+ * `<val-empty-state>` — molecule declarativo para estados de "no hay datos /
27179
+ * hubo error / sin red". Reemplaza el patrón ad-hoc (ion-icon + val-title +
27180
+ * val-text + val-button) que cada página venía improvisando.
27181
+ *
27182
+ * Uso típico:
27183
+ *
27184
+ * ```html
27185
+ * @if (loading()) {
27186
+ * <val-skeleton-layout [props]="..." />
27187
+ * } @else if (errorState(); as e) {
27188
+ * <val-empty-state [props]="e" />
27189
+ * } @else if (items().length === 0) {
27190
+ * <val-empty-state [props]="emptyProps" />
27191
+ * } @else {
27192
+ * <!-- contenido normal -->
27193
+ * }
27194
+ * ```
27195
+ *
27196
+ * I18n: las cadenas vienen ya traducidas en las props (la lib es agnóstica de
27197
+ * namespaces). Para wiring automático desde un error capturado, usar el
27198
+ * helper `createErrorStateProps(err, ...)` (`valtech-components`).
27199
+ */
27200
+ class EmptyStateComponent {
27201
+ constructor() {
27202
+ this.props = input.required();
27203
+ /** Variante resuelta (default `'empty'`). */
27204
+ this.variantClass = computed(() => this.props().variant ?? 'empty');
27205
+ /** Icono final — custom si se pasó, default de la variante si no. */
27206
+ this.iconName = computed(() => this.props().icon ?? DEFAULT_ICON_BY_VARIANT[this.variantClass()]);
27207
+ /** Tamaño en px. Default 64. */
27208
+ this.iconSize = computed(() => this.props().iconSize ?? 64);
27209
+ /**
27210
+ * Props del botón CTA. Construidas a partir de `props().action`. El estado
27211
+ * `WORKING` se aplica cuando `action.loading === true` (spinner mientras
27212
+ * el handler corre).
27213
+ */
27214
+ this.ctaProps = computed(() => {
27215
+ const action = this.props().action;
27216
+ return {
27217
+ text: action?.label ?? '',
27218
+ color: 'dark',
27219
+ type: 'button',
27220
+ fill: 'solid',
27221
+ size: 'default',
27222
+ shape: 'round',
27223
+ state: action?.loading ? ComponentStates.WORKING : ComponentStates.ENABLED,
27224
+ };
27225
+ });
27226
+ }
27227
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EmptyStateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
27228
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: EmptyStateComponent, isStandalone: true, selector: "val-empty-state", inputs: { props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
27229
+ <section
27230
+ class="empty-state"
27231
+ [class.empty-state--error]="variantClass() === 'error'"
27232
+ [class.empty-state--offline]="variantClass() === 'offline'"
27233
+ role="status"
27234
+ aria-live="polite"
27235
+ >
27236
+ <ion-icon
27237
+ class="empty-state__icon"
27238
+ [name]="iconName()"
27239
+ [style.font-size.px]="iconSize()"
27240
+ aria-hidden="true"
27241
+ />
27242
+
27243
+ <val-title
27244
+ [props]="{
27245
+ size: 'large',
27246
+ color: 'dark',
27247
+ bold: true,
27248
+ content: props().title,
27249
+ }"
27250
+ />
27251
+
27252
+ @if (props().description; as desc) {
27253
+ <val-text
27254
+ [props]="{
27255
+ size: 'large',
27256
+ color: 'medium',
27257
+ bold: false,
27258
+ content: desc,
27259
+ }"
27260
+ />
27261
+ }
27262
+
27263
+ @if (props().action; as a) {
27264
+ <val-button
27265
+ class="empty-state__cta"
27266
+ [props]="ctaProps()"
27267
+ (click)="a.handler()"
27268
+ />
27269
+ }
27270
+ </section>
27271
+ `, isInline: true, styles: [".empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;padding:32px 16px;min-height:240px}.empty-state__icon{color:var(--ion-color-medium)}.empty-state--error .empty-state__icon{color:var(--ion-color-warning, #ffb800)}.empty-state--offline .empty-state__icon{color:var(--ion-color-medium);opacity:.7}.empty-state__cta{margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
27272
+ }
27273
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EmptyStateComponent, decorators: [{
27274
+ type: Component,
27275
+ args: [{ selector: 'val-empty-state', standalone: true, imports: [CommonModule, IonIcon, ButtonComponent, TextComponent, TitleComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
27276
+ <section
27277
+ class="empty-state"
27278
+ [class.empty-state--error]="variantClass() === 'error'"
27279
+ [class.empty-state--offline]="variantClass() === 'offline'"
27280
+ role="status"
27281
+ aria-live="polite"
27282
+ >
27283
+ <ion-icon
27284
+ class="empty-state__icon"
27285
+ [name]="iconName()"
27286
+ [style.font-size.px]="iconSize()"
27287
+ aria-hidden="true"
27288
+ />
27289
+
27290
+ <val-title
27291
+ [props]="{
27292
+ size: 'large',
27293
+ color: 'dark',
27294
+ bold: true,
27295
+ content: props().title,
27296
+ }"
27297
+ />
27298
+
27299
+ @if (props().description; as desc) {
27300
+ <val-text
27301
+ [props]="{
27302
+ size: 'large',
27303
+ color: 'medium',
27304
+ bold: false,
27305
+ content: desc,
27306
+ }"
27307
+ />
27308
+ }
27309
+
27310
+ @if (props().action; as a) {
27311
+ <val-button
27312
+ class="empty-state__cta"
27313
+ [props]="ctaProps()"
27314
+ (click)="a.handler()"
27315
+ />
27316
+ }
27317
+ </section>
27318
+ `, styles: [".empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;padding:32px 16px;min-height:240px}.empty-state__icon{color:var(--ion-color-medium)}.empty-state--error .empty-state__icon{color:var(--ion-color-warning, #ffb800)}.empty-state--offline .empty-state__icon{color:var(--ion-color-medium);opacity:.7}.empty-state__cta{margin-top:8px}\n"] }]
27319
+ }] });
27320
+
27321
+ /**
27322
+ * Convierte un error capturado en `EmptyStateMetadata` listo para
27323
+ * `<val-empty-state>`. Decide la variante (`offline` vs `error`) según
27324
+ * `interpretError(err).isNetwork`.
27325
+ *
27326
+ * Uso típico en una page:
27327
+ *
27328
+ * ```ts
27329
+ * readonly errorState = computed(() => {
27330
+ * const err = this.loadError();
27331
+ * if (!err) return null;
27332
+ * return createErrorStateProps(err, {
27333
+ * title: { offline: this.t('offlineTitle'), error: this.t('errorTitle') },
27334
+ * description: { offline: this.t('offlineHint'), error: interpretError(err).message },
27335
+ * retryLabel: this.t('retry'),
27336
+ * onRetry: () => this.reload(),
27337
+ * retrying: this.reloading(),
27338
+ * });
27339
+ * });
27340
+ * ```
27341
+ *
27342
+ * En el template:
27343
+ *
27344
+ * ```html
27345
+ * @if (errorState(); as e) {
27346
+ * <val-empty-state [props]="e" />
27347
+ * }
27348
+ * ```
27349
+ *
27350
+ * Para escenarios SIN error (lista vacía), construir `EmptyStateMetadata`
27351
+ * manualmente con `variant: 'empty'` — no es lo que este helper resuelve.
27352
+ */
27353
+ function createErrorStateProps(err, opts) {
27354
+ const interpreted = interpretError(err);
27355
+ const variant = interpreted.isNetwork ? 'offline' : 'error';
27356
+ const pick = (v) => typeof v === 'object' && v !== null && 'offline' in v
27357
+ ? v[variant]
27358
+ : v;
27359
+ const action = opts.retryLabel && opts.onRetry
27360
+ ? {
27361
+ label: opts.retryLabel,
27362
+ handler: opts.onRetry,
27363
+ loading: opts.retrying ?? false,
27364
+ }
27365
+ : undefined;
27366
+ return {
27367
+ variant,
27368
+ title: pick(opts.title),
27369
+ description: opts.description ? pick(opts.description) : undefined,
27370
+ action,
27371
+ };
27372
+ }
27373
+
27162
27374
  /**
27163
27375
  * ImageCropComponent
27164
27376
  *
@@ -31078,6 +31290,8 @@ class MfaModalComponent {
31078
31290
  this.totpQr = signal(null);
31079
31291
  /** Códigos de respaldo recién regenerados — se muestran una sola vez. */
31080
31292
  this.regeneratedCodes = signal(null);
31293
+ /** Marca momentánea cuando el secreto TOTP se acaba de copiar (feedback visual). */
31294
+ this.copiedSecret = signal(false);
31081
31295
  this.resendCooldown = signal(0);
31082
31296
  this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
31083
31297
  this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
@@ -31120,7 +31334,7 @@ class MfaModalComponent {
31120
31334
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
31121
31335
  }));
31122
31336
  this.resendTimer = null;
31123
- addIcons({ informationCircleOutline });
31337
+ addIcons({ informationCircleOutline, copyOutline, checkmarkOutline });
31124
31338
  }
31125
31339
  ngOnDestroy() {
31126
31340
  this.stopCooldown();
@@ -31352,6 +31566,18 @@ class MfaModalComponent {
31352
31566
  /* sin recurso de copia disponible */
31353
31567
  }
31354
31568
  }
31569
+ /** Copia el secreto TOTP al portapapeles + feedback visual (check) durante 2s. */
31570
+ async copySecret(secret) {
31571
+ try {
31572
+ await navigator.clipboard.writeText(secret);
31573
+ this.copiedSecret.set(true);
31574
+ this.showToast(this.t('mfaSecretCopied'));
31575
+ setTimeout(() => this.copiedSecret.set(false), 2000);
31576
+ }
31577
+ catch {
31578
+ /* sin recurso de copia disponible */
31579
+ }
31580
+ }
31355
31581
  /** Etiqueta i18n legible para un método MFA. */
31356
31582
  methodLabel(method) {
31357
31583
  switch (method) {
@@ -31424,7 +31650,7 @@ class MfaModalComponent {
31424
31650
  this.toast.show({ message, duration: 3500 });
31425
31651
  }
31426
31652
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
31427
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen", prefillCode: "prefillCode" }, 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\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n <val-display [props]=\"{ content: t('mfaManageTitle'), size: 'small', color: 'dark' }\" />\n\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') { @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <val-title [props]=\"{ content: t('mfaEnableTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <val-title [props]=\"{ content: t('mfaTotpSetupTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <val-title [props]=\"{ content: t('mfaConfirmTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { 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: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
31653
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen", prefillCode: "prefillCode" }, 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\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n <val-display [props]=\"{ content: t('mfaManageTitle'), size: 'small', color: 'dark' }\" />\n\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') { @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <val-title [props]=\"{ content: t('mfaEnableTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <val-title [props]=\"{ content: t('mfaTotpSetupTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <div class=\"mfa-secret-row\">\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n <ion-button fill=\"clear\" size=\"small\" (click)=\"copySecret(setup.secret)\" [attr.aria-label]=\"t('copy')\">\n @if (copiedSecret()) {\n <ion-icon name=\"checkmark-outline\"></ion-icon>\n } @else {\n <ion-icon name=\"copy-outline\"></ion-icon>\n }\n </ion-button>\n </div>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <val-title [props]=\"{ content: t('mfaConfirmTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-status{display:inline-flex;align-items:center;width:fit-content;padding:4px 10px;border-radius:999px;font-size:13px;font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade);background:rgba(var(--ion-color-success-rgb),.12)}.mfa-status--off{color:var(--ion-color-medium-shade);background:var(--ion-color-light)}.mfa-secret-row{display:flex;align-items:center;gap:8px}.mfa-secret-row .mfa-secret{flex:1;margin:0}.mfa-secret-row ion-button{flex-shrink:0;--color: var(--ion-color-dark);margin:0}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { 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: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
31428
31654
  }
31429
31655
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
31430
31656
  type: Component,
@@ -31448,7 +31674,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
31448
31674
  QrCodeComponent,
31449
31675
  TitleComponent,
31450
31676
  PinInputComponent,
31451
- ], 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\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n <val-display [props]=\"{ content: t('mfaManageTitle'), size: 'small', color: 'dark' }\" />\n\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') { @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <val-title [props]=\"{ content: t('mfaEnableTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <val-title [props]=\"{ content: t('mfaTotpSetupTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <val-title [props]=\"{ content: t('mfaConfirmTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
31677
+ ], 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\" (click)=\"close()\">\n <strong>{{ t('close') }}</strong>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n <val-display [props]=\"{ content: t('mfaManageTitle'), size: 'small', color: 'dark' }\" />\n\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') { @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <val-title [props]=\"{ content: t('mfaEnableTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <val-title [props]=\"{ content: t('mfaTotpSetupTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <div class=\"mfa-secret-row\">\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n <ion-button fill=\"clear\" size=\"small\" (click)=\"copySecret(setup.secret)\" [attr.aria-label]=\"t('copy')\">\n @if (copiedSecret()) {\n <ion-icon name=\"checkmark-outline\"></ion-icon>\n } @else {\n <ion-icon name=\"copy-outline\"></ion-icon>\n }\n </ion-button>\n </div>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <val-title [props]=\"{ content: t('mfaConfirmTitle'), size: 'large', color: '', bold: false }\" />\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-status{display:inline-flex;align-items:center;width:fit-content;padding:4px 10px;border-radius:999px;font-size:13px;font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade);background:rgba(var(--ion-color-success-rgb),.12)}.mfa-status--off{color:var(--ion-color-medium-shade);background:var(--ion-color-light)}.mfa-secret-row{display:flex;align-items:center;gap:8px}.mfa-secret-row .mfa-secret{flex:1;margin:0}.mfa-secret-row ion-button{flex-shrink:0;--color: var(--ion-color-dark);margin:0}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
31452
31678
  }], ctorParameters: () => [], propDecorators: { isOpen: [{
31453
31679
  type: Input
31454
31680
  }], prefillCode: [{
@@ -47868,5 +48094,5 @@ function buildFooterLinks(links, t, resolver) {
47868
48094
  * Generated bundle index. Do not edit.
47869
48095
  */
47870
48096
 
47871
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, 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_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_SOCIAL_LINKS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
48097
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, EmptyStateComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, 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_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_SOCIAL_LINKS, VERSION, ValtechErrorService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
47872
48098
  //# sourceMappingURL=valtech-components.mjs.map