valtech-components 2.0.983 → 2.0.985

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 (23) hide show
  1. package/esm2022/lib/components/organisms/bottom-nav/bottom-nav.component.mjs +3 -3
  2. package/esm2022/lib/components/organisms/login-attempt-modal/login-attempt-modal.component.mjs +298 -0
  3. package/esm2022/lib/components/organisms/security-view/security-view.component.mjs +819 -0
  4. package/esm2022/lib/components/organisms/security-view/security-view.i18n.mjs +105 -0
  5. package/esm2022/lib/components/organisms/security-view/security.routes.mjs +29 -0
  6. package/esm2022/lib/components/organisms/security-view/types.mjs +2 -0
  7. package/esm2022/lib/components/organisms/session-list-modal/session-list-modal.component.mjs +206 -0
  8. package/esm2022/lib/components/organisms/session-list-modal/types.mjs +2 -0
  9. package/esm2022/lib/services/font-size/font-size.service.mjs +3 -1
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +7 -1
  12. package/fesm2022/valtech-components.mjs +2397 -990
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/organisms/login-attempt-modal/login-attempt-modal.component.d.ts +58 -0
  15. package/lib/components/organisms/security-view/security-view.component.d.ts +130 -0
  16. package/lib/components/organisms/security-view/security-view.i18n.d.ts +12 -0
  17. package/lib/components/organisms/security-view/security.routes.d.ts +24 -0
  18. package/lib/components/organisms/security-view/types.d.ts +48 -0
  19. package/lib/components/organisms/session-list-modal/session-list-modal.component.d.ts +56 -0
  20. package/lib/components/organisms/session-list-modal/types.d.ts +27 -0
  21. package/lib/version.d.ts +1 -1
  22. package/package.json +1 -1
  23. package/public-api.d.ts +6 -0
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, viewChild, Injector, isSignal, ViewEncapsulation } from '@angular/core';
2
+ import { inject, InjectionToken, makeEnvironmentProviders, APP_INITIALIZER, ErrorHandler, Injectable, DestroyRef, Inject, signal, computed, PLATFORM_ID, isDevMode, Optional, runInInjectionContext, effect, Pipe, TemplateRef, ViewContainerRef, input, Directive, EventEmitter, Component, Input, Output, ChangeDetectionStrategy, HostBinding, HostListener, NgZone, ViewChild, ChangeDetectorRef, output, ContentChild, ElementRef, viewChild, untracked, Injector, isSignal, ViewEncapsulation } from '@angular/core';
3
3
  import { BehaviorSubject, throwError, Subject, map, distinctUntilChanged, filter as filter$1, take as take$1, firstValueFrom, of, from, EMPTY, Observable, interval, debounceTime, switchMap as switchMap$1, catchError as catchError$1, takeUntil, isObservable, shareReplay, race, timer, forkJoin } from 'rxjs';
4
4
  import { catchError, switchMap, finalize, filter, take, map as map$1, tap, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
5
5
  import * as i1$3 from '@angular/common/http';
@@ -25,7 +25,7 @@ import { getApps, getApp, initializeApp as initializeApp$1 } from 'firebase/app'
25
25
  import * as i2$1 from '@ionic/angular/standalone';
26
26
  import { AlertController, IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonTextarea, IonDatetime, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonRippleEffect, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
27
27
  import { addIcons } from 'ionicons';
28
- 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, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, 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, attachOutline, cameraOutline, closeCircleOutline, imageOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, menuOutline } from 'ionicons/icons';
28
+ 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, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, 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, attachOutline, cameraOutline, closeCircleOutline, imageOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, menuOutline } from 'ionicons/icons';
29
29
  import { Browser } from '@capacitor/browser';
30
30
  import * as i1$6 from '@angular/platform-browser';
31
31
  import { DomSanitizer, Meta, Title } from '@angular/platform-browser';
@@ -54,7 +54,7 @@ import 'prismjs/components/prism-json';
54
54
  * Current version of valtech-components.
55
55
  * This is automatically updated during the publish process.
56
56
  */
57
- const VERSION = '2.0.983';
57
+ const VERSION = '2.0.985';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -40269,7 +40269,7 @@ class BottomNavComponent {
40269
40269
  }
40270
40270
  </div>
40271
40271
  </nav>
40272
- `, isInline: true, styles: [":host{display:block;position:fixed;bottom:0;left:0;right:0;z-index:100;pointer-events:none}.bottom-nav{--bottom-nav-bg: var(--ion-background-color);--bottom-nav-active: var(--ion-color-primary);--bottom-nav-inactive: var(--ion-color-medium);--bottom-nav-radius: 16px 16px 0 0;--bottom-nav-height: 56px;--bottom-nav-fab-size: 56px;--bottom-nav-max-width: var(--val-container-md, 720px);--fab-color: var(--ion-color-primary);pointer-events:auto;background:var(--bottom-nav-bg);border-radius:var(--bottom-nav-radius);height:var(--bottom-nav-height);padding:0 8px;max-width:var(--bottom-nav-max-width);margin:0 auto}@media (min-width: 576px){.bottom-nav{--bottom-nav-height: 64px}}.bottom-nav--elevated{box-shadow:0 -4px 20px #00000014}.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.75);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-top:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.06)}.bottom-nav--floating{margin:0 auto 8px;width:calc(100% - 32px);border-radius:32px;border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08);box-shadow:0 4px 24px #00000014,0 8px 32px #0000000a;--bottom-nav-radius: 32px}.bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.82);backdrop-filter:blur(28px) saturate(200%);-webkit-backdrop-filter:blur(28px) saturate(200%);border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08)}.bottom-nav--floating.bottom-nav--elevated{box-shadow:0 4px 24px #0000001a,0 12px 48px #00000014}.bottom-nav--safe-area{padding-bottom:env(safe-area-inset-bottom,0);height:calc(var(--bottom-nav-height) + env(safe-area-inset-bottom,0))}.bottom-nav--floating.bottom-nav--safe-area{margin-bottom:calc(8px + env(safe-area-inset-bottom,0));padding-bottom:0;height:var(--bottom-nav-height)}.bottom-nav__container{display:flex;align-items:center;justify-content:space-around;height:var(--bottom-nav-height);max-width:500px;margin:0 auto}.bottom-nav__tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;height:100%;padding:8px 4px;background:transparent;border:none;cursor:pointer;position:relative;overflow:hidden;color:var(--bottom-nav-inactive);transition:color .2s ease;max-width:80px;--ripple-color: var(--bottom-nav-active)}.bottom-nav__tab:focus-visible{outline:2px solid var(--bottom-nav-active);outline-offset:-2px;border-radius:8px}.bottom-nav__tab--active{color:var(--bottom-nav-active)}.bottom-nav__tab--disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.bottom-nav__tab[data-animation=scale] .bottom-nav__tab-icon{transition:transform .2s cubic-bezier(.4,0,.2,1)}.bottom-nav__tab[data-animation=scale].bottom-nav__tab--active .bottom-nav__tab-icon{transform:scale(1.15)}.bottom-nav__tab[data-animation=fade]{transition:opacity .2s ease,color .2s ease}.bottom-nav__tab[data-animation=fade]:not(.bottom-nav__tab--active){opacity:.6}.bottom-nav__tab[data-animation=slide] .bottom-nav__tab-label{transition:transform .2s ease,opacity .2s ease;transform:translateY(6px);opacity:0}.bottom-nav__tab[data-animation=slide].bottom-nav__tab--active .bottom-nav__tab-label{transform:translateY(0);opacity:1}.bottom-nav__tab-icon{position:relative;font-size:1.5rem;line-height:1}.bottom-nav__tab-icon ion-icon{display:block}.bottom-nav__tab-label{font-size:.6875rem;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;letter-spacing:.01em}.bottom-nav__badge{position:absolute;top:-4px;right:-10px;min-width:18px;height:18px;padding:0 5px;font-size:.625rem;font-weight:600;line-height:18px;text-align:center;color:#fff;background-color:var(--ion-color-danger);border-radius:9px;box-shadow:0 1px 3px #0003}.bottom-nav__badge--dot{min-width:10px;width:10px;height:10px;padding:0;top:-2px;right:-4px;border-radius:50%}.bottom-nav__fab{position:relative;flex-shrink:0;width:var(--bottom-nav-fab-size);height:var(--bottom-nav-fab-size);margin:0 12px;margin-top:calc(var(--bottom-nav-fab-size) * -.35);background:var(--fab-color);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.75rem;box-shadow:0 4px 14px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.4);transition:transform .2s cubic-bezier(.4,0,.2,1),box-shadow .2s ease;overflow:hidden;--ripple-color: rgba(255, 255, 255, .3)}.bottom-nav__fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.5)}.bottom-nav__fab:active{transform:scale(.95)}.bottom-nav__fab:focus-visible{outline:3px solid white;outline-offset:2px}.bottom-nav__fab--small{--bottom-nav-fab-size: 48px;font-size:1.5rem;margin-top:-14.4px}.bottom-nav__fab ion-icon{display:block}.bottom-nav--hide-labels{--bottom-nav-height: 56px}.bottom-nav--hide-labels .bottom-nav__tab-label{display:none}.bottom-nav--hide-labels .bottom-nav__tab-icon{font-size:1.625rem}:host-context(.dark) .bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--translucent,:host-context(body.dark) .bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.85);border-top-color:#ffffff14}:host-context(.dark) .bottom-nav--elevated,:host-context(.ion-palette-dark) .bottom-nav--elevated,:host-context(html.ion-palette-dark) .bottom-nav--elevated,:host-context(body.dark) .bottom-nav--elevated,:host-context([data-theme=dark]) .bottom-nav--elevated{box-shadow:0 -4px 20px #0000004d}:host-context(.dark) .bottom-nav--floating,:host-context(.ion-palette-dark) .bottom-nav--floating,:host-context(html.ion-palette-dark) .bottom-nav--floating,:host-context(body.dark) .bottom-nav--floating,:host-context([data-theme=dark]) .bottom-nav--floating{border-color:#ffffff1a;box-shadow:0 4px 24px #0003,0 8px 32px #00000026}:host-context(.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(body.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.88);border-color:#ffffff1a}:host-context(.dark) .bottom-nav__fab,:host-context(.ion-palette-dark) .bottom-nav__fab,:host-context(html.ion-palette-dark) .bottom-nav__fab,:host-context(body.dark) .bottom-nav__fab,:host-context([data-theme=dark]) .bottom-nav__fab{box-shadow:0 4px 14px #00000080}:host-context(.dark) .bottom-nav__fab:hover,:host-context(.ion-palette-dark) .bottom-nav__fab:hover,:host-context(html.ion-palette-dark) .bottom-nav__fab:hover,:host-context(body.dark) .bottom-nav__fab:hover,:host-context([data-theme=dark]) .bottom-nav__fab:hover{box-shadow:0 6px 20px #0009}@supports (padding-bottom: env(safe-area-inset-bottom)){.bottom-nav--safe-area .bottom-nav__container{height:var(--bottom-nav-height)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$5.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonRippleEffect, selector: "ion-ripple-effect", inputs: ["type"] }] }); }
40272
+ `, isInline: true, styles: [":host{display:block;position:fixed;bottom:0;left:0;right:0;z-index:100;pointer-events:none}.bottom-nav{--bottom-nav-bg: var(--ion-background-color);--bottom-nav-active: var(--ion-color-primary);--bottom-nav-inactive: var(--ion-color-medium);--bottom-nav-radius: 16px 16px 0 0;--bottom-nav-height: 56px;--bottom-nav-fab-size: 56px;--bottom-nav-max-width: var(--val-container-md, 720px);--fab-color: var(--ion-color-primary);pointer-events:auto;background:var(--bottom-nav-bg);border-radius:var(--bottom-nav-radius);height:var(--bottom-nav-height);padding:0 8px;max-width:var(--bottom-nav-max-width);margin:0 auto}@media (min-width: 576px){.bottom-nav{--bottom-nav-height: 64px}}.bottom-nav--elevated{box-shadow:0 -4px 20px #00000014}.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.75);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-top:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.06)}.bottom-nav--floating{margin:0 auto 8px;width:calc(100% - 32px);border-radius:32px;border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08);box-shadow:0 4px 24px #00000014,0 8px 32px #0000000a;--bottom-nav-radius: 32px}.bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.82);backdrop-filter:blur(28px) saturate(200%);-webkit-backdrop-filter:blur(28px) saturate(200%);border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08)}.bottom-nav--floating.bottom-nav--elevated{box-shadow:0 4px 24px #0000001a,0 12px 48px #00000014}.bottom-nav--safe-area{padding-bottom:env(safe-area-inset-bottom,0);height:calc(var(--bottom-nav-height) + env(safe-area-inset-bottom,0))}.bottom-nav--floating.bottom-nav--safe-area{margin-bottom:calc(8px + env(safe-area-inset-bottom,0));padding-bottom:0;height:var(--bottom-nav-height)}.bottom-nav__container{display:flex;align-items:center;justify-content:space-around;height:var(--bottom-nav-height);max-width:500px;margin:0 auto}.bottom-nav__tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;height:100%;padding:8px 4px;background:transparent;border:none;cursor:pointer;position:relative;overflow:hidden;color:var(--bottom-nav-inactive);transition:color .2s ease;max-width:80px;--ripple-color: var(--bottom-nav-active)}.bottom-nav__tab:focus-visible{outline:2px solid var(--bottom-nav-active);outline-offset:-2px;border-radius:8px}.bottom-nav__tab--active{color:var(--bottom-nav-active)}.bottom-nav__tab--disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.bottom-nav__tab[data-animation=scale] .bottom-nav__tab-icon{transition:transform .2s cubic-bezier(.4,0,.2,1)}.bottom-nav__tab[data-animation=scale].bottom-nav__tab--active .bottom-nav__tab-icon{transform:scale(1.15)}.bottom-nav__tab[data-animation=fade]{transition:opacity .2s ease,color .2s ease}.bottom-nav__tab[data-animation=fade]:not(.bottom-nav__tab--active){opacity:.6}.bottom-nav__tab[data-animation=slide] .bottom-nav__tab-label{transition:transform .2s ease,opacity .2s ease;transform:translateY(6px);opacity:0}.bottom-nav__tab[data-animation=slide].bottom-nav__tab--active .bottom-nav__tab-label{transform:translateY(0);opacity:1}.bottom-nav__tab-icon{position:relative;font-size:1.5rem;line-height:1}.bottom-nav__tab-icon ion-icon{display:block}.bottom-nav__tab-label{font-size:.6875rem;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;letter-spacing:.01em}.bottom-nav__badge{position:absolute;top:-4px;right:-10px;min-width:18px;height:18px;padding:0 5px;font-size:.625rem;font-weight:600;line-height:18px;text-align:center;color:#fff;background-color:var(--ion-color-danger);border-radius:9px;box-shadow:0 1px 3px #0003}.bottom-nav__badge--dot{min-width:10px;width:10px;height:10px;padding:0;top:-2px;right:-4px;border-radius:50%}.bottom-nav__fab{position:relative;flex-shrink:0;width:var(--bottom-nav-fab-size);height:var(--bottom-nav-fab-size);margin:0 12px;margin-top:calc(var(--bottom-nav-fab-size) * -.35);background:var(--fab-color);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.75rem;box-shadow:0 4px 14px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.4);transition:transform .2s cubic-bezier(.4,0,.2,1),box-shadow .2s ease;overflow:hidden;--ripple-color: rgba(255, 255, 255, .3)}.bottom-nav__fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.5)}.bottom-nav__fab:active{transform:scale(.95)}.bottom-nav__fab:focus-visible{outline:3px solid white;outline-offset:2px}.bottom-nav__fab--small{--bottom-nav-fab-size: 48px;font-size:1.5rem;margin-top:-14.4px}.bottom-nav__fab ion-icon{display:block}.bottom-nav--hide-labels{--bottom-nav-height: 56px}.bottom-nav--hide-labels .bottom-nav__tab-label{display:none}.bottom-nav--hide-labels .bottom-nav__tab-icon{font-size:1.625rem}:host-context(body.font-large) .bottom-nav{--bottom-nav-height: 56px}:host-context(body.font-large) .bottom-nav .bottom-nav__tab-label{display:none}:host-context(body.font-large) .bottom-nav .bottom-nav__tab-icon{font-size:1.625rem}:host-context(.dark) .bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--translucent,:host-context(body.dark) .bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.85);border-top-color:#ffffff14}:host-context(.dark) .bottom-nav--elevated,:host-context(.ion-palette-dark) .bottom-nav--elevated,:host-context(html.ion-palette-dark) .bottom-nav--elevated,:host-context(body.dark) .bottom-nav--elevated,:host-context([data-theme=dark]) .bottom-nav--elevated{box-shadow:0 -4px 20px #0000004d}:host-context(.dark) .bottom-nav--floating,:host-context(.ion-palette-dark) .bottom-nav--floating,:host-context(html.ion-palette-dark) .bottom-nav--floating,:host-context(body.dark) .bottom-nav--floating,:host-context([data-theme=dark]) .bottom-nav--floating{border-color:#ffffff1a;box-shadow:0 4px 24px #0003,0 8px 32px #00000026}:host-context(.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(body.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.88);border-color:#ffffff1a}:host-context(.dark) .bottom-nav__fab,:host-context(.ion-palette-dark) .bottom-nav__fab,:host-context(html.ion-palette-dark) .bottom-nav__fab,:host-context(body.dark) .bottom-nav__fab,:host-context([data-theme=dark]) .bottom-nav__fab{box-shadow:0 4px 14px #00000080}:host-context(.dark) .bottom-nav__fab:hover,:host-context(.ion-palette-dark) .bottom-nav__fab:hover,:host-context(html.ion-palette-dark) .bottom-nav__fab:hover,:host-context(body.dark) .bottom-nav__fab:hover,:host-context([data-theme=dark]) .bottom-nav__fab:hover{box-shadow:0 6px 20px #0009}@supports (padding-bottom: env(safe-area-inset-bottom)){.bottom-nav--safe-area .bottom-nav__container{height:var(--bottom-nav-height)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$5.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonRippleEffect, selector: "ion-ripple-effect", inputs: ["type"] }] }); }
40273
40273
  }
40274
40274
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BottomNavComponent, decorators: [{
40275
40275
  type: Component,
@@ -40343,7 +40343,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
40343
40343
  }
40344
40344
  </div>
40345
40345
  </nav>
40346
- `, styles: [":host{display:block;position:fixed;bottom:0;left:0;right:0;z-index:100;pointer-events:none}.bottom-nav{--bottom-nav-bg: var(--ion-background-color);--bottom-nav-active: var(--ion-color-primary);--bottom-nav-inactive: var(--ion-color-medium);--bottom-nav-radius: 16px 16px 0 0;--bottom-nav-height: 56px;--bottom-nav-fab-size: 56px;--bottom-nav-max-width: var(--val-container-md, 720px);--fab-color: var(--ion-color-primary);pointer-events:auto;background:var(--bottom-nav-bg);border-radius:var(--bottom-nav-radius);height:var(--bottom-nav-height);padding:0 8px;max-width:var(--bottom-nav-max-width);margin:0 auto}@media (min-width: 576px){.bottom-nav{--bottom-nav-height: 64px}}.bottom-nav--elevated{box-shadow:0 -4px 20px #00000014}.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.75);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-top:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.06)}.bottom-nav--floating{margin:0 auto 8px;width:calc(100% - 32px);border-radius:32px;border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08);box-shadow:0 4px 24px #00000014,0 8px 32px #0000000a;--bottom-nav-radius: 32px}.bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.82);backdrop-filter:blur(28px) saturate(200%);-webkit-backdrop-filter:blur(28px) saturate(200%);border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08)}.bottom-nav--floating.bottom-nav--elevated{box-shadow:0 4px 24px #0000001a,0 12px 48px #00000014}.bottom-nav--safe-area{padding-bottom:env(safe-area-inset-bottom,0);height:calc(var(--bottom-nav-height) + env(safe-area-inset-bottom,0))}.bottom-nav--floating.bottom-nav--safe-area{margin-bottom:calc(8px + env(safe-area-inset-bottom,0));padding-bottom:0;height:var(--bottom-nav-height)}.bottom-nav__container{display:flex;align-items:center;justify-content:space-around;height:var(--bottom-nav-height);max-width:500px;margin:0 auto}.bottom-nav__tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;height:100%;padding:8px 4px;background:transparent;border:none;cursor:pointer;position:relative;overflow:hidden;color:var(--bottom-nav-inactive);transition:color .2s ease;max-width:80px;--ripple-color: var(--bottom-nav-active)}.bottom-nav__tab:focus-visible{outline:2px solid var(--bottom-nav-active);outline-offset:-2px;border-radius:8px}.bottom-nav__tab--active{color:var(--bottom-nav-active)}.bottom-nav__tab--disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.bottom-nav__tab[data-animation=scale] .bottom-nav__tab-icon{transition:transform .2s cubic-bezier(.4,0,.2,1)}.bottom-nav__tab[data-animation=scale].bottom-nav__tab--active .bottom-nav__tab-icon{transform:scale(1.15)}.bottom-nav__tab[data-animation=fade]{transition:opacity .2s ease,color .2s ease}.bottom-nav__tab[data-animation=fade]:not(.bottom-nav__tab--active){opacity:.6}.bottom-nav__tab[data-animation=slide] .bottom-nav__tab-label{transition:transform .2s ease,opacity .2s ease;transform:translateY(6px);opacity:0}.bottom-nav__tab[data-animation=slide].bottom-nav__tab--active .bottom-nav__tab-label{transform:translateY(0);opacity:1}.bottom-nav__tab-icon{position:relative;font-size:1.5rem;line-height:1}.bottom-nav__tab-icon ion-icon{display:block}.bottom-nav__tab-label{font-size:.6875rem;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;letter-spacing:.01em}.bottom-nav__badge{position:absolute;top:-4px;right:-10px;min-width:18px;height:18px;padding:0 5px;font-size:.625rem;font-weight:600;line-height:18px;text-align:center;color:#fff;background-color:var(--ion-color-danger);border-radius:9px;box-shadow:0 1px 3px #0003}.bottom-nav__badge--dot{min-width:10px;width:10px;height:10px;padding:0;top:-2px;right:-4px;border-radius:50%}.bottom-nav__fab{position:relative;flex-shrink:0;width:var(--bottom-nav-fab-size);height:var(--bottom-nav-fab-size);margin:0 12px;margin-top:calc(var(--bottom-nav-fab-size) * -.35);background:var(--fab-color);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.75rem;box-shadow:0 4px 14px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.4);transition:transform .2s cubic-bezier(.4,0,.2,1),box-shadow .2s ease;overflow:hidden;--ripple-color: rgba(255, 255, 255, .3)}.bottom-nav__fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.5)}.bottom-nav__fab:active{transform:scale(.95)}.bottom-nav__fab:focus-visible{outline:3px solid white;outline-offset:2px}.bottom-nav__fab--small{--bottom-nav-fab-size: 48px;font-size:1.5rem;margin-top:-14.4px}.bottom-nav__fab ion-icon{display:block}.bottom-nav--hide-labels{--bottom-nav-height: 56px}.bottom-nav--hide-labels .bottom-nav__tab-label{display:none}.bottom-nav--hide-labels .bottom-nav__tab-icon{font-size:1.625rem}:host-context(.dark) .bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--translucent,:host-context(body.dark) .bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.85);border-top-color:#ffffff14}:host-context(.dark) .bottom-nav--elevated,:host-context(.ion-palette-dark) .bottom-nav--elevated,:host-context(html.ion-palette-dark) .bottom-nav--elevated,:host-context(body.dark) .bottom-nav--elevated,:host-context([data-theme=dark]) .bottom-nav--elevated{box-shadow:0 -4px 20px #0000004d}:host-context(.dark) .bottom-nav--floating,:host-context(.ion-palette-dark) .bottom-nav--floating,:host-context(html.ion-palette-dark) .bottom-nav--floating,:host-context(body.dark) .bottom-nav--floating,:host-context([data-theme=dark]) .bottom-nav--floating{border-color:#ffffff1a;box-shadow:0 4px 24px #0003,0 8px 32px #00000026}:host-context(.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(body.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.88);border-color:#ffffff1a}:host-context(.dark) .bottom-nav__fab,:host-context(.ion-palette-dark) .bottom-nav__fab,:host-context(html.ion-palette-dark) .bottom-nav__fab,:host-context(body.dark) .bottom-nav__fab,:host-context([data-theme=dark]) .bottom-nav__fab{box-shadow:0 4px 14px #00000080}:host-context(.dark) .bottom-nav__fab:hover,:host-context(.ion-palette-dark) .bottom-nav__fab:hover,:host-context(html.ion-palette-dark) .bottom-nav__fab:hover,:host-context(body.dark) .bottom-nav__fab:hover,:host-context([data-theme=dark]) .bottom-nav__fab:hover{box-shadow:0 6px 20px #0009}@supports (padding-bottom: env(safe-area-inset-bottom)){.bottom-nav--safe-area .bottom-nav__container{height:var(--bottom-nav-height)}}\n"] }]
40346
+ `, styles: [":host{display:block;position:fixed;bottom:0;left:0;right:0;z-index:100;pointer-events:none}.bottom-nav{--bottom-nav-bg: var(--ion-background-color);--bottom-nav-active: var(--ion-color-primary);--bottom-nav-inactive: var(--ion-color-medium);--bottom-nav-radius: 16px 16px 0 0;--bottom-nav-height: 56px;--bottom-nav-fab-size: 56px;--bottom-nav-max-width: var(--val-container-md, 720px);--fab-color: var(--ion-color-primary);pointer-events:auto;background:var(--bottom-nav-bg);border-radius:var(--bottom-nav-radius);height:var(--bottom-nav-height);padding:0 8px;max-width:var(--bottom-nav-max-width);margin:0 auto}@media (min-width: 576px){.bottom-nav{--bottom-nav-height: 64px}}.bottom-nav--elevated{box-shadow:0 -4px 20px #00000014}.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.75);backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);border-top:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.06)}.bottom-nav--floating{margin:0 auto 8px;width:calc(100% - 32px);border-radius:32px;border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08);box-shadow:0 4px 24px #00000014,0 8px 32px #0000000a;--bottom-nav-radius: 32px}.bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 255, 255, 255),.82);backdrop-filter:blur(28px) saturate(200%);-webkit-backdrop-filter:blur(28px) saturate(200%);border:1px solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.08)}.bottom-nav--floating.bottom-nav--elevated{box-shadow:0 4px 24px #0000001a,0 12px 48px #00000014}.bottom-nav--safe-area{padding-bottom:env(safe-area-inset-bottom,0);height:calc(var(--bottom-nav-height) + env(safe-area-inset-bottom,0))}.bottom-nav--floating.bottom-nav--safe-area{margin-bottom:calc(8px + env(safe-area-inset-bottom,0));padding-bottom:0;height:var(--bottom-nav-height)}.bottom-nav__container{display:flex;align-items:center;justify-content:space-around;height:var(--bottom-nav-height);max-width:500px;margin:0 auto}.bottom-nav__tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;height:100%;padding:8px 4px;background:transparent;border:none;cursor:pointer;position:relative;overflow:hidden;color:var(--bottom-nav-inactive);transition:color .2s ease;max-width:80px;--ripple-color: var(--bottom-nav-active)}.bottom-nav__tab:focus-visible{outline:2px solid var(--bottom-nav-active);outline-offset:-2px;border-radius:8px}.bottom-nav__tab--active{color:var(--bottom-nav-active)}.bottom-nav__tab--disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.bottom-nav__tab[data-animation=scale] .bottom-nav__tab-icon{transition:transform .2s cubic-bezier(.4,0,.2,1)}.bottom-nav__tab[data-animation=scale].bottom-nav__tab--active .bottom-nav__tab-icon{transform:scale(1.15)}.bottom-nav__tab[data-animation=fade]{transition:opacity .2s ease,color .2s ease}.bottom-nav__tab[data-animation=fade]:not(.bottom-nav__tab--active){opacity:.6}.bottom-nav__tab[data-animation=slide] .bottom-nav__tab-label{transition:transform .2s ease,opacity .2s ease;transform:translateY(6px);opacity:0}.bottom-nav__tab[data-animation=slide].bottom-nav__tab--active .bottom-nav__tab-label{transform:translateY(0);opacity:1}.bottom-nav__tab-icon{position:relative;font-size:1.5rem;line-height:1}.bottom-nav__tab-icon ion-icon{display:block}.bottom-nav__tab-label{font-size:.6875rem;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;letter-spacing:.01em}.bottom-nav__badge{position:absolute;top:-4px;right:-10px;min-width:18px;height:18px;padding:0 5px;font-size:.625rem;font-weight:600;line-height:18px;text-align:center;color:#fff;background-color:var(--ion-color-danger);border-radius:9px;box-shadow:0 1px 3px #0003}.bottom-nav__badge--dot{min-width:10px;width:10px;height:10px;padding:0;top:-2px;right:-4px;border-radius:50%}.bottom-nav__fab{position:relative;flex-shrink:0;width:var(--bottom-nav-fab-size);height:var(--bottom-nav-fab-size);margin:0 12px;margin-top:calc(var(--bottom-nav-fab-size) * -.35);background:var(--fab-color);border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.75rem;box-shadow:0 4px 14px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.4);transition:transform .2s cubic-bezier(.4,0,.2,1),box-shadow .2s ease;overflow:hidden;--ripple-color: rgba(255, 255, 255, .3)}.bottom-nav__fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(var(--ion-color-primary-rgb, 56, 128, 255),.5)}.bottom-nav__fab:active{transform:scale(.95)}.bottom-nav__fab:focus-visible{outline:3px solid white;outline-offset:2px}.bottom-nav__fab--small{--bottom-nav-fab-size: 48px;font-size:1.5rem;margin-top:-14.4px}.bottom-nav__fab ion-icon{display:block}.bottom-nav--hide-labels{--bottom-nav-height: 56px}.bottom-nav--hide-labels .bottom-nav__tab-label{display:none}.bottom-nav--hide-labels .bottom-nav__tab-icon{font-size:1.625rem}:host-context(body.font-large) .bottom-nav{--bottom-nav-height: 56px}:host-context(body.font-large) .bottom-nav .bottom-nav__tab-label{display:none}:host-context(body.font-large) .bottom-nav .bottom-nav__tab-icon{font-size:1.625rem}:host-context(.dark) .bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--translucent,:host-context(body.dark) .bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.85);border-top-color:#ffffff14}:host-context(.dark) .bottom-nav--elevated,:host-context(.ion-palette-dark) .bottom-nav--elevated,:host-context(html.ion-palette-dark) .bottom-nav--elevated,:host-context(body.dark) .bottom-nav--elevated,:host-context([data-theme=dark]) .bottom-nav--elevated{box-shadow:0 -4px 20px #0000004d}:host-context(.dark) .bottom-nav--floating,:host-context(.ion-palette-dark) .bottom-nav--floating,:host-context(html.ion-palette-dark) .bottom-nav--floating,:host-context(body.dark) .bottom-nav--floating,:host-context([data-theme=dark]) .bottom-nav--floating{border-color:#ffffff1a;box-shadow:0 4px 24px #0003,0 8px 32px #00000026}:host-context(.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(html.ion-palette-dark) .bottom-nav--floating.bottom-nav--translucent,:host-context(body.dark) .bottom-nav--floating.bottom-nav--translucent,:host-context([data-theme=dark]) .bottom-nav--floating.bottom-nav--translucent{background:rgba(var(--ion-background-color-rgb, 18, 18, 18),.88);border-color:#ffffff1a}:host-context(.dark) .bottom-nav__fab,:host-context(.ion-palette-dark) .bottom-nav__fab,:host-context(html.ion-palette-dark) .bottom-nav__fab,:host-context(body.dark) .bottom-nav__fab,:host-context([data-theme=dark]) .bottom-nav__fab{box-shadow:0 4px 14px #00000080}:host-context(.dark) .bottom-nav__fab:hover,:host-context(.ion-palette-dark) .bottom-nav__fab:hover,:host-context(html.ion-palette-dark) .bottom-nav__fab:hover,:host-context(body.dark) .bottom-nav__fab:hover,:host-context([data-theme=dark]) .bottom-nav__fab:hover{box-shadow:0 6px 20px #0009}@supports (padding-bottom: env(safe-area-inset-bottom)){.bottom-nav--safe-area .bottom-nav__container{height:var(--bottom-nav-height)}}\n"] }]
40347
40347
  }], propDecorators: { tabClick: [{
40348
40348
  type: Output
40349
40349
  }], fabClick: [{
@@ -41652,7 +41652,7 @@ const PROFILE_VIEW_I18N = {
41652
41652
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41653
41653
  const HANDLE_MIN = 3;
41654
41654
  const HANDLE_MAX = 20;
41655
- const DEFAULT_NAMESPACE$1 = 'Settings.Profile';
41655
+ const DEFAULT_NAMESPACE$2 = 'Settings.Profile';
41656
41656
  /**
41657
41657
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41658
41658
  *
@@ -41702,7 +41702,7 @@ class ProfileViewComponent {
41702
41702
  showPhone: merged.showPhone ?? true,
41703
41703
  showShareCta: merged.showShareCta ?? true,
41704
41704
  profileBaseUrl: merged.profileBaseUrl ?? '',
41705
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
41705
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
41706
41706
  onSaved: merged.onSaved,
41707
41707
  onAvatarUploaded: merged.onAvatarUploaded,
41708
41708
  };
@@ -42342,6 +42342,8 @@ class FontSizeService {
42342
42342
  if (!this.isBrowser)
42343
42343
  return;
42344
42344
  document.documentElement.style.fontSize = SIZE_PX[size];
42345
+ document.body.classList.toggle('font-large', size === FontSizeOption.LARGE);
42346
+ document.body.classList.toggle('font-small', size === FontSizeOption.SMALL);
42345
42347
  }
42346
42348
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FontSizeService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
42347
42349
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FontSizeService, providedIn: 'root' }); }
@@ -42616,7 +42618,7 @@ const PREFERENCES_VIEW_I18N = {
42616
42618
  },
42617
42619
  };
42618
42620
 
42619
- const DEFAULT_NAMESPACE = 'Settings.General';
42621
+ const DEFAULT_NAMESPACE$1 = 'Settings.General';
42620
42622
  const DEFAULT_LANGUAGES = ['es', 'en'];
42621
42623
  /**
42622
42624
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42653,7 +42655,7 @@ class PreferencesViewComponent {
42653
42655
  showLanguage: merged.showLanguage ?? true,
42654
42656
  showFontSize: merged.showFontSize ?? true,
42655
42657
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42656
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
42658
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
42657
42659
  onThemeChanged: merged.onThemeChanged,
42658
42660
  onLanguageChanged: merged.onLanguageChanged,
42659
42661
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -43357,387 +43359,2308 @@ function provideValtechSettingsRoutes(opts) {
43357
43359
  ];
43358
43360
  }
43359
43361
 
43360
- /** Built-in label sets for the three platform locales. */
43361
- const CALLOUT_LABELS = {
43362
- es: {
43363
- NOTE: 'Nota',
43364
- TIP: 'Tip',
43365
- INFO: 'Info',
43366
- IMPORTANT: 'Importante',
43367
- WARNING: 'Atención',
43368
- CAUTION: 'Precaución',
43369
- },
43370
- en: {
43371
- NOTE: 'Note',
43372
- TIP: 'Tip',
43373
- INFO: 'Info',
43374
- IMPORTANT: 'Important',
43375
- WARNING: 'Warning',
43376
- CAUTION: 'Caution',
43377
- },
43378
- pt: {
43379
- NOTE: 'Nota',
43380
- TIP: 'Dica',
43381
- INFO: 'Info',
43382
- IMPORTANT: 'Importante',
43383
- WARNING: 'Atenção',
43384
- CAUTION: 'Cuidado',
43385
- },
43386
- };
43387
- /**
43388
- * Per-callout-kind color (Ionic semantic color). Fixed across locales since
43389
- * red = danger no matter what language you speak.
43390
- */
43391
- const CALLOUT_COLORS = {
43392
- NOTE: 'primary',
43393
- TIP: 'success',
43394
- INFO: 'tertiary',
43395
- IMPORTANT: 'warning',
43396
- WARNING: 'warning',
43397
- CAUTION: 'danger',
43398
- };
43399
- const BOX_DRAWING = /[┌┐└┘├┤┬┴┼─│╔╗╚╝═║]/;
43400
- const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT)\]\s*(.*)$/i;
43401
- const FENCE_RE = /^```([\w-]*)\s*$/;
43402
- const HEADING_RE = /^(#{1,6})\s+(.+)$/;
43403
- const SEPARATOR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
43404
- const UNORDERED_RE = /^\s*[-*+]\s+(.+)$/;
43405
- const CHECKLIST_RE = /^\s*[-*+]\s+\[([ xX])\]\s+(.+)$/;
43406
- const ORDERED_RE = /^\s*\d+\.\s+(.+)$/;
43407
- const TABLE_DIVIDER_RE = /^\s*\|?[\s:|-]+\|?\s*$/;
43408
- const TABLE_ROW_RE = /^\s*\|(.+)\|\s*$/;
43362
+ addIcons({ shieldCheckmarkOutline, checkmarkCircleOutline });
43409
43363
  /**
43410
- * Pure Markdown ArticleMetadata parser. No Angular deps usable from Node scripts
43411
- * (build-time generation) and from the Angular `MarkdownArticleParserService` wrapper.
43364
+ * `val-login-attempt-modal` modal de confirmación de intento de inicio de sesión.
43412
43365
  *
43413
- * Supported syntax:
43414
- * - Headings (#, ##, ### …) title / subtitle
43415
- * - Paragraphs paragraph (with `processLinks` + `allowPartialBold`)
43416
- * - Lists (-, *, 1.) → unordered / ordered list (- [ ] / - [x] for checklist)
43417
- * - Blockquotes (>) → quote, with GitHub callouts `> [!NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT]` mapped to notes
43418
- * - Code fences (```lang) code
43419
- * - Box-drawing ASCII art (┌┐└┘│─) auto-wrapped as code
43420
- * - Tables flattened into paragraphs with bold keys
43421
- * - Horizontal rules (---, ***, ___) → separator
43366
+ * Se abre cuando el usuario llega a la vista Seguridad vía
43367
+ * `?token=...` desde el email/push de "nuevo acceso". Muestra el dispositivo
43368
+ * detectado y dos acciones: confirmar que fue uno mismo (cierra sin hacer nada)
43369
+ * o bloquear el dispositivo (ejecuta el token de acción).
43370
+ *
43371
+ * Inputs vía `componentProps` del ModalService. Sin `@Output` (no funcionan con
43372
+ * `modalController.create()`) usa callback `onBlock` y `_modalRef.dismiss`.
43373
+ * Recibe los labels ya traducidos (agnóstico a i18n).
43374
+ *
43375
+ * Header canónico (Regla #5): sin `ion-title`, botón Cerrar texto en `slot=end`,
43376
+ * título en el body con `val-display`.
43422
43377
  */
43423
- function parseMarkdownArticle(markdown, options) {
43424
- const lines = normalize(markdown).split('\n');
43425
- const elements = [];
43426
- const labels = options?.calloutLabels ??
43427
- (options?.locale ? CALLOUT_LABELS[options.locale] : CALLOUT_LABELS.es);
43428
- let i = 0;
43429
- while (i < lines.length) {
43430
- const line = lines[i];
43431
- if (line.trim() === '') {
43432
- i++;
43433
- continue;
43434
- }
43435
- const fence = line.match(FENCE_RE);
43436
- if (fence) {
43437
- const lang = fence[1] || undefined;
43438
- const body = [];
43439
- i++;
43440
- while (i < lines.length && !FENCE_RE.test(lines[i])) {
43441
- body.push(lines[i]);
43442
- i++;
43443
- }
43444
- i++;
43445
- elements.push({
43446
- type: 'code',
43447
- props: { code: body.join('\n'), language: lang, theme: 'dark' },
43448
- });
43449
- continue;
43450
- }
43451
- if (BOX_DRAWING.test(line)) {
43452
- const body = [];
43453
- while (i < lines.length && (lines[i].trim() === '' || BOX_DRAWING.test(lines[i]))) {
43454
- body.push(lines[i]);
43455
- i++;
43456
- }
43457
- while (body.length && body[body.length - 1].trim() === '')
43458
- body.pop();
43459
- elements.push({
43460
- type: 'code',
43461
- props: { code: body.join('\n'), language: 'text', theme: 'dark' },
43462
- });
43463
- continue;
43464
- }
43465
- if (SEPARATOR_RE.test(line)) {
43466
- elements.push({ type: 'separator', props: { style: 'line' } });
43467
- i++;
43468
- continue;
43469
- }
43470
- const heading = line.match(HEADING_RE);
43471
- if (heading) {
43472
- elements.push(makeHeading(heading[1].length, heading[2].trim()));
43473
- i++;
43474
- continue;
43475
- }
43476
- if (line.startsWith('>')) {
43477
- const block = [];
43478
- while (i < lines.length && lines[i].startsWith('>')) {
43479
- block.push(lines[i]);
43480
- i++;
43481
- }
43482
- elements.push(makeQuoteOrCallout(block, labels));
43483
- continue;
43484
- }
43485
- if (TABLE_ROW_RE.test(line)) {
43486
- const rows = [];
43487
- while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
43488
- rows.push(lines[i]);
43489
- i++;
43490
- }
43491
- elements.push(...makeTable(rows));
43492
- continue;
43493
- }
43494
- if (CHECKLIST_RE.test(line) || UNORDERED_RE.test(line) || ORDERED_RE.test(line)) {
43495
- const listType = CHECKLIST_RE.test(line)
43496
- ? 'checklist'
43497
- : ORDERED_RE.test(line)
43498
- ? 'ordered'
43499
- : 'unordered';
43500
- const items = [];
43501
- while (i < lines.length && lines[i].trim() !== '') {
43502
- const cur = lines[i];
43503
- const check = cur.match(CHECKLIST_RE);
43504
- const unord = cur.match(UNORDERED_RE);
43505
- const ord = cur.match(ORDERED_RE);
43506
- if (check)
43507
- items.push({ text: processInlineBold(check[2].trim()) });
43508
- else if (ord && listType === 'ordered')
43509
- items.push({ text: processInlineBold(ord[1].trim()) });
43510
- else if (unord && listType !== 'ordered')
43511
- items.push({ text: processInlineBold(unord[1].trim()) });
43512
- else
43513
- break;
43514
- i++;
43515
- }
43516
- elements.push({ type: 'list', props: { items, listType } });
43517
- continue;
43378
+ class LoginAttemptModalComponent {
43379
+ constructor() {
43380
+ /** Título del modal (i18n ya resuelto). */
43381
+ this.title = '';
43382
+ /** Texto explicativo. */
43383
+ this.body = '';
43384
+ /** Descripción del dispositivo (navegador · OS · ubicación). */
43385
+ this.deviceLabel = '';
43386
+ /** Metadata secundaria (fecha, IP). Opcional. */
43387
+ this.metaLabel = '';
43388
+ /** Mensaje mostrado tras bloquear. */
43389
+ this.doneLabel = '';
43390
+ /** Labels de botones. */
43391
+ this.mineLabel = '';
43392
+ this.blockLabel = '';
43393
+ this.closeLabel = '';
43394
+ /** `true` si el dispositivo es móvil — cambia el icono. */
43395
+ this.isMobile = false;
43396
+ /** `true` tras bloquear con éxito — cambia el modal a estado "hecho". */
43397
+ this.done = signal(false);
43398
+ this.working = false;
43399
+ }
43400
+ get deviceIcon() {
43401
+ return this.isMobile ? 'phone-portrait-outline' : 'laptop-outline';
43402
+ }
43403
+ /** "Sí, fui yo" — cierra sin acción. El dispositivo ya está activo. */
43404
+ confirmMine() {
43405
+ this._modalRef?.dismiss(undefined, 'mine');
43406
+ }
43407
+ /** "No fui yo" — ejecuta el bloqueo y pasa a estado done. */
43408
+ async confirmBlock() {
43409
+ if (this.working || !this.onBlock)
43410
+ return;
43411
+ this.working = true;
43412
+ try {
43413
+ await this.onBlock();
43414
+ this.done.set(true);
43518
43415
  }
43519
- const paragraph = [];
43520
- while (i < lines.length && lines[i].trim() !== '' && !startsNewBlock(lines[i])) {
43521
- paragraph.push(lines[i]);
43522
- i++;
43416
+ catch {
43417
+ // El parent muestra el toast de error; cerramos el modal.
43418
+ this._modalRef?.dismiss(undefined, 'error');
43523
43419
  }
43524
- const text = processInlineBold(paragraph.join(' ').trim());
43525
- if (text) {
43526
- elements.push({
43527
- type: 'paragraph',
43528
- props: {
43529
- content: text,
43530
- size: 'medium',
43531
- color: 'dark',
43532
- bold: false,
43533
- processLinks: true,
43534
- allowPartialBold: true,
43535
- },
43536
- });
43420
+ finally {
43421
+ this.working = false;
43537
43422
  }
43538
43423
  }
43539
- // Strip parser-only options before merging into ArticleMetadata
43540
- const { locale: _l, calloutLabels: _c, ...metadataOverrides } = options ?? {};
43541
- return {
43542
- elements,
43543
- maxWidth: '900px',
43544
- centered: true,
43545
- theme: 'auto',
43546
- ...metadataOverrides,
43547
- };
43548
- }
43549
- function normalize(md) {
43550
- return md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
43551
- }
43552
- function startsNewBlock(line) {
43553
- return (HEADING_RE.test(line) ||
43554
- FENCE_RE.test(line) ||
43555
- SEPARATOR_RE.test(line) ||
43556
- TABLE_ROW_RE.test(line) ||
43557
- UNORDERED_RE.test(line) ||
43558
- ORDERED_RE.test(line) ||
43559
- CHECKLIST_RE.test(line) ||
43560
- line.startsWith('>') ||
43561
- BOX_DRAWING.test(line));
43562
- }
43563
- function processInlineBold(text) {
43564
- return text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
43565
- }
43566
- function makeHeading(level, content) {
43567
- if (level === 1) {
43424
+ close() {
43425
+ this._modalRef?.dismiss(undefined, this.done() ? 'blocked' : 'close');
43426
+ }
43427
+ get mineBtn() {
43568
43428
  return {
43569
- type: 'title',
43570
- props: { content, size: 'xlarge', color: 'dark', bold: true },
43429
+ text: this.mineLabel,
43430
+ color: 'medium',
43431
+ fill: 'outline',
43432
+ size: 'default',
43433
+ type: 'button',
43434
+ state: ComponentStates.ENABLED,
43571
43435
  };
43572
43436
  }
43573
- const size = level === 2 ? 'large' : level === 3 ? 'medium' : 'small';
43574
- return {
43575
- type: 'subtitle',
43576
- props: { content, size, color: 'dark', bold: true },
43577
- };
43578
- }
43579
- function makeQuoteOrCallout(block, labels) {
43580
- const first = block[0];
43581
- const callout = first.match(CALLOUT_RE);
43582
- const lines = block.map(l => l.replace(/^>\s?/, ''));
43583
- if (callout) {
43584
- const type = callout[1].toUpperCase();
43585
- const firstLineRest = callout[2] || '';
43586
- const rest = lines.slice(1).join(' ').trim();
43587
- const text = processInlineBold([firstLineRest, rest].filter(Boolean).join(' ').trim());
43588
- return makeNote(type, text, labels);
43437
+ get blockBtn() {
43438
+ return {
43439
+ text: this.blockLabel,
43440
+ color: 'danger',
43441
+ fill: 'solid',
43442
+ size: 'default',
43443
+ type: 'button',
43444
+ state: ComponentStates.ENABLED,
43445
+ };
43589
43446
  }
43590
- const text = processInlineBold(lines.join(' ').trim());
43591
- return {
43592
- type: 'quote',
43593
- props: {
43594
- content: text,
43447
+ get closeBtn() {
43448
+ return {
43449
+ text: this.closeLabel,
43450
+ color: 'dark',
43451
+ fill: 'solid',
43452
+ size: 'default',
43453
+ type: 'button',
43454
+ state: ComponentStates.ENABLED,
43455
+ };
43456
+ }
43457
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginAttemptModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
43458
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LoginAttemptModalComponent, isStandalone: true, selector: "val-login-attempt-modal", inputs: { title: "title", body: "body", deviceLabel: "deviceLabel", metaLabel: "metaLabel", doneLabel: "doneLabel", mineLabel: "mineLabel", blockLabel: "blockLabel", closeLabel: "closeLabel", isMobile: "isMobile", onBlock: "onBlock", _modalRef: "_modalRef" }, ngImport: i0, template: `
43459
+ <ion-header>
43460
+ <ion-toolbar>
43461
+ <ion-buttons slot="end">
43462
+ <ion-button fill="clear" color="dark" (click)="close()">
43463
+ <strong>{{ closeLabel }}</strong>
43464
+ </ion-button>
43465
+ </ion-buttons>
43466
+ </ion-toolbar>
43467
+ </ion-header>
43468
+ <ion-content class="ion-padding">
43469
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
43470
+ <div class="attempt">
43471
+ <div class="attempt__hero">
43472
+ <ion-icon name="shield-checkmark-outline" aria-hidden="true" />
43473
+ </div>
43474
+
43475
+ <val-text
43476
+ [props]="{
43477
+ content: body,
43595
43478
  size: 'medium',
43596
- color: 'medium',
43479
+ color: 'dark',
43597
43480
  bold: false,
43598
- showQuoteMark: true,
43599
- alignment: 'left',
43600
- },
43601
- };
43481
+ }"
43482
+ />
43483
+
43484
+ <div class="attempt__device">
43485
+ <ion-icon [name]="deviceIcon" aria-hidden="true" />
43486
+ <div class="attempt__device-body">
43487
+ <val-text
43488
+ [props]="{
43489
+ content: deviceLabel,
43490
+ size: 'medium',
43491
+ color: 'dark',
43492
+ bold: true,
43493
+ }"
43494
+ />
43495
+ @if (metaLabel) {
43496
+ <val-text
43497
+ [props]="{
43498
+ content: metaLabel,
43499
+ size: 'small',
43500
+ color: 'medium',
43501
+ bold: false,
43502
+ }"
43503
+ />
43504
+ }
43505
+ </div>
43506
+ </div>
43507
+
43508
+ @if (done()) {
43509
+ <div class="attempt__done">
43510
+ <ion-icon name="checkmark-circle-outline" aria-hidden="true" />
43511
+ <val-text
43512
+ [props]="{
43513
+ content: doneLabel,
43514
+ size: 'small',
43515
+ color: 'medium',
43516
+ bold: false,
43517
+ }"
43518
+ />
43519
+ </div>
43520
+ }
43521
+ </div>
43522
+ </ion-content>
43523
+
43524
+ @if (!done()) {
43525
+ <div class="attempt__actions">
43526
+ <val-button [props]="mineBtn" (click)="confirmMine()" />
43527
+ <val-button [props]="blockBtn" (click)="confirmBlock()" />
43528
+ </div>
43529
+ } @else {
43530
+ <div class="attempt__actions">
43531
+ <val-button [props]="closeBtn" (click)="close()" />
43532
+ </div>
43533
+ }
43534
+ `, isInline: true, styles: [":host{display:block}.attempt{display:flex;flex-direction:column;gap:18px;align-items:flex-start}.attempt__hero{width:56px;height:56px;border-radius:16px;display:flex;align-items:center;justify-content:center;background:#7026df1a}.attempt__hero ion-icon{font-size:30px;color:var(--ion-color-primary, #7026df)}.attempt__device{display:flex;align-items:center;gap:12px;width:100%;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.attempt__device ion-icon{font-size:26px;color:var(--ion-color-dark);flex-shrink:0}.attempt__device-body{display:flex;flex-direction:column;gap:2px;min-width:0}.attempt__done{display:flex;align-items:center;gap:8px}.attempt__done ion-icon{font-size:20px;color:var(--ion-color-success);flex-shrink:0}.attempt__actions{display:flex;gap:10px;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.attempt__actions val-button{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: 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: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
43602
43535
  }
43603
- function makeNote(kind, text, labels) {
43604
- return {
43605
- type: 'note',
43606
- props: {
43607
- text,
43608
- prefix: `${labels[kind]}:`,
43609
- color: CALLOUT_COLORS[kind],
43610
- textColor: 'dark',
43536
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoginAttemptModalComponent, decorators: [{
43537
+ type: Component,
43538
+ args: [{ selector: 'val-login-attempt-modal', standalone: true, imports: [
43539
+ CommonModule,
43540
+ IonContent,
43541
+ IonHeader,
43542
+ IonToolbar,
43543
+ IonButtons,
43544
+ IonButton,
43545
+ IonIcon,
43546
+ DisplayComponent,
43547
+ TextComponent,
43548
+ ButtonComponent,
43549
+ ], template: `
43550
+ <ion-header>
43551
+ <ion-toolbar>
43552
+ <ion-buttons slot="end">
43553
+ <ion-button fill="clear" color="dark" (click)="close()">
43554
+ <strong>{{ closeLabel }}</strong>
43555
+ </ion-button>
43556
+ </ion-buttons>
43557
+ </ion-toolbar>
43558
+ </ion-header>
43559
+ <ion-content class="ion-padding">
43560
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
43561
+ <div class="attempt">
43562
+ <div class="attempt__hero">
43563
+ <ion-icon name="shield-checkmark-outline" aria-hidden="true" />
43564
+ </div>
43565
+
43566
+ <val-text
43567
+ [props]="{
43568
+ content: body,
43611
43569
  size: 'medium',
43612
- rounded: true,
43613
- allowPartialBold: true,
43614
- processLinks: true,
43615
- },
43616
- };
43617
- }
43618
- function makeTable(rows) {
43619
- const parsed = rows
43620
- .filter(r => !TABLE_DIVIDER_RE.test(r))
43621
- .map(r => r
43622
- .trim()
43623
- .replace(/^\|/, '')
43624
- .replace(/\|$/, '')
43625
- .split('|')
43626
- .map(c => c.trim()));
43627
- if (parsed.length === 0)
43628
- return [];
43629
- const header = parsed[0];
43630
- const dataRows = parsed.slice(1);
43631
- if (dataRows.length === 0) {
43632
- return [
43633
- {
43634
- type: 'paragraph',
43635
- props: {
43636
- content: header.join(' · '),
43637
- size: 'medium',
43638
- color: 'dark',
43639
- bold: false,
43640
- processLinks: true,
43641
- allowPartialBold: true,
43642
- },
43643
- },
43644
- ];
43645
- }
43646
- return dataRows.map(row => {
43647
- const pairs = row.map((cell, idx) => {
43648
- const key = header[idx] ?? '';
43649
- return key ? `<strong>${key}:</strong> ${cell}` : cell;
43650
- });
43651
- return {
43652
- type: 'paragraph',
43653
- props: {
43654
- content: pairs.join(' · '),
43570
+ color: 'dark',
43571
+ bold: false,
43572
+ }"
43573
+ />
43574
+
43575
+ <div class="attempt__device">
43576
+ <ion-icon [name]="deviceIcon" aria-hidden="true" />
43577
+ <div class="attempt__device-body">
43578
+ <val-text
43579
+ [props]="{
43580
+ content: deviceLabel,
43655
43581
  size: 'medium',
43656
43582
  color: 'dark',
43583
+ bold: true,
43584
+ }"
43585
+ />
43586
+ @if (metaLabel) {
43587
+ <val-text
43588
+ [props]="{
43589
+ content: metaLabel,
43590
+ size: 'small',
43591
+ color: 'medium',
43592
+ bold: false,
43593
+ }"
43594
+ />
43595
+ }
43596
+ </div>
43597
+ </div>
43598
+
43599
+ @if (done()) {
43600
+ <div class="attempt__done">
43601
+ <ion-icon name="checkmark-circle-outline" aria-hidden="true" />
43602
+ <val-text
43603
+ [props]="{
43604
+ content: doneLabel,
43605
+ size: 'small',
43606
+ color: 'medium',
43657
43607
  bold: false,
43658
- processLinks: true,
43659
- allowPartialBold: true,
43660
- },
43661
- };
43662
- });
43663
- }
43608
+ }"
43609
+ />
43610
+ </div>
43611
+ }
43612
+ </div>
43613
+ </ion-content>
43614
+
43615
+ @if (!done()) {
43616
+ <div class="attempt__actions">
43617
+ <val-button [props]="mineBtn" (click)="confirmMine()" />
43618
+ <val-button [props]="blockBtn" (click)="confirmBlock()" />
43619
+ </div>
43620
+ } @else {
43621
+ <div class="attempt__actions">
43622
+ <val-button [props]="closeBtn" (click)="close()" />
43623
+ </div>
43624
+ }
43625
+ `, styles: [":host{display:block}.attempt{display:flex;flex-direction:column;gap:18px;align-items:flex-start}.attempt__hero{width:56px;height:56px;border-radius:16px;display:flex;align-items:center;justify-content:center;background:#7026df1a}.attempt__hero ion-icon{font-size:30px;color:var(--ion-color-primary, #7026df)}.attempt__device{display:flex;align-items:center;gap:12px;width:100%;padding:14px 16px;border-radius:12px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.attempt__device ion-icon{font-size:26px;color:var(--ion-color-dark);flex-shrink:0}.attempt__device-body{display:flex;flex-direction:column;gap:2px;min-width:0}.attempt__done{display:flex;align-items:center;gap:8px}.attempt__done ion-icon{font-size:20px;color:var(--ion-color-success);flex-shrink:0}.attempt__actions{display:flex;gap:10px;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.attempt__actions val-button{flex:1}\n"] }]
43626
+ }], propDecorators: { title: [{
43627
+ type: Input
43628
+ }], body: [{
43629
+ type: Input
43630
+ }], deviceLabel: [{
43631
+ type: Input
43632
+ }], metaLabel: [{
43633
+ type: Input
43634
+ }], doneLabel: [{
43635
+ type: Input
43636
+ }], mineLabel: [{
43637
+ type: Input
43638
+ }], blockLabel: [{
43639
+ type: Input
43640
+ }], closeLabel: [{
43641
+ type: Input
43642
+ }], isMobile: [{
43643
+ type: Input
43644
+ }], onBlock: [{
43645
+ type: Input
43646
+ }], _modalRef: [{
43647
+ type: Input
43648
+ }] } });
43664
43649
 
43665
43650
  /**
43666
- * `val-faq`
43651
+ * `val-session-list-modal` — modal de "Ver todas las sesiones activas". Organism
43652
+ * compartido entre la vista Seguridad y la vista Cuenta del factory.
43667
43653
  *
43668
- * Organism de preguntas frecuentes. Q&A categorizado con accordion colapsable
43669
- * y buscador en vivo. Pensado para vistas FAQ públicas — el caller puede
43670
- * reusar la misma data (`FaqMetadata.categories`) para emitir el structured
43671
- * data `FAQPage` (JSON-LD) y ganar rich results en buscadores.
43654
+ * Render reusa el mismo markup de la sección de sesiones inline (mismas clases
43655
+ * CSS) pero sin truncar la lista.
43672
43656
  *
43673
- * Las respuestas (`FaqItem.answer`) son **Markdown** — se parsean con
43674
- * `parseMarkdownArticle` y se renderizan vía `val-article`. Soportan links,
43675
- * listas, negritas, tablas, etc.
43657
+ * Inputs (vienen de `componentProps` del ModalService):
43658
+ * - `sessions` snapshot inicial (mostrado al abrir)
43659
+ * - `thisLabel` / `revokeLabel` — i18n strings ya resueltas
43660
+ * - `onRevoke` — callback async que el parent ejecuta contra el backend; debe
43661
+ * retornar la lista actualizada para que el modal refresque en sitio.
43676
43662
  *
43677
- * @example
43678
- * ```html
43679
- * <val-faq [props]="{ categories: faqCategories, searchable: true }" />
43680
- * ```
43663
+ * `@Output` events NO funcionan con `modalController.create()` — por eso usamos
43664
+ * callback pasado por props + `_modalRef` para dismiss desde dentro.
43665
+ *
43666
+ * Header canónico (Regla #5): sin `ion-title`, botón Cerrar texto en `slot=end`,
43667
+ * título en el body con `val-display`.
43681
43668
  */
43682
- class FaqComponent {
43669
+ class SessionListModalComponent {
43683
43670
  constructor() {
43684
- this.i18n = inject(I18nService);
43685
- this.props_ = signal({ categories: [] });
43686
- /** Término de búsqueda actual. */
43687
- this.query = signal('');
43688
- /**
43689
- * Cache de respuestas parseadas (Markdown → ArticleMetadata). Se reconstruye
43690
- * solo cuando cambian las categorías — el parseo NO corre por keystroke.
43691
- * Keyed por id de item (los ids son únicos en toda la FAQ).
43692
- */
43693
- this.parsedAnswers = computed(() => {
43694
- const locale = (this.i18n.lang() ?? 'es');
43695
- const map = new Map();
43696
- for (const cat of this.props_().categories ?? []) {
43697
- for (const item of cat.items) {
43698
- map.set(item.id, parseMarkdownArticle(item.answer, { locale }));
43699
- }
43700
- }
43701
- return map;
43702
- });
43703
- /**
43704
- * Categorías visibles tras el filtro. Una categoría desaparece si ninguna
43705
- * de sus preguntas matchea. Match sobre pregunta + respuesta (raw markdown),
43706
- * case-insensitive.
43707
- */
43708
- this.visibleCategories = computed(() => {
43709
- const q = this.query().trim().toLowerCase();
43710
- const cats = this.props_().categories ?? [];
43711
- if (!q)
43712
- return cats;
43713
- return cats
43714
- .map(cat => ({
43715
- ...cat,
43716
- items: cat.items.filter(it => it.question.toLowerCase().includes(q) || it.answer.toLowerCase().includes(q)),
43717
- }))
43718
- .filter(cat => cat.items.length > 0);
43719
- });
43720
- /** `true` si se debe mostrar el label de cada categoría. */
43721
- this.showCategoryLabel = computed(() => {
43722
- const p = this.props_();
43723
- if (p.hideSingleCategoryLabel === false)
43724
- return true;
43725
- return (p.categories?.length ?? 0) > 1;
43726
- });
43727
- }
43728
- set props(value) {
43729
- if (value)
43730
- this.props_.set(value);
43731
- }
43732
- get props() {
43733
- return this.props_();
43671
+ /** Título del modal (ya traducido). */
43672
+ this.title = '';
43673
+ /** Label del botón cerrar del header (ya traducido). */
43674
+ this.closeLabel = '';
43675
+ /** Label "Esta sesión" (en idioma activo). */
43676
+ this.thisLabel = '';
43677
+ /** Label del botón revoke individual. */
43678
+ this.revokeLabel = '';
43679
+ /** Lista local mutable — refrescada después de cada revoke confirmado. */
43680
+ this.sessionsLocal = signal([]);
43681
+ }
43682
+ /** Snapshot inicial el modal mantiene su propia copia y la refresca tras revoke. */
43683
+ set sessions(value) {
43684
+ this.sessionsLocal.set([...value]);
43734
43685
  }
43735
- /** Respuesta parseada de un item — desde el cache. */
43736
- answerOf(item) {
43737
- return this.parsedAnswers().get(item.id) ?? { elements: [] };
43686
+ close() {
43687
+ this._modalRef?.dismiss(undefined, 'close');
43738
43688
  }
43739
- onSearch(term) {
43740
- this.query.set(term ?? '');
43689
+ async handleRevoke(sessionId) {
43690
+ if (!this.onRevoke)
43691
+ return;
43692
+ try {
43693
+ const updated = await this.onRevoke(sessionId);
43694
+ this.sessionsLocal.set([...updated]);
43695
+ }
43696
+ catch {
43697
+ // El parent muestra el toast de error; modal no necesita feedback extra.
43698
+ }
43699
+ }
43700
+ get revokeBtn() {
43701
+ return {
43702
+ text: this.revokeLabel,
43703
+ color: 'dark',
43704
+ fill: 'clear',
43705
+ size: 'small',
43706
+ type: 'button',
43707
+ state: ComponentStates.ENABLED,
43708
+ };
43709
+ }
43710
+ deviceIcon(s) {
43711
+ const ua = (s.deviceInfo || '').toLowerCase();
43712
+ const isMobile = /iphone|ipad|ipod|android|mobile/.test(ua);
43713
+ return isMobile ? 'phone-portrait-outline' : 'laptop-outline';
43714
+ }
43715
+ formatMeta(s) {
43716
+ const parts = [];
43717
+ if (s.location)
43718
+ parts.push(s.location);
43719
+ else if (s.ip)
43720
+ parts.push(s.ip);
43721
+ if (s.createdAt)
43722
+ parts.push(new Date(s.createdAt).toLocaleString());
43723
+ return parts.join(' · ');
43724
+ }
43725
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionListModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
43726
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SessionListModalComponent, isStandalone: true, selector: "val-session-list-modal", inputs: { sessions: "sessions", title: "title", closeLabel: "closeLabel", thisLabel: "thisLabel", revokeLabel: "revokeLabel", onRevoke: "onRevoke", _modalRef: "_modalRef" }, ngImport: i0, template: `
43727
+ <ion-header>
43728
+ <ion-toolbar>
43729
+ <ion-buttons slot="end">
43730
+ <ion-button fill="clear" color="dark" (click)="close()">
43731
+ <strong>{{ closeLabel }}</strong>
43732
+ </ion-button>
43733
+ </ion-buttons>
43734
+ </ion-toolbar>
43735
+ </ion-header>
43736
+ <ion-content class="ion-padding">
43737
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
43738
+ <ul class="sessions">
43739
+ @for (s of sessionsLocal(); track s.sessionId) {
43740
+ <li class="session" [class.session--current]="s.isCurrent">
43741
+ <ion-icon class="session__icon" [name]="deviceIcon(s)" aria-hidden="true" />
43742
+ <div class="session__body">
43743
+ <val-text
43744
+ [props]="{
43745
+ content: s.deviceInfo || s.deviceId || '—',
43746
+ size: 'medium',
43747
+ color: 'dark',
43748
+ bold: true,
43749
+ }"
43750
+ />
43751
+ <val-text
43752
+ [props]="{
43753
+ content: formatMeta(s),
43754
+ size: 'small',
43755
+ color: 'medium',
43756
+ bold: false,
43757
+ }"
43758
+ />
43759
+ </div>
43760
+ <div class="session__end">
43761
+ @if (s.isCurrent) {
43762
+ <span class="session__badge">{{ thisLabel }}</span>
43763
+ } @else {
43764
+ <val-button [props]="revokeBtn" (click)="handleRevoke(s.sessionId)" />
43765
+ }
43766
+ </div>
43767
+ </li>
43768
+ }
43769
+ </ul>
43770
+ </ion-content>
43771
+ `, isInline: true, styles: [":host{display:block;height:100%}.sessions{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px}.session{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:14px;background:var(--ion-color-light, rgba(0, 0, 0, .04));border:1px solid transparent}.session--current{background:#2dd36f1a;border-color:#2dd36f4d}.session__icon{font-size:26px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0}.session--current .session__icon{color:var(--ion-color-success-shade, #28ba62)}.session__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}.session__end{flex-shrink:0}.session__badge{display:inline-block;padding:4px 10px;font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--ion-color-success-shade, #28ba62);background:#2dd36f2e;border-radius:999px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: 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: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
43772
+ }
43773
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SessionListModalComponent, decorators: [{
43774
+ type: Component,
43775
+ args: [{ selector: 'val-session-list-modal', standalone: true, imports: [
43776
+ CommonModule,
43777
+ IonContent,
43778
+ IonHeader,
43779
+ IonToolbar,
43780
+ IonButtons,
43781
+ IonButton,
43782
+ IonIcon,
43783
+ DisplayComponent,
43784
+ TextComponent,
43785
+ ButtonComponent,
43786
+ ], template: `
43787
+ <ion-header>
43788
+ <ion-toolbar>
43789
+ <ion-buttons slot="end">
43790
+ <ion-button fill="clear" color="dark" (click)="close()">
43791
+ <strong>{{ closeLabel }}</strong>
43792
+ </ion-button>
43793
+ </ion-buttons>
43794
+ </ion-toolbar>
43795
+ </ion-header>
43796
+ <ion-content class="ion-padding">
43797
+ <val-display [props]="{ content: title, size: 'small', color: 'dark' }" />
43798
+ <ul class="sessions">
43799
+ @for (s of sessionsLocal(); track s.sessionId) {
43800
+ <li class="session" [class.session--current]="s.isCurrent">
43801
+ <ion-icon class="session__icon" [name]="deviceIcon(s)" aria-hidden="true" />
43802
+ <div class="session__body">
43803
+ <val-text
43804
+ [props]="{
43805
+ content: s.deviceInfo || s.deviceId || '—',
43806
+ size: 'medium',
43807
+ color: 'dark',
43808
+ bold: true,
43809
+ }"
43810
+ />
43811
+ <val-text
43812
+ [props]="{
43813
+ content: formatMeta(s),
43814
+ size: 'small',
43815
+ color: 'medium',
43816
+ bold: false,
43817
+ }"
43818
+ />
43819
+ </div>
43820
+ <div class="session__end">
43821
+ @if (s.isCurrent) {
43822
+ <span class="session__badge">{{ thisLabel }}</span>
43823
+ } @else {
43824
+ <val-button [props]="revokeBtn" (click)="handleRevoke(s.sessionId)" />
43825
+ }
43826
+ </div>
43827
+ </li>
43828
+ }
43829
+ </ul>
43830
+ </ion-content>
43831
+ `, styles: [":host{display:block;height:100%}.sessions{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px}.session{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:14px;background:var(--ion-color-light, rgba(0, 0, 0, .04));border:1px solid transparent}.session--current{background:#2dd36f1a;border-color:#2dd36f4d}.session__icon{font-size:26px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0}.session--current .session__icon{color:var(--ion-color-success-shade, #28ba62)}.session__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}.session__end{flex-shrink:0}.session__badge{display:inline-block;padding:4px 10px;font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--ion-color-success-shade, #28ba62);background:#2dd36f2e;border-radius:999px}\n"] }]
43832
+ }], propDecorators: { sessions: [{
43833
+ type: Input
43834
+ }], title: [{
43835
+ type: Input
43836
+ }], closeLabel: [{
43837
+ type: Input
43838
+ }], thisLabel: [{
43839
+ type: Input
43840
+ }], revokeLabel: [{
43841
+ type: Input
43842
+ }], onRevoke: [{
43843
+ type: Input
43844
+ }], _modalRef: [{
43845
+ type: Input
43846
+ }] } });
43847
+
43848
+ /**
43849
+ * Standard cross-app site links (env-aware)
43850
+ *
43851
+ * En el factory MyValtech cada producto satélite (showcase, sigify, …) enlaza a
43852
+ * `web` —la landing de marca— para soporte, contacto y contenido legal. `web`
43853
+ * es el dueño canónico de esas rutas. Este módulo centraliza:
43854
+ *
43855
+ * 1. El mapa ambiente → base URL de `web` (`VALTECH_WEB_BASE_URLS`).
43856
+ * 2. El catálogo de paths estándar que viven en `web` (`VALTECH_SITE_PATHS`).
43857
+ *
43858
+ * El ambiente se decide en BUILD TIME (campo `valtechEnv` en el `environment*.ts`
43859
+ * de cada app), igual que `apiUrl`. Es el mecanismo más confiable: detectar por
43860
+ * hostname en runtime se rompe con dominios custom / `web.app` / variantes de
43861
+ * localhost.
43862
+ *
43863
+ * La resolución real (join baseUrl + path, abrir en tab nueva, `?lang=`) la hace
43864
+ * `LegalLinkService`; este módulo solo le entrega el `baseUrl` correcto por env.
43865
+ *
43866
+ * @see provideValtechSite — wiring de una sola línea para apps satélite.
43867
+ */
43868
+ /**
43869
+ * Base URL de `web` (landing de marca) por ambiente.
43870
+ *
43871
+ * - `local` → `ng serve` de `web` (puerto 8100).
43872
+ * - `dev` → proyecto Firebase `myvaltech-dev`.
43873
+ * - `prod` → dominio custom de producción.
43874
+ *
43875
+ * Sin trailing slash (lo asume `LegalLinkService`).
43876
+ */
43877
+ const VALTECH_WEB_BASE_URLS = {
43878
+ local: 'http://localhost:8100',
43879
+ dev: 'https://myvaltech-dev.web.app',
43880
+ prod: 'https://myvaltech.com',
43881
+ };
43882
+ /**
43883
+ * Resuelve la base URL de `web` para un ambiente.
43884
+ *
43885
+ * Fallback a `prod` cuando el env es desconocido o falta: ante la duda, apuntar
43886
+ * a producción es lo seguro (nunca enlaza a un host local/dev inexistente).
43887
+ */
43888
+ function resolveWebBaseUrl(env) {
43889
+ if (env && env in VALTECH_WEB_BASE_URLS) {
43890
+ return VALTECH_WEB_BASE_URLS[env];
43891
+ }
43892
+ return VALTECH_WEB_BASE_URLS.prod;
43893
+ }
43894
+ /**
43895
+ * Catálogo de paths estándar que viven en `web`. Las apps resuelven estos
43896
+ * contra la base URL del ambiente vía `LegalLinkService.resolve(path)`.
43897
+ *
43898
+ * Mantener alineado con las rutas reales de `web/src/app/app.routes.ts` y con
43899
+ * `VALTECH_COMPANY_LINKS` (footer).
43900
+ */
43901
+ const VALTECH_SITE_PATHS = {
43902
+ home: '/',
43903
+ support: '/contact',
43904
+ contact: '/contact',
43905
+ faq: '/faq',
43906
+ about: '/about',
43907
+ terms: '/legal/terms',
43908
+ privacy: '/legal/privacy',
43909
+ cookies: '/legal/cookies',
43910
+ legalNotice: '/legal/legal-notice',
43911
+ };
43912
+
43913
+ const VALTECH_LEGAL_CONFIG = new InjectionToken('VALTECH_LEGAL_CONFIG');
43914
+ /**
43915
+ * Resolves legal/site paths against a configurable main-site base URL.
43916
+ *
43917
+ * - **Main site mode**: no `provideValtechLegal` called → `resolve('/legal/terms')` returns `/legal/terms` (relative).
43918
+ * - **Satellite mode**: `provideValtechLegal({ baseUrl: 'https://myvaltech.com' })` → `resolve('/legal/terms')` returns `'https://myvaltech.com/legal/terms'` and `isExternal('/legal/terms')` returns `true`.
43919
+ *
43920
+ * Absolute URLs passed in are returned unchanged (no double-prefix).
43921
+ */
43922
+ class LegalLinkService {
43923
+ constructor() {
43924
+ this.config = inject(VALTECH_LEGAL_CONFIG, { optional: true }) ?? {};
43925
+ }
43926
+ /** Effective base URL (null if running as main site). */
43927
+ get baseUrl() {
43928
+ return this.config.baseUrl?.replace(/\/$/, '') ?? null;
43929
+ }
43930
+ /** Whether resolved external links should open in a new tab. */
43931
+ get openInNewTab() {
43932
+ return this.config.openInNewTab ?? Boolean(this.config.baseUrl);
43933
+ }
43934
+ /**
43935
+ * Returns the URL to use for a given internal path. Absolute URLs pass through.
43936
+ *
43937
+ * When `locale` is provided AND the resolved URL would be cross-origin, appends
43938
+ * `?lang={locale}` so the main site can render the legal doc in the user's
43939
+ * active language (instead of its default). Locale is omitted for same-origin
43940
+ * resolves since the main site reads locale from its own `LocaleService`.
43941
+ *
43942
+ * @example
43943
+ * resolve('/legal/terms')
43944
+ * // main site: '/legal/terms'
43945
+ * // satellite: 'https://myvaltech.com/legal/terms'
43946
+ *
43947
+ * resolve('/legal/terms', { locale: 'pt' })
43948
+ * // main site: '/legal/terms' (locale stays implicit)
43949
+ * // satellite: 'https://myvaltech.com/legal/terms?lang=pt'
43950
+ *
43951
+ * resolve('https://x.com/y') // unchanged
43952
+ */
43953
+ resolve(path, options) {
43954
+ if (!path)
43955
+ return path;
43956
+ if (/^https?:\/\//i.test(path))
43957
+ return path;
43958
+ const base = this.baseUrl;
43959
+ if (!base)
43960
+ return path;
43961
+ const url = `${base}${path.startsWith('/') ? '' : '/'}${path}`;
43962
+ if (options?.locale) {
43963
+ const sep = url.includes('?') ? '&' : '?';
43964
+ return `${url}${sep}lang=${encodeURIComponent(options.locale)}`;
43965
+ }
43966
+ return url;
43967
+ }
43968
+ /** `true` if the path would be resolved to a cross-origin URL. */
43969
+ isExternal(path) {
43970
+ if (/^https?:\/\//i.test(path))
43971
+ return true;
43972
+ return this.baseUrl !== null;
43973
+ }
43974
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
43975
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, providedIn: 'root' }); }
43976
+ }
43977
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, decorators: [{
43978
+ type: Injectable,
43979
+ args: [{ providedIn: 'root' }]
43980
+ }] });
43981
+ /**
43982
+ * Wires `LegalLinkService` for satellite apps. Omit in the main site (the one
43983
+ * that hosts the canonical /legal/* routes) so links stay relative.
43984
+ *
43985
+ * @example
43986
+ * // main.ts of showcase / sigify / etc.
43987
+ * provideValtechLegal({
43988
+ * baseUrl: 'https://myvaltech.com',
43989
+ * openInNewTab: true,
43990
+ * }),
43991
+ */
43992
+ function provideValtechLegal(config) {
43993
+ return { provide: VALTECH_LEGAL_CONFIG, useValue: config };
43994
+ }
43995
+ /**
43996
+ * Wiring de una sola línea para que una app satélite apunte sus links de sitio
43997
+ * (legal, soporte, contacto, …) a `web` —la landing de marca— en el ambiente
43998
+ * correcto. Resuelve el `baseUrl` desde `VALTECH_WEB_BASE_URLS` según el `env`
43999
+ * baked en build-time (`environment.valtechEnv`) y delega en `provideValtechLegal`.
44000
+ *
44001
+ * Ambiente desconocido/ausente → fallback a `prod` (ver `resolveWebBaseUrl`).
44002
+ * El main site (`web`) NO llama esto — sus links quedan relativos.
44003
+ *
44004
+ * @example
44005
+ * // main.ts de showcase / sigify / …
44006
+ * import { environment } from './environments/environment';
44007
+ * provideValtechSite(environment.valtechEnv), // 'local' | 'dev' | 'prod'
44008
+ *
44009
+ * @see resolveWebBaseUrl
44010
+ * @see VALTECH_SITE_PATHS — catálogo de paths estándar que viven en `web`.
44011
+ */
44012
+ function provideValtechSite(env, options) {
44013
+ return provideValtechLegal({
44014
+ baseUrl: resolveWebBaseUrl(env),
44015
+ openInNewTab: options?.openInNewTab ?? true,
44016
+ });
44017
+ }
44018
+
44019
+ /**
44020
+ * Default modal sizes.
44021
+ */
44022
+ const MODAL_SIZES = {
44023
+ small: { width: '320px', height: '220px' },
44024
+ medium: { width: '500px', height: '400px' },
44025
+ large: { width: '800px', height: '80vh' },
44026
+ full: { width: '100%', height: '100%' },
44027
+ auto: { width: 'auto', height: '50vh' },
44028
+ };
44029
+ /**
44030
+ * Default modal buttons.
44031
+ */
44032
+ const DEFAULT_MODAL_CONFIRM_BUTTON = {
44033
+ text: 'Confirm',
44034
+ role: 'confirm',
44035
+ color: 'primary',
44036
+ fill: 'solid',
44037
+ };
44038
+ const DEFAULT_MODAL_CANCEL_BUTTON = {
44039
+ text: 'Cancel',
44040
+ role: 'cancel',
44041
+ color: 'medium',
44042
+ fill: 'clear',
44043
+ };
44044
+
44045
+ /**
44046
+ * Internal component for simple content modals.
44047
+ * Used by ModalService.openSimple()
44048
+ *
44049
+ * Header + título + close button delegados a `val-modal-shell` (estándar
44050
+ * canónico — ver Regla #5 en `frontend/CLAUDE.md`). El cuerpo (innerHTML) +
44051
+ * footer con botones quedan locales.
44052
+ */
44053
+ class SimpleModalContentComponent {
44054
+ constructor() {
44055
+ this.title = '';
44056
+ this.content = '';
44057
+ this.buttons = [];
44058
+ this.showCloseButton = true;
44059
+ }
44060
+ async onButtonClick(button) {
44061
+ if (button.handler) {
44062
+ const result = await button.handler();
44063
+ // If handler returns false, don't dismiss
44064
+ if (result === false) {
44065
+ return;
44066
+ }
44067
+ }
44068
+ this._modalRef?.dismiss(undefined, button.role || 'confirm');
44069
+ }
44070
+ onDismiss(role) {
44071
+ this._modalRef?.dismiss(undefined, role);
44072
+ }
44073
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleModalContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
44074
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SimpleModalContentComponent, isStandalone: true, selector: "val-simple-modal-content", inputs: { title: "title", content: "content", buttons: "buttons", showCloseButton: "showCloseButton", _modalRef: "_modalRef" }, ngImport: i0, template: `
44075
+ <val-modal-shell [title]="title" [showClose]="showCloseButton" (close)="onDismiss('close')">
44076
+ <div [innerHTML]="content"></div>
44077
+ </val-modal-shell>
44078
+
44079
+ @if (buttons && buttons.length > 0) {
44080
+ <ion-footer>
44081
+ <ion-toolbar>
44082
+ <div class="button-container">
44083
+ @for (button of buttons; track button.text) {
44084
+ <ion-button
44085
+ [color]="button.color || 'medium'"
44086
+ [fill]="button.fill || 'solid'"
44087
+ [size]="button.size || 'default'"
44088
+ [disabled]="button.disabled || button.loading"
44089
+ [class]="button.cssClass"
44090
+ (click)="onButtonClick(button)"
44091
+ >
44092
+ @if (button.loading) {
44093
+ <ion-spinner name="circular"></ion-spinner>
44094
+ } @else {
44095
+ {{ button.text }}
44096
+ }
44097
+ </ion-button>
44098
+ }
44099
+ </div>
44100
+ </ion-toolbar>
44101
+ </ion-footer>
44102
+ }
44103
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}ion-footer{flex-shrink:0}.button-container{display:flex;justify-content:flex-end;gap:8px;padding:8px 0}ion-footer ion-toolbar{--padding-start: 16px;--padding-end: 16px}:host ::ng-deep ul,:host ::ng-deep ol{padding-left:20px;margin:8px 0}:host ::ng-deep li{margin:4px 0}:host ::ng-deep p{margin:8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { 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: IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ModalShellComponent, selector: "val-modal-shell", inputs: ["title", "subtitle", "closeLabel", "showClose"], outputs: ["close"] }] }); }
44104
+ }
44105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleModalContentComponent, decorators: [{
44106
+ type: Component,
44107
+ args: [{ selector: 'val-simple-modal-content', standalone: true, imports: [CommonModule, IonToolbar, IonButton, IonFooter, IonSpinner, ModalShellComponent], template: `
44108
+ <val-modal-shell [title]="title" [showClose]="showCloseButton" (close)="onDismiss('close')">
44109
+ <div [innerHTML]="content"></div>
44110
+ </val-modal-shell>
44111
+
44112
+ @if (buttons && buttons.length > 0) {
44113
+ <ion-footer>
44114
+ <ion-toolbar>
44115
+ <div class="button-container">
44116
+ @for (button of buttons; track button.text) {
44117
+ <ion-button
44118
+ [color]="button.color || 'medium'"
44119
+ [fill]="button.fill || 'solid'"
44120
+ [size]="button.size || 'default'"
44121
+ [disabled]="button.disabled || button.loading"
44122
+ [class]="button.cssClass"
44123
+ (click)="onButtonClick(button)"
44124
+ >
44125
+ @if (button.loading) {
44126
+ <ion-spinner name="circular"></ion-spinner>
44127
+ } @else {
44128
+ {{ button.text }}
44129
+ }
44130
+ </ion-button>
44131
+ }
44132
+ </div>
44133
+ </ion-toolbar>
44134
+ </ion-footer>
44135
+ }
44136
+ `, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}ion-footer{flex-shrink:0}.button-container{display:flex;justify-content:flex-end;gap:8px;padding:8px 0}ion-footer ion-toolbar{--padding-start: 16px;--padding-end: 16px}:host ::ng-deep ul,:host ::ng-deep ol{padding-left:20px;margin:8px 0}:host ::ng-deep li{margin:4px 0}:host ::ng-deep p{margin:8px 0}\n"] }]
44137
+ }], propDecorators: { title: [{
44138
+ type: Input
44139
+ }], content: [{
44140
+ type: Input
44141
+ }], buttons: [{
44142
+ type: Input
44143
+ }], showCloseButton: [{
44144
+ type: Input
44145
+ }], _modalRef: [{
44146
+ type: Input
44147
+ }] } });
44148
+
44149
+ /**
44150
+ * ModalService
44151
+ *
44152
+ * A service for creating and managing modals with various configurations.
44153
+ *
44154
+ * @example Basic component modal
44155
+ * ```typescript
44156
+ * const result = await modalService.open({
44157
+ * component: MyFormComponent,
44158
+ * componentProps: { userId: 123 },
44159
+ * header: { title: 'Edit User', showCloseButton: true },
44160
+ * size: 'medium'
44161
+ * });
44162
+ *
44163
+ * if (result.role === 'confirm') {
44164
+ * console.log('Saved:', result.data);
44165
+ * }
44166
+ * ```
44167
+ *
44168
+ * @example Simple content modal
44169
+ * ```typescript
44170
+ * await modalService.openSimple({
44171
+ * title: 'Terms of Service',
44172
+ * content: '<p>Terms content here...</p>',
44173
+ * size: 'large',
44174
+ * showCloseButton: true
44175
+ * });
44176
+ * ```
44177
+ *
44178
+ * @example Sheet modal (mobile)
44179
+ * ```typescript
44180
+ * const result = await modalService.openSheet({
44181
+ * component: FilterComponent,
44182
+ * breakpoints: {
44183
+ * initialBreakpoint: 0.5,
44184
+ * breakpoints: [0, 0.5, 1],
44185
+ * showHandle: true
44186
+ * }
44187
+ * });
44188
+ * ```
44189
+ */
44190
+ class ModalService {
44191
+ constructor(modalController) {
44192
+ this.modalController = modalController;
44193
+ }
44194
+ /**
44195
+ * Opens a modal with a component.
44196
+ * @param options - Modal configuration
44197
+ * @returns Promise resolving to the modal result
44198
+ */
44199
+ async open(options) {
44200
+ // A sheet modal (breakpoints present) gets its height from the breakpoints
44201
+ // and must span the full viewport width. Applying a size preset's
44202
+ // `--width`/`--height` here collapses the sheet (e.g. `auto` → shrink to
44203
+ // content) or fights the breakpoint height. So when it's a sheet we skip
44204
+ // the size preset entirely.
44205
+ const isSheet = !!options.breakpoints?.breakpoints?.length;
44206
+ const size = isSheet ? undefined : MODAL_SIZES[options.size || 'medium'];
44207
+ const cssClasses = this.buildCssClasses(options);
44208
+ const modal = await this.modalController.create({
44209
+ component: options.component,
44210
+ componentProps: {
44211
+ ...options.componentProps,
44212
+ // Pass modal reference for dismiss from within component
44213
+ _modalRef: {
44214
+ dismiss: (data, role) => modal.dismiss(data, role),
44215
+ },
44216
+ },
44217
+ id: options.id,
44218
+ showBackdrop: options.showBackdrop ?? true,
44219
+ backdropDismiss: options.backdropDismiss ?? true,
44220
+ keyboardClose: options.keyboardClose ?? true,
44221
+ cssClass: cssClasses,
44222
+ mode: options.mode,
44223
+ animated: options.animated ?? true,
44224
+ canDismiss: this.normalizeCanDismiss(options.canDismiss ?? true),
44225
+ presentingElement: options.presentingElement,
44226
+ // Sheet modal options
44227
+ initialBreakpoint: options.breakpoints?.initialBreakpoint,
44228
+ breakpoints: options.breakpoints?.breakpoints,
44229
+ handle: options.breakpoints?.showHandle,
44230
+ handleBehavior: options.breakpoints?.handleBehavior,
44231
+ });
44232
+ // Apply custom size styles. Skipped for sheet modals — their dimensions
44233
+ // come from the breakpoints (Ionic spans the sheet full-width and uses the
44234
+ // breakpoint values for height).
44235
+ if (!isSheet && (options.width || options.height || size)) {
44236
+ const modalEl = modal;
44237
+ modalEl.style.setProperty('--width', options.width || size?.width || 'auto');
44238
+ modalEl.style.setProperty('--height', options.height || size?.height || '50vh');
44239
+ }
44240
+ await modal.present();
44241
+ const { data, role } = await modal.onDidDismiss();
44242
+ return { data, role };
44243
+ }
44244
+ /**
44245
+ * Opens a simple content modal without a custom component.
44246
+ * Uses Ionic's alert-like styling but with modal capabilities.
44247
+ * @param options - Simple modal configuration
44248
+ * @returns Promise resolving to the modal result
44249
+ */
44250
+ async openSimple(options) {
44251
+ return this.open({
44252
+ component: SimpleModalContentComponent,
44253
+ componentProps: {
44254
+ title: options.title,
44255
+ content: options.content,
44256
+ buttons: options.buttons || [DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON],
44257
+ showCloseButton: options.showCloseButton ?? true,
44258
+ },
44259
+ size: options.size || 'medium',
44260
+ backdropDismiss: options.backdropDismiss ?? true,
44261
+ cssClass: ['simple-modal', options.cssClass].filter(Boolean).join(' '),
44262
+ });
44263
+ }
44264
+ /**
44265
+ * Opens a confirmation modal.
44266
+ * @param title - Modal title
44267
+ * @param message - Modal message
44268
+ * @param confirmButton - Confirm button config (optional)
44269
+ * @param cancelButton - Cancel button config (optional)
44270
+ * @returns Promise resolving to true if confirmed
44271
+ */
44272
+ async confirm(title, message, confirmButton, cancelButton) {
44273
+ const result = await this.openSimple({
44274
+ title,
44275
+ content: message,
44276
+ size: 'small',
44277
+ showCloseButton: false,
44278
+ backdropDismiss: false,
44279
+ buttons: [
44280
+ { ...DEFAULT_MODAL_CANCEL_BUTTON, ...cancelButton },
44281
+ { ...DEFAULT_MODAL_CONFIRM_BUTTON, ...confirmButton },
44282
+ ],
44283
+ });
44284
+ return result.role === 'confirm';
44285
+ }
44286
+ /**
44287
+ * Opens a sheet modal (iOS-style bottom sheet).
44288
+ * @param options - Modal configuration with sheet-specific options
44289
+ * @returns Promise resolving to the modal result
44290
+ */
44291
+ async openSheet(options) {
44292
+ return this.open({
44293
+ ...options,
44294
+ breakpoints: {
44295
+ initialBreakpoint: options.initialBreakpoint ?? 0.5,
44296
+ breakpoints: options.breakpoints ?? [0, 0.5, 1],
44297
+ showHandle: options.showHandle ?? true,
44298
+ },
44299
+ cssClass: ['sheet-modal', options.cssClass].filter(Boolean).join(' '),
44300
+ });
44301
+ }
44302
+ /**
44303
+ * Opens a fullscreen modal.
44304
+ * @param options - Modal configuration
44305
+ * @returns Promise resolving to the modal result
44306
+ */
44307
+ async openFullscreen(options) {
44308
+ return this.open({
44309
+ ...options,
44310
+ size: 'full',
44311
+ cssClass: ['fullscreen-modal', options.cssClass].filter(Boolean).join(' '),
44312
+ });
44313
+ }
44314
+ /**
44315
+ * Dismisses the top-most modal.
44316
+ * @param data - Optional data to return
44317
+ * @param role - Optional role
44318
+ */
44319
+ async dismiss(data, role) {
44320
+ return this.modalController.dismiss(data, role);
44321
+ }
44322
+ /**
44323
+ * Gets the top-most modal.
44324
+ */
44325
+ async getTop() {
44326
+ return this.modalController.getTop();
44327
+ }
44328
+ normalizeCanDismiss(canDismiss) {
44329
+ if (typeof canDismiss === 'boolean') {
44330
+ return canDismiss;
44331
+ }
44332
+ // Wrap the function to ensure it returns a Promise
44333
+ return async (data, role) => {
44334
+ const result = canDismiss();
44335
+ return Promise.resolve(result);
44336
+ };
44337
+ }
44338
+ buildCssClasses(options) {
44339
+ const classes = ['val-modal'];
44340
+ if (options.size) {
44341
+ classes.push(`modal-${options.size}`);
44342
+ }
44343
+ if (options.header) {
44344
+ classes.push('modal-with-header');
44345
+ }
44346
+ if (options.footer) {
44347
+ classes.push('modal-with-footer');
44348
+ }
44349
+ if (options.cssClass) {
44350
+ classes.push(options.cssClass);
44351
+ }
44352
+ return classes.join(' ');
44353
+ }
44354
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, deps: [{ token: i2$1.ModalController }], target: i0.ɵɵFactoryTarget.Injectable }); }
44355
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, providedIn: 'root' }); }
44356
+ }
44357
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, decorators: [{
44358
+ type: Injectable,
44359
+ args: [{
44360
+ providedIn: 'root',
44361
+ }]
44362
+ }], ctorParameters: () => [{ type: i2$1.ModalController }] });
44363
+
44364
+ /**
44365
+ * Defaults i18n (es/en) embebidos en `val-security-view`. Auto-registrados en el
44366
+ * constructor del componente si el consumer no proveyó el namespace
44367
+ * (`Settings.Security` por default). Garantiza que nunca haya una key faltante
44368
+ * evaluada por cada change-detection (ver nota en valtech-components/CLAUDE.md
44369
+ * sobre el storm de CD con `val-debug-console`). El consumer puede override
44370
+ * registrando el mismo namespace antes de que el componente monte.
44371
+ *
44372
+ * SOLO incluye las keys que la vista realmente usa.
44373
+ */
44374
+ const SECURITY_VIEW_I18N = {
44375
+ es: {
44376
+ pageTitle: 'Seguridad',
44377
+ pageDescription: 'Contraseñas, MFA y sesiones activas',
44378
+ blocked: 'Dispositivo bloqueado',
44379
+ tokenActionFailed: 'El enlace del email no es válido o expiró.',
44380
+ loginAttemptTitle: 'Confirmar inicio de sesión',
44381
+ loginAttemptBody: 'Detectamos un inicio de sesión en este dispositivo. Si fuiste tú, no tienes que hacer nada. Si no lo reconoces, bloquéalo.',
44382
+ loginAttemptMine: 'Sí, fui yo',
44383
+ loginAttemptBlock: 'No fui yo — bloquear',
44384
+ loginAttemptClose: 'Cerrar',
44385
+ loginAttemptDone: 'Dispositivo bloqueado. Restablece tu contraseña para reforzar tu cuenta.',
44386
+ loginAttemptUnknownDevice: 'Dispositivo desconocido',
44387
+ sessionsTitle: 'Sesiones activas',
44388
+ sessionsTitleCount: 'Sesiones activas ({n})',
44389
+ sessionsLoading: 'Cargando sesiones…',
44390
+ sessionsEmpty: 'No hay sesiones activas.',
44391
+ sessionsThis: 'Esta sesión',
44392
+ sessionsRevoke: 'Cerrar',
44393
+ sessionsRevokeAll: 'Cerrar todas las demás',
44394
+ sessionsViewAll: 'Ver todas',
44395
+ sessionsModalTitle: 'Todas las sesiones activas',
44396
+ sessionsRevoked: 'Sesión cerrada.',
44397
+ sessionsAllRevoked: 'Todas las demás sesiones fueron cerradas.',
44398
+ sessionsError: 'No se pudo completar la operación.',
44399
+ revokeAllConfirmTitle: 'Cerrar todas las otras sesiones',
44400
+ revokeAllConfirmMessage: 'Vas a cerrar todas tus sesiones excepto la actual. Tendrás que volver a iniciar sesión en los otros dispositivos.',
44401
+ revokeAllConfirm: 'Cerrar todas',
44402
+ cancel: 'Cancelar',
44403
+ quickActionsTitle: 'Otras opciones',
44404
+ changePassword: 'Cambiar contraseña',
44405
+ changePasswordDesc: 'Actualiza tu contraseña de acceso',
44406
+ changeEmail: 'Cambiar email',
44407
+ changeEmailDesc: 'Actualiza el correo electrónico de tu cuenta',
44408
+ mfa: 'Autenticación de dos pasos (MFA)',
44409
+ mfaDesc: 'Agrega un segundo factor a tu inicio de sesión',
44410
+ sessions: 'Sesiones activas',
44411
+ sessionsDesc: 'Revisa y cierra sesiones en otros dispositivos',
44412
+ policiesTitle: 'Políticas relacionadas',
44413
+ privacyLink: 'Política de Privacidad',
44414
+ cookiesLink: 'Política de Cookies',
44415
+ dataUsageLink: 'Uso de Datos',
44416
+ termsLink: 'Términos y Condiciones',
44417
+ thirdPartyLink: 'Software y Servicios de Terceros',
44418
+ legalNoticeLink: 'Aviso Legal',
44419
+ soonHint: 'Próximamente',
44420
+ },
44421
+ en: {
44422
+ pageTitle: 'Security',
44423
+ pageDescription: 'Passwords, MFA, and active sessions',
44424
+ blocked: 'Device blocked',
44425
+ tokenActionFailed: 'The email link is invalid or has expired.',
44426
+ loginAttemptTitle: 'Confirm sign-in',
44427
+ loginAttemptBody: "We detected a sign-in on this device. If it was you, there's nothing to do. If you don't recognize it, block it.",
44428
+ loginAttemptMine: 'Yes, it was me',
44429
+ loginAttemptBlock: 'Not me — block it',
44430
+ loginAttemptClose: 'Close',
44431
+ loginAttemptDone: 'Device blocked. Reset your password to secure your account.',
44432
+ loginAttemptUnknownDevice: 'Unknown device',
44433
+ sessionsTitle: 'Active sessions',
44434
+ sessionsTitleCount: 'Active sessions ({n})',
44435
+ sessionsLoading: 'Loading sessions…',
44436
+ sessionsEmpty: 'No active sessions.',
44437
+ sessionsThis: 'This session',
44438
+ sessionsRevoke: 'End',
44439
+ sessionsRevokeAll: 'End all others',
44440
+ sessionsViewAll: 'View all',
44441
+ sessionsModalTitle: 'All active sessions',
44442
+ sessionsRevoked: 'Session ended.',
44443
+ sessionsAllRevoked: 'All other sessions were ended.',
44444
+ sessionsError: 'Could not complete the operation.',
44445
+ revokeAllConfirmTitle: 'End all other sessions',
44446
+ revokeAllConfirmMessage: "You're about to end every session except this one. You'll need to sign in again on the other devices.",
44447
+ revokeAllConfirm: 'End all',
44448
+ cancel: 'Cancel',
44449
+ quickActionsTitle: 'Other options',
44450
+ changePassword: 'Change password',
44451
+ changePasswordDesc: 'Update your access password',
44452
+ changeEmail: 'Change email',
44453
+ changeEmailDesc: 'Update the email address on your account',
44454
+ mfa: 'Two-step verification (MFA)',
44455
+ mfaDesc: 'Add a second factor to your sign-in',
44456
+ sessions: 'Active sessions',
44457
+ sessionsDesc: 'Review and sign out from other devices',
44458
+ policiesTitle: 'Related policies',
44459
+ privacyLink: 'Privacy Policy',
44460
+ cookiesLink: 'Cookie Policy',
44461
+ dataUsageLink: 'Data Usage',
44462
+ termsLink: 'Terms and Conditions',
44463
+ thirdPartyLink: 'Third-Party Software and Services',
44464
+ legalNoticeLink: 'Legal Notice',
44465
+ soonHint: 'Coming soon',
44466
+ },
44467
+ };
44468
+
44469
+ addIcons({ laptopOutline, phonePortraitOutline });
44470
+ const DEFAULT_NAMESPACE = 'Settings.Security';
44471
+ /**
44472
+ * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44473
+ *
44474
+ * Promovida desde `showcase` bajo el proceso de ADR-021. Reúne en una sola vista:
44475
+ * - **Sesiones activas** — única vista user-facing de "dónde tengo sesión".
44476
+ * Lista inline (limit 3) + modal "Ver todas" (`val-session-list-modal`) +
44477
+ * revoke individual / revoke-all (con confirmación).
44478
+ * - **Action-cards** — cambiar contraseña, cambiar email y MFA (cada una abre
44479
+ * el modal correspondiente de la lib).
44480
+ * - **Políticas legales** — links resueltos vía `LegalLinkService` (apps
44481
+ * satélite apuntan al main site).
44482
+ *
44483
+ * Deep-links del email (gateados por `config.handleDeepLinks`, default `true`):
44484
+ * - `?token=...&action=...` → modal `val-login-attempt-modal` (autorizar/bloquear
44485
+ * el nuevo acceso).
44486
+ * - `?mfaCode=...` → abre el modal MFA con el código pre-cargado.
44487
+ *
44488
+ * NO renderiza ion-content — vive dentro de val-page-wrapper.
44489
+ *
44490
+ * Auto-registra sus defaults i18n (es/en) en el constructor si el consumer no
44491
+ * proveyó el namespace configurado (default `Settings.Security`).
44492
+ */
44493
+ class SecurityViewComponent {
44494
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
44495
+ get ns() {
44496
+ return this.resolvedConfig().i18nNamespace;
44497
+ }
44498
+ constructor() {
44499
+ this.nav = inject(NavigationService);
44500
+ this.i18n = inject(I18nService);
44501
+ this.deviceService = inject(DeviceService);
44502
+ this.legalLink = inject(LegalLinkService);
44503
+ this.toast = inject(ToastService);
44504
+ this.errors = inject(ValtechErrorService);
44505
+ this.router = inject(Router, { optional: true });
44506
+ this.route = inject(ActivatedRoute, { optional: true });
44507
+ this.sessionService = inject(SessionService);
44508
+ this.modalService = inject(ModalService);
44509
+ this.confirmDialog = inject(ConfirmationDialogService);
44510
+ this.resolvedConfig = computed(() => {
44511
+ const fromRoute = (this.route?.snapshot.data['securityConfig'] ?? {});
44512
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
44513
+ return {
44514
+ showSessions: merged.showSessions ?? true,
44515
+ showChangePassword: merged.showChangePassword ?? true,
44516
+ showChangeEmail: merged.showChangeEmail ?? true,
44517
+ showMfa: merged.showMfa ?? true,
44518
+ showPolicies: merged.showPolicies ?? true,
44519
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
44520
+ handleDeepLinks: merged.handleDeepLinks ?? true,
44521
+ homeRoute: merged.homeRoute ?? '/app/home',
44522
+ onMfaCompleted: merged.onMfaCompleted,
44523
+ };
44524
+ });
44525
+ /**
44526
+ * Lista de dispositivos del user. No se renderiza — solo alimenta el modal
44527
+ * de confirmación de inicio de sesión (`handleActionTokenFromUrl`) para
44528
+ * resolver un label legible desde el `targetId` del token.
44529
+ */
44530
+ this.devices = signal([]);
44531
+ this.sessions = signal([]);
44532
+ this.sessionsLoading = signal(true);
44533
+ this.isChangeEmailModalOpen = signal(false);
44534
+ this.isChangePasswordModalOpen = signal(false);
44535
+ this.isMfaModalOpen = signal(false);
44536
+ /** Código MFA del deep-link `/verify` — pre-carga el modal en confirmación. */
44537
+ this.mfaPrefillCode = signal(undefined);
44538
+ this.pageTitle = computed(() => this.tt('pageTitle'));
44539
+ this.pageDescription = computed(() => this.tt('pageDescription'));
44540
+ this.quickActionsTitle = computed(() => this.tt('quickActionsTitle'));
44541
+ this.policiesTitle = computed(() => this.tt('policiesTitle'));
44542
+ this.hasActionCards = computed(() => this.actionCards().length > 0);
44543
+ this.actionCards = computed(() => {
44544
+ this.i18n.lang();
44545
+ const cfg = this.resolvedConfig();
44546
+ const cards = [];
44547
+ if (cfg.showChangePassword) {
44548
+ cards.push({
44549
+ token: 'action-change-password',
44550
+ title: this.tt('changePassword'),
44551
+ description: this.tt('changePasswordDesc'),
44552
+ icon: { ionicon: 'key-outline', color: 'dark', backgroundColor: 'light' },
44553
+ showChevron: true,
44554
+ shadowed: true,
44555
+ });
44556
+ }
44557
+ if (cfg.showChangeEmail) {
44558
+ cards.push({
44559
+ token: 'action-change-email',
44560
+ title: this.tt('changeEmail'),
44561
+ description: this.tt('changeEmailDesc'),
44562
+ icon: { ionicon: 'mail-outline', color: 'dark', backgroundColor: 'light' },
44563
+ showChevron: true,
44564
+ shadowed: true,
44565
+ });
44566
+ }
44567
+ if (cfg.showMfa) {
44568
+ cards.push({
44569
+ token: 'action-mfa',
44570
+ title: this.tt('mfa'),
44571
+ description: this.tt('mfaDesc'),
44572
+ icon: { ionicon: 'shield-checkmark-outline', color: 'dark', backgroundColor: 'light' },
44573
+ showChevron: true,
44574
+ shadowed: true,
44575
+ });
44576
+ }
44577
+ return cards;
44578
+ });
44579
+ /**
44580
+ * Fuente única de links legales del producto. Se resuelven vía
44581
+ * `LegalLinkService` — apps satélite apuntan al main site, el main site los
44582
+ * deja relativos.
44583
+ */
44584
+ this.policyLinksProps = computed(() => {
44585
+ const locale = this.i18n.lang();
44586
+ const link = (path, labelKey) => ({
44587
+ label: this.tt(labelKey),
44588
+ href: this.legalLink.resolve(path, { locale }),
44589
+ external: this.legalLink.isExternal(path),
44590
+ });
44591
+ return {
44592
+ links: [
44593
+ link('/legal/terms', 'termsLink'),
44594
+ link('/legal/privacy', 'privacyLink'),
44595
+ link('/legal/cookies', 'cookiesLink'),
44596
+ link('/legal/data-usage', 'dataUsageLink'),
44597
+ link('/legal/third-party', 'thirdPartyLink'),
44598
+ link('/legal/legal-notice', 'legalNoticeLink'),
44599
+ ],
44600
+ showDividers: true,
44601
+ };
44602
+ });
44603
+ // ===========================================================================
44604
+ // Sessions
44605
+ // ===========================================================================
44606
+ this.sessionsTitle = computed(() => this.tt('sessionsTitle'));
44607
+ this.sessionsEmptyLabel = computed(() => this.tt('sessionsEmpty'));
44608
+ this.sessionsThis = computed(() => this.tt('sessionsThis'));
44609
+ this.hasOtherSessions = computed(() => this.sessions().some(s => !s.isCurrent));
44610
+ /** Sesiones ordenadas: current primero, resto por createdAt desc. */
44611
+ this.sortedSessions = computed(() => {
44612
+ const all = [...this.sessions()];
44613
+ return all.sort((a, b) => {
44614
+ if (a.isCurrent && !b.isCurrent)
44615
+ return -1;
44616
+ if (!a.isCurrent && b.isCurrent)
44617
+ return 1;
44618
+ return (b.createdAt || '').localeCompare(a.createdAt || '');
44619
+ });
44620
+ });
44621
+ /** Limit de sesiones mostradas inline. Si total > limit, "Ver todas" abre modal. */
44622
+ this.VISIBLE_LIMIT = 3;
44623
+ this.visibleSessions = computed(() => this.sortedSessions().slice(0, this.VISIBLE_LIMIT));
44624
+ this.hasMoreSessions = computed(() => this.sortedSessions().length > this.VISIBLE_LIMIT);
44625
+ /** Titulo con count: "Sesiones activas (4)". Sin count si N=0. */
44626
+ this.sessionsTitleWithCount = computed(() => {
44627
+ const n = this.sortedSessions().length;
44628
+ if (n === 0)
44629
+ return this.sessionsTitle();
44630
+ return this.tt('sessionsTitleCount').replace('{n}', String(n));
44631
+ });
44632
+ this.revokeOneButtonProps = computed(() => ({
44633
+ text: this.tt('sessionsRevoke'),
44634
+ color: 'dark',
44635
+ fill: 'clear',
44636
+ size: 'small',
44637
+ type: 'button',
44638
+ token: 'session-revoke',
44639
+ }));
44640
+ this.viewAllButtonProps = computed(() => ({
44641
+ text: this.tt('sessionsViewAll'),
44642
+ color: 'dark',
44643
+ fill: 'clear',
44644
+ size: 'small',
44645
+ type: 'button',
44646
+ state: ComponentStates.ENABLED,
44647
+ }));
44648
+ /** Action-header de Sesiones: title + count + boton "Cerrar todas las demas". */
44649
+ this.sessionsHeader = computed(() => ({
44650
+ headingKind: 'title',
44651
+ title: {
44652
+ size: 'medium',
44653
+ color: 'dark',
44654
+ bold: true,
44655
+ content: this.sessionsTitleWithCount(),
44656
+ },
44657
+ action: {
44658
+ text: this.tt('sessionsRevokeAll'),
44659
+ color: 'dark',
44660
+ fill: 'outline',
44661
+ size: 'small',
44662
+ type: 'button',
44663
+ state: ComponentStates.ENABLED,
44664
+ token: 'session-revoke-all',
44665
+ handler: () => this.onRevokeAll(),
44666
+ },
44667
+ }));
44668
+ const ns = this.ns;
44669
+ if (!this.i18n.hasNamespace(ns)) {
44670
+ this.i18n.registerContent(ns, SECURITY_VIEW_I18N);
44671
+ }
44672
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
44673
+ // Carga inicial de sesiones - recarga reactiva a cambio de locale para
44674
+ // que los textos y metadatos se mantengan frescos. Los devices solo se
44675
+ // cargan on-demand desde el flujo del modal (handleActionTokenFromUrl).
44676
+ effect(() => {
44677
+ this.i18n.lang();
44678
+ const showSessions = this.resolvedConfig().showSessions;
44679
+ untracked(() => {
44680
+ if (showSessions)
44681
+ void this.loadSessions();
44682
+ });
44683
+ }, { allowSignalWrites: true });
44684
+ // Pull-to-refresh estandar (Regla #3) - re-fetchea las sesiones activas.
44685
+ connectPageRefresh(() => this.loadSessions());
44686
+ // Deep-links del email - solo si estan habilitados Y hay ruta (montada como
44687
+ // route). En modo embebido sin ruta es no-op.
44688
+ if (this.resolvedConfig().handleDeepLinks && this.route) {
44689
+ // Si el user llego via link del email (?token=...&action=...), abre el
44690
+ // modal de confirmacion para autorizar/rechazar el intento.
44691
+ void this.handleActionTokenFromUrl();
44692
+ // Deep-link del email de setup MFA-email (?mfaCode=...).
44693
+ this.handleMfaCodeFromUrl();
44694
+ }
44695
+ }
44696
+ /**
44697
+ * Llega vía link del email de "nuevo acceso" (`?token=...`). Valida el token
44698
+ * (sin ejecutar) y levanta el modal de confirmación. El bloqueo solo se
44699
+ * ejecuta si el user confirma "no fui yo" dentro del modal.
44700
+ */
44701
+ async handleActionTokenFromUrl() {
44702
+ const token = this.route?.snapshot.queryParamMap.get('token');
44703
+ if (!token)
44704
+ return;
44705
+ // Limpia ?token/?action de la URL ya - el modal es la fuente de verdad;
44706
+ // un refresh no debe re-abrirlo ni re-validar.
44707
+ void this.router?.navigate([], {
44708
+ relativeTo: this.route ?? undefined,
44709
+ queryParams: { token: null, action: null },
44710
+ queryParamsHandling: 'merge',
44711
+ replaceUrl: true,
44712
+ });
44713
+ let validation;
44714
+ try {
44715
+ validation = await firstValueFrom(this.deviceService.validateAction(token));
44716
+ }
44717
+ catch (err) {
44718
+ this.errors.handle(err, {
44719
+ context: 'security.validateActionToken',
44720
+ fallbackKey: 'tokenActionFailed',
44721
+ i18nNamespace: this.ns,
44722
+ });
44723
+ return;
44724
+ }
44725
+ if (!validation.valid) {
44726
+ this.toastTokenInvalid();
44727
+ return;
44728
+ }
44729
+ // Resuelve el dispositivo objetivo desde la lista cargada para mostrarlo.
44730
+ await this.loadDevices();
44731
+ const target = this.devices().find(d => d.deviceId === validation.targetId);
44732
+ const isMobile = target?.platform === 'ios' || target?.platform === 'android';
44733
+ const onBlock = async () => {
44734
+ await firstValueFrom(this.deviceService.executeAction(token));
44735
+ await this.loadDevices();
44736
+ };
44737
+ const result = await this.modalService.open({
44738
+ component: LoginAttemptModalComponent,
44739
+ componentProps: {
44740
+ title: this.tt('loginAttemptTitle'),
44741
+ body: this.tt('loginAttemptBody'),
44742
+ deviceLabel: target ? this.describeDevice(target) : this.tt('loginAttemptUnknownDevice'),
44743
+ metaLabel: target ? this.formatDate(target.lastActive) : '',
44744
+ mineLabel: this.tt('loginAttemptMine'),
44745
+ blockLabel: this.tt('loginAttemptBlock'),
44746
+ closeLabel: this.tt('loginAttemptClose'),
44747
+ doneLabel: this.tt('loginAttemptDone'),
44748
+ isMobile,
44749
+ onBlock,
44750
+ },
44751
+ });
44752
+ if (result.role === 'blocked') {
44753
+ this.toastOk(this.tt('blocked'));
44754
+ }
44755
+ else if (result.role === 'error') {
44756
+ this.toastTokenInvalid();
44757
+ }
44758
+ }
44759
+ toastTokenInvalid() {
44760
+ this.toast.show({
44761
+ message: this.tt('tokenActionFailed'),
44762
+ duration: 3500,
44763
+ color: 'dark',
44764
+ position: 'top',
44765
+ });
44766
+ }
44767
+ async loadDevices() {
44768
+ try {
44769
+ const list = await firstValueFrom(this.deviceService.listDevices());
44770
+ this.devices.set(list || []);
44771
+ }
44772
+ catch {
44773
+ this.devices.set([]);
44774
+ }
44775
+ }
44776
+ onActionClick(action) {
44777
+ if (action.token === 'action-change-password') {
44778
+ this.isChangePasswordModalOpen.set(true);
44779
+ return;
44780
+ }
44781
+ if (action.token === 'action-change-email') {
44782
+ this.isChangeEmailModalOpen.set(true);
44783
+ return;
44784
+ }
44785
+ if (action.token === 'action-mfa') {
44786
+ this.mfaPrefillCode.set(undefined);
44787
+ this.isMfaModalOpen.set(true);
44788
+ }
44789
+ }
44790
+ onMfaModalDismissed() {
44791
+ this.isMfaModalOpen.set(false);
44792
+ this.mfaPrefillCode.set(undefined);
44793
+ }
44794
+ /**
44795
+ * El user llegó vía link del email, confirmó MFA en el modal. Cerramos el
44796
+ * modal y, si hay hook `onMfaCompleted`, lo invocamos; si no, navegamos al
44797
+ * `homeRoute` configurado — quedarse en Settings → Security daría la
44798
+ * sensación de "tarea inconclusa" cuando el flow ya terminó.
44799
+ */
44800
+ onMfaEnabledViaDeeplink() {
44801
+ this.isMfaModalOpen.set(false);
44802
+ this.mfaPrefillCode.set(undefined);
44803
+ const cfg = this.resolvedConfig();
44804
+ if (cfg.onMfaCompleted) {
44805
+ cfg.onMfaCompleted();
44806
+ }
44807
+ else {
44808
+ void this.router?.navigateByUrl(cfg.homeRoute);
44809
+ }
44810
+ }
44811
+ /**
44812
+ * Deep-link `/verify?...&verify-code=` → redirige a esta vista con `?mfaCode=`.
44813
+ * Abre el modal MFA con el código pre-cargado en el paso de confirmación.
44814
+ */
44815
+ handleMfaCodeFromUrl() {
44816
+ const code = this.route?.snapshot.queryParamMap.get('mfaCode');
44817
+ if (!code)
44818
+ return;
44819
+ // Limpia el query param - un refresh no debe re-abrir el modal.
44820
+ void this.router?.navigate([], {
44821
+ relativeTo: this.route ?? undefined,
44822
+ queryParams: { mfaCode: null },
44823
+ queryParamsHandling: 'merge',
44824
+ replaceUrl: true,
44825
+ });
44826
+ this.mfaPrefillCode.set(code);
44827
+ this.isMfaModalOpen.set(true);
44828
+ }
44829
+ describeDevice(d) {
44830
+ const parts = [d.platform, d.browser, d.location].filter(Boolean);
44831
+ return parts.join(' · ') || d.deviceId;
44832
+ }
44833
+ formatDate(iso) {
44834
+ if (!iso)
44835
+ return '';
44836
+ try {
44837
+ return new Date(iso).toLocaleString(this.i18n.lang());
44838
+ }
44839
+ catch {
44840
+ return iso;
44841
+ }
44842
+ }
44843
+ toastOk(msg) {
44844
+ this.toast.show({
44845
+ message: msg,
44846
+ duration: 2200,
44847
+ color: 'dark',
44848
+ position: 'top',
44849
+ });
44850
+ }
44851
+ // ===========================================================================
44852
+ // Sessions - métodos
44853
+ // ===========================================================================
44854
+ /** Icono según device: phone si UA mobile, sino laptop. */
44855
+ deviceIcon(s) {
44856
+ const ua = (s.deviceInfo || '').toLowerCase();
44857
+ const isMobile = /iphone|ipad|ipod|android|mobile/.test(ua);
44858
+ return isMobile ? 'phone-portrait-outline' : 'laptop-outline';
44859
+ }
44860
+ formatSessionMeta(s) {
44861
+ const parts = [];
44862
+ if (s.location)
44863
+ parts.push(s.location);
44864
+ else if (s.ip)
44865
+ parts.push(s.ip);
44866
+ if (s.createdAt)
44867
+ parts.push(new Date(s.createdAt).toLocaleString());
44868
+ return parts.join(' · ');
44869
+ }
44870
+ async onRevokeOne(sessionId) {
44871
+ try {
44872
+ await firstValueFrom(this.sessionService.revokeSession(sessionId));
44873
+ this.toast.show({
44874
+ message: this.tt('sessionsRevoked'),
44875
+ duration: 2500,
44876
+ color: 'dark',
44877
+ position: 'top',
44878
+ });
44879
+ await this.loadSessions();
44880
+ }
44881
+ catch (err) {
44882
+ this.errors.handle(err, {
44883
+ context: 'security.revokeSession',
44884
+ fallbackKey: 'sessionsError',
44885
+ i18nNamespace: this.ns,
44886
+ });
44887
+ }
44888
+ }
44889
+ async onRevokeAll() {
44890
+ const result = await this.confirmDialog.confirmDestructive({
44891
+ title: this.tt('revokeAllConfirmTitle'),
44892
+ message: this.tt('revokeAllConfirmMessage'),
44893
+ confirmButton: { text: this.tt('revokeAllConfirm') },
44894
+ cancelButton: { text: this.tt('cancel') },
44895
+ });
44896
+ if (!result.confirmed)
44897
+ return;
44898
+ try {
44899
+ await firstValueFrom(this.sessionService.revokeAllSessions());
44900
+ this.toast.show({
44901
+ message: this.tt('sessionsAllRevoked'),
44902
+ duration: 2500,
44903
+ color: 'dark',
44904
+ position: 'top',
44905
+ });
44906
+ await this.loadSessions();
44907
+ }
44908
+ catch (err) {
44909
+ this.errors.handle(err, {
44910
+ context: 'security.revokeAllSessions',
44911
+ fallbackKey: 'sessionsError',
44912
+ i18nNamespace: this.ns,
44913
+ });
44914
+ }
44915
+ }
44916
+ /** Abre modal con la lista completa. Cada revoke pasa por `onRevoke`. */
44917
+ async openSessionsModal() {
44918
+ const onRevoke = async (sessionId) => {
44919
+ try {
44920
+ await firstValueFrom(this.sessionService.revokeSession(sessionId));
44921
+ this.toast.show({
44922
+ message: this.tt('sessionsRevoked'),
44923
+ duration: 2500,
44924
+ color: 'dark',
44925
+ position: 'top',
44926
+ });
44927
+ }
44928
+ catch (err) {
44929
+ this.errors.handle(err, {
44930
+ context: 'security.revokeSession.modal',
44931
+ fallbackKey: 'sessionsError',
44932
+ i18nNamespace: this.ns,
44933
+ });
44934
+ throw err;
44935
+ }
44936
+ await this.loadSessions();
44937
+ return this.sortedSessions();
44938
+ };
44939
+ await this.modalService.openSheet({
44940
+ component: SessionListModalComponent,
44941
+ componentProps: {
44942
+ sessions: this.sortedSessions(),
44943
+ title: this.tt('sessionsModalTitle'),
44944
+ closeLabel: this.tt('loginAttemptClose'),
44945
+ thisLabel: this.sessionsThis(),
44946
+ revokeLabel: this.tt('sessionsRevoke'),
44947
+ onRevoke,
44948
+ },
44949
+ initialBreakpoint: 1,
44950
+ breakpoints: [0, 1],
44951
+ showHandle: false,
44952
+ });
44953
+ }
44954
+ async loadSessions() {
44955
+ this.sessionsLoading.set(true);
44956
+ try {
44957
+ const list = await firstValueFrom(this.sessionService.listSessions());
44958
+ this.sessions.set(list);
44959
+ }
44960
+ catch (err) {
44961
+ console.error('[Security] listSessions failed:', err);
44962
+ this.sessions.set([]);
44963
+ }
44964
+ finally {
44965
+ this.sessionsLoading.set(false);
44966
+ }
44967
+ }
44968
+ /** Lee del namespace configurado. */
44969
+ tt(key) {
44970
+ return this.i18n.t(key, this.ns);
44971
+ }
44972
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SecurityViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
44973
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SecurityViewComponent, isStandalone: true, selector: "val-security-view", inputs: { config: "config" }, ngImport: i0, template: `
44974
+ <div class="page">
44975
+ <header class="page-header">
44976
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
44977
+ <val-title
44978
+ [props]="{
44979
+ size: 'large',
44980
+ color: 'dark',
44981
+ bold: false,
44982
+ content: pageDescription(),
44983
+ }"
44984
+ />
44985
+ </header>
44986
+
44987
+ @if (resolvedConfig().showSessions) {
44988
+ <!-- 1. Active sessions - unica vista user-facing de donde tengo sesion. -->
44989
+ <section class="settings-section">
44990
+ @if (hasOtherSessions()) {
44991
+ <val-action-header [props]="sessionsHeader()" />
44992
+ } @else {
44993
+ <val-title
44994
+ [props]="{
44995
+ size: 'medium',
44996
+ color: 'dark',
44997
+ bold: true,
44998
+ content: sessionsTitleWithCount(),
44999
+ }"
45000
+ />
45001
+ }
45002
+
45003
+ <div class="section-body">
45004
+ @if (sessionsLoading()) {
45005
+ <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
45006
+ } @else if (sessions().length === 0) {
45007
+ <val-text
45008
+ [props]="{
45009
+ content: sessionsEmptyLabel(),
45010
+ size: 'small',
45011
+ color: 'medium',
45012
+ bold: false,
45013
+ }"
45014
+ />
45015
+ } @else {
45016
+ <ul class="sessions">
45017
+ @for (s of visibleSessions(); track s.sessionId) {
45018
+ <li class="session" [class.session--current]="s.isCurrent">
45019
+ <ion-icon class="session__icon" [name]="deviceIcon(s)" aria-hidden="true" />
45020
+ <div class="session__body">
45021
+ <val-text
45022
+ [props]="{
45023
+ content: s.deviceInfo || s.deviceId || '—',
45024
+ size: 'medium',
45025
+ color: 'dark',
45026
+ bold: true,
45027
+ }"
45028
+ />
45029
+ <val-text
45030
+ [props]="{
45031
+ content: formatSessionMeta(s),
45032
+ size: 'small',
45033
+ color: 'medium',
45034
+ bold: false,
45035
+ }"
45036
+ />
45037
+ </div>
45038
+ <div class="session__end">
45039
+ @if (s.isCurrent) {
45040
+ <span class="session__badge">{{ sessionsThis() }}</span>
45041
+ } @else {
45042
+ <val-button [props]="revokeOneButtonProps()" (click)="onRevokeOne(s.sessionId)" />
45043
+ }
45044
+ </div>
45045
+ </li>
45046
+ }
45047
+ </ul>
45048
+ @if (hasMoreSessions()) {
45049
+ <div class="row-actions row-actions--center">
45050
+ <val-button [props]="viewAllButtonProps()" (click)="openSessionsModal()" />
45051
+ </div>
45052
+ }
45053
+ }
45054
+ </div>
45055
+ </section>
45056
+ }
45057
+
45058
+ @if (hasActionCards()) {
45059
+ <!-- 2. Quick actions: change password, change email, MFA -->
45060
+ <section class="settings-section">
45061
+ <h2>{{ quickActionsTitle() }}</h2>
45062
+ <ion-grid class="cards-grid">
45063
+ <ion-row>
45064
+ @for (action of actionCards(); track action.token) {
45065
+ <ion-col size="12" size-sm="6">
45066
+ <val-action-card [props]="action" (onClick)="onActionClick(action)" />
45067
+ </ion-col>
45068
+ }
45069
+ </ion-row>
45070
+ </ion-grid>
45071
+ </section>
45072
+ }
45073
+
45074
+ @if (resolvedConfig().showPolicies) {
45075
+ <!-- 3. Related policies -->
45076
+ <section class="settings-section">
45077
+ <h2>{{ policiesTitle() }}</h2>
45078
+ <val-page-links [props]="policyLinksProps()" />
45079
+ </section>
45080
+ }
45081
+
45082
+ <val-change-email-modal
45083
+ [isOpen]="isChangeEmailModalOpen()"
45084
+ (changed)="isChangeEmailModalOpen.set(false)"
45085
+ (dismissed)="isChangeEmailModalOpen.set(false)"
45086
+ />
45087
+
45088
+ <val-change-password-modal
45089
+ [isOpen]="isChangePasswordModalOpen()"
45090
+ (changed)="isChangePasswordModalOpen.set(false)"
45091
+ (dismissed)="isChangePasswordModalOpen.set(false)"
45092
+ />
45093
+
45094
+ <val-mfa-modal
45095
+ [isOpen]="isMfaModalOpen()"
45096
+ [prefillCode]="mfaPrefillCode()"
45097
+ (dismissed)="onMfaModalDismissed()"
45098
+ (enabledViaDeeplink)="onMfaEnabledViaDeeplink()"
45099
+ />
45100
+ </div>
45101
+ `, 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))}h2{font-size:16px;font-weight:600;margin:0 0 4px}.cards-grid{margin:0 -4px;padding:0}.cards-grid ion-col{padding:4px;display:flex;flex-direction:column}.cards-grid ion-col val-action-card{flex:1;display:flex;flex-direction:column}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--center{display:flex;justify-content:center;margin-top:4px}.sessions{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px}.session{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:14px;background:var(--ion-color-light, rgba(0, 0, 0, .04));border:1px solid transparent;transition:background .15s ease,border-color .15s ease}.session--current{background:#2dd36f1a;border-color:#2dd36f4d}.session__icon{font-size:26px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0}.session--current .session__icon{color:var(--ion-color-success-shade, #28ba62)}.session__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}.session__end{flex-shrink:0}.session__badge{display:inline-block;padding:4px 10px;font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--ion-color-success-shade, #28ba62);background:#2dd36f2e;border-radius:999px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonRow, selector: "ion-row" }, { kind: "component", type: IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: DisplayComponent, selector: "val-display", 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: ActionCardComponent, selector: "val-action-card", inputs: ["props"], outputs: ["onClick"] }, { kind: "component", type: ActionHeaderComponent, selector: "val-action-header", inputs: ["props"] }, { kind: "component", type: PageLinksComponent, selector: "val-page-links", inputs: ["props"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: ChangeEmailModalComponent, selector: "val-change-email-modal", inputs: ["isOpen"], outputs: ["changed", "dismissed"] }, { kind: "component", type: ChangePasswordModalComponent, selector: "val-change-password-modal", inputs: ["isOpen"], outputs: ["changed", "dismissed"] }, { kind: "component", type: MfaModalComponent, selector: "val-mfa-modal", inputs: ["isOpen", "prefillCode"], outputs: ["changed", "enabledViaDeeplink", "dismissed"] }] }); }
45102
+ }
45103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SecurityViewComponent, decorators: [{
45104
+ type: Component,
45105
+ args: [{ selector: 'val-security-view', standalone: true, imports: [
45106
+ CommonModule,
45107
+ IonGrid,
45108
+ IonRow,
45109
+ IonCol,
45110
+ IonIcon,
45111
+ DisplayComponent,
45112
+ TitleComponent,
45113
+ TextComponent,
45114
+ ButtonComponent,
45115
+ ActionCardComponent,
45116
+ ActionHeaderComponent,
45117
+ PageLinksComponent,
45118
+ SkeletonLayoutComponent,
45119
+ ChangeEmailModalComponent,
45120
+ ChangePasswordModalComponent,
45121
+ MfaModalComponent,
45122
+ ], template: `
45123
+ <div class="page">
45124
+ <header class="page-header">
45125
+ <val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
45126
+ <val-title
45127
+ [props]="{
45128
+ size: 'large',
45129
+ color: 'dark',
45130
+ bold: false,
45131
+ content: pageDescription(),
45132
+ }"
45133
+ />
45134
+ </header>
45135
+
45136
+ @if (resolvedConfig().showSessions) {
45137
+ <!-- 1. Active sessions - unica vista user-facing de donde tengo sesion. -->
45138
+ <section class="settings-section">
45139
+ @if (hasOtherSessions()) {
45140
+ <val-action-header [props]="sessionsHeader()" />
45141
+ } @else {
45142
+ <val-title
45143
+ [props]="{
45144
+ size: 'medium',
45145
+ color: 'dark',
45146
+ bold: true,
45147
+ content: sessionsTitleWithCount(),
45148
+ }"
45149
+ />
45150
+ }
45151
+
45152
+ <div class="section-body">
45153
+ @if (sessionsLoading()) {
45154
+ <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
45155
+ } @else if (sessions().length === 0) {
45156
+ <val-text
45157
+ [props]="{
45158
+ content: sessionsEmptyLabel(),
45159
+ size: 'small',
45160
+ color: 'medium',
45161
+ bold: false,
45162
+ }"
45163
+ />
45164
+ } @else {
45165
+ <ul class="sessions">
45166
+ @for (s of visibleSessions(); track s.sessionId) {
45167
+ <li class="session" [class.session--current]="s.isCurrent">
45168
+ <ion-icon class="session__icon" [name]="deviceIcon(s)" aria-hidden="true" />
45169
+ <div class="session__body">
45170
+ <val-text
45171
+ [props]="{
45172
+ content: s.deviceInfo || s.deviceId || '—',
45173
+ size: 'medium',
45174
+ color: 'dark',
45175
+ bold: true,
45176
+ }"
45177
+ />
45178
+ <val-text
45179
+ [props]="{
45180
+ content: formatSessionMeta(s),
45181
+ size: 'small',
45182
+ color: 'medium',
45183
+ bold: false,
45184
+ }"
45185
+ />
45186
+ </div>
45187
+ <div class="session__end">
45188
+ @if (s.isCurrent) {
45189
+ <span class="session__badge">{{ sessionsThis() }}</span>
45190
+ } @else {
45191
+ <val-button [props]="revokeOneButtonProps()" (click)="onRevokeOne(s.sessionId)" />
45192
+ }
45193
+ </div>
45194
+ </li>
45195
+ }
45196
+ </ul>
45197
+ @if (hasMoreSessions()) {
45198
+ <div class="row-actions row-actions--center">
45199
+ <val-button [props]="viewAllButtonProps()" (click)="openSessionsModal()" />
45200
+ </div>
45201
+ }
45202
+ }
45203
+ </div>
45204
+ </section>
45205
+ }
45206
+
45207
+ @if (hasActionCards()) {
45208
+ <!-- 2. Quick actions: change password, change email, MFA -->
45209
+ <section class="settings-section">
45210
+ <h2>{{ quickActionsTitle() }}</h2>
45211
+ <ion-grid class="cards-grid">
45212
+ <ion-row>
45213
+ @for (action of actionCards(); track action.token) {
45214
+ <ion-col size="12" size-sm="6">
45215
+ <val-action-card [props]="action" (onClick)="onActionClick(action)" />
45216
+ </ion-col>
45217
+ }
45218
+ </ion-row>
45219
+ </ion-grid>
45220
+ </section>
45221
+ }
45222
+
45223
+ @if (resolvedConfig().showPolicies) {
45224
+ <!-- 3. Related policies -->
45225
+ <section class="settings-section">
45226
+ <h2>{{ policiesTitle() }}</h2>
45227
+ <val-page-links [props]="policyLinksProps()" />
45228
+ </section>
45229
+ }
45230
+
45231
+ <val-change-email-modal
45232
+ [isOpen]="isChangeEmailModalOpen()"
45233
+ (changed)="isChangeEmailModalOpen.set(false)"
45234
+ (dismissed)="isChangeEmailModalOpen.set(false)"
45235
+ />
45236
+
45237
+ <val-change-password-modal
45238
+ [isOpen]="isChangePasswordModalOpen()"
45239
+ (changed)="isChangePasswordModalOpen.set(false)"
45240
+ (dismissed)="isChangePasswordModalOpen.set(false)"
45241
+ />
45242
+
45243
+ <val-mfa-modal
45244
+ [isOpen]="isMfaModalOpen()"
45245
+ [prefillCode]="mfaPrefillCode()"
45246
+ (dismissed)="onMfaModalDismissed()"
45247
+ (enabledViaDeeplink)="onMfaEnabledViaDeeplink()"
45248
+ />
45249
+ </div>
45250
+ `, 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))}h2{font-size:16px;font-weight:600;margin:0 0 4px}.cards-grid{margin:0 -4px;padding:0}.cards-grid ion-col{padding:4px;display:flex;flex-direction:column}.cards-grid ion-col val-action-card{flex:1;display:flex;flex-direction:column}.section-body{margin-top:16px;display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--center{display:flex;justify-content:center;margin-top:4px}.sessions{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px}.session{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:14px;background:var(--ion-color-light, rgba(0, 0, 0, .04));border:1px solid transparent;transition:background .15s ease,border-color .15s ease}.session--current{background:#2dd36f1a;border-color:#2dd36f4d}.session__icon{font-size:26px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0}.session--current .session__icon{color:var(--ion-color-success-shade, #28ba62)}.session__body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}.session__end{flex-shrink:0}.session__badge{display:inline-block;padding:4px 10px;font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--ion-color-success-shade, #28ba62);background:#2dd36f2e;border-radius:999px}\n"] }]
45251
+ }], ctorParameters: () => [], propDecorators: { config: [{
45252
+ type: Input
45253
+ }] } });
45254
+
45255
+ /**
45256
+ * Helper para montar la vista Seguridad (`val-security-view`) como ruta en una
45257
+ * app del factory. El `config` se pasa por route `data` (`securityConfig`) y el
45258
+ * componente lo lee como fallback de su `@Input() config`.
45259
+ *
45260
+ * @example
45261
+ * ```ts
45262
+ * // settings.routes.ts
45263
+ * export const settingsRoutes: Routes = [
45264
+ * ...provideValtechSecurityRoutes(),
45265
+ * { path: 'preferences', loadComponent: () => ... },
45266
+ * ];
45267
+ *
45268
+ * // con config acotada:
45269
+ * ...provideValtechSecurityRoutes({ config: { showPolicies: false } }),
45270
+ * ...provideValtechSecurityRoutes({ config: { homeRoute: '/app/dashboard' } }),
45271
+ * ```
45272
+ */
45273
+ function provideValtechSecurityRoutes(opts) {
45274
+ return [
45275
+ {
45276
+ path: opts?.path ?? 'security',
45277
+ component: SecurityViewComponent,
45278
+ data: { securityConfig: opts?.config },
45279
+ },
45280
+ ];
45281
+ }
45282
+
45283
+ /** Built-in label sets for the three platform locales. */
45284
+ const CALLOUT_LABELS = {
45285
+ es: {
45286
+ NOTE: 'Nota',
45287
+ TIP: 'Tip',
45288
+ INFO: 'Info',
45289
+ IMPORTANT: 'Importante',
45290
+ WARNING: 'Atención',
45291
+ CAUTION: 'Precaución',
45292
+ },
45293
+ en: {
45294
+ NOTE: 'Note',
45295
+ TIP: 'Tip',
45296
+ INFO: 'Info',
45297
+ IMPORTANT: 'Important',
45298
+ WARNING: 'Warning',
45299
+ CAUTION: 'Caution',
45300
+ },
45301
+ pt: {
45302
+ NOTE: 'Nota',
45303
+ TIP: 'Dica',
45304
+ INFO: 'Info',
45305
+ IMPORTANT: 'Importante',
45306
+ WARNING: 'Atenção',
45307
+ CAUTION: 'Cuidado',
45308
+ },
45309
+ };
45310
+ /**
45311
+ * Per-callout-kind color (Ionic semantic color). Fixed across locales since
45312
+ * red = danger no matter what language you speak.
45313
+ */
45314
+ const CALLOUT_COLORS = {
45315
+ NOTE: 'primary',
45316
+ TIP: 'success',
45317
+ INFO: 'tertiary',
45318
+ IMPORTANT: 'warning',
45319
+ WARNING: 'warning',
45320
+ CAUTION: 'danger',
45321
+ };
45322
+ const BOX_DRAWING = /[┌┐└┘├┤┬┴┼─│╔╗╚╝═║]/;
45323
+ const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT)\]\s*(.*)$/i;
45324
+ const FENCE_RE = /^```([\w-]*)\s*$/;
45325
+ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
45326
+ const SEPARATOR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
45327
+ const UNORDERED_RE = /^\s*[-*+]\s+(.+)$/;
45328
+ const CHECKLIST_RE = /^\s*[-*+]\s+\[([ xX])\]\s+(.+)$/;
45329
+ const ORDERED_RE = /^\s*\d+\.\s+(.+)$/;
45330
+ const TABLE_DIVIDER_RE = /^\s*\|?[\s:|-]+\|?\s*$/;
45331
+ const TABLE_ROW_RE = /^\s*\|(.+)\|\s*$/;
45332
+ /**
45333
+ * Pure Markdown → ArticleMetadata parser. No Angular deps — usable from Node scripts
45334
+ * (build-time generation) and from the Angular `MarkdownArticleParserService` wrapper.
45335
+ *
45336
+ * Supported syntax:
45337
+ * - Headings (#, ##, ### …) → title / subtitle
45338
+ * - Paragraphs → paragraph (with `processLinks` + `allowPartialBold`)
45339
+ * - Lists (-, *, 1.) → unordered / ordered list (- [ ] / - [x] for checklist)
45340
+ * - Blockquotes (>) → quote, with GitHub callouts `> [!NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT]` mapped to notes
45341
+ * - Code fences (```lang) → code
45342
+ * - Box-drawing ASCII art (┌┐└┘│─) auto-wrapped as code
45343
+ * - Tables → flattened into paragraphs with bold keys
45344
+ * - Horizontal rules (---, ***, ___) → separator
45345
+ */
45346
+ function parseMarkdownArticle(markdown, options) {
45347
+ const lines = normalize(markdown).split('\n');
45348
+ const elements = [];
45349
+ const labels = options?.calloutLabels ??
45350
+ (options?.locale ? CALLOUT_LABELS[options.locale] : CALLOUT_LABELS.es);
45351
+ let i = 0;
45352
+ while (i < lines.length) {
45353
+ const line = lines[i];
45354
+ if (line.trim() === '') {
45355
+ i++;
45356
+ continue;
45357
+ }
45358
+ const fence = line.match(FENCE_RE);
45359
+ if (fence) {
45360
+ const lang = fence[1] || undefined;
45361
+ const body = [];
45362
+ i++;
45363
+ while (i < lines.length && !FENCE_RE.test(lines[i])) {
45364
+ body.push(lines[i]);
45365
+ i++;
45366
+ }
45367
+ i++;
45368
+ elements.push({
45369
+ type: 'code',
45370
+ props: { code: body.join('\n'), language: lang, theme: 'dark' },
45371
+ });
45372
+ continue;
45373
+ }
45374
+ if (BOX_DRAWING.test(line)) {
45375
+ const body = [];
45376
+ while (i < lines.length && (lines[i].trim() === '' || BOX_DRAWING.test(lines[i]))) {
45377
+ body.push(lines[i]);
45378
+ i++;
45379
+ }
45380
+ while (body.length && body[body.length - 1].trim() === '')
45381
+ body.pop();
45382
+ elements.push({
45383
+ type: 'code',
45384
+ props: { code: body.join('\n'), language: 'text', theme: 'dark' },
45385
+ });
45386
+ continue;
45387
+ }
45388
+ if (SEPARATOR_RE.test(line)) {
45389
+ elements.push({ type: 'separator', props: { style: 'line' } });
45390
+ i++;
45391
+ continue;
45392
+ }
45393
+ const heading = line.match(HEADING_RE);
45394
+ if (heading) {
45395
+ elements.push(makeHeading(heading[1].length, heading[2].trim()));
45396
+ i++;
45397
+ continue;
45398
+ }
45399
+ if (line.startsWith('>')) {
45400
+ const block = [];
45401
+ while (i < lines.length && lines[i].startsWith('>')) {
45402
+ block.push(lines[i]);
45403
+ i++;
45404
+ }
45405
+ elements.push(makeQuoteOrCallout(block, labels));
45406
+ continue;
45407
+ }
45408
+ if (TABLE_ROW_RE.test(line)) {
45409
+ const rows = [];
45410
+ while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
45411
+ rows.push(lines[i]);
45412
+ i++;
45413
+ }
45414
+ elements.push(...makeTable(rows));
45415
+ continue;
45416
+ }
45417
+ if (CHECKLIST_RE.test(line) || UNORDERED_RE.test(line) || ORDERED_RE.test(line)) {
45418
+ const listType = CHECKLIST_RE.test(line)
45419
+ ? 'checklist'
45420
+ : ORDERED_RE.test(line)
45421
+ ? 'ordered'
45422
+ : 'unordered';
45423
+ const items = [];
45424
+ while (i < lines.length && lines[i].trim() !== '') {
45425
+ const cur = lines[i];
45426
+ const check = cur.match(CHECKLIST_RE);
45427
+ const unord = cur.match(UNORDERED_RE);
45428
+ const ord = cur.match(ORDERED_RE);
45429
+ if (check)
45430
+ items.push({ text: processInlineBold(check[2].trim()) });
45431
+ else if (ord && listType === 'ordered')
45432
+ items.push({ text: processInlineBold(ord[1].trim()) });
45433
+ else if (unord && listType !== 'ordered')
45434
+ items.push({ text: processInlineBold(unord[1].trim()) });
45435
+ else
45436
+ break;
45437
+ i++;
45438
+ }
45439
+ elements.push({ type: 'list', props: { items, listType } });
45440
+ continue;
45441
+ }
45442
+ const paragraph = [];
45443
+ while (i < lines.length && lines[i].trim() !== '' && !startsNewBlock(lines[i])) {
45444
+ paragraph.push(lines[i]);
45445
+ i++;
45446
+ }
45447
+ const text = processInlineBold(paragraph.join(' ').trim());
45448
+ if (text) {
45449
+ elements.push({
45450
+ type: 'paragraph',
45451
+ props: {
45452
+ content: text,
45453
+ size: 'medium',
45454
+ color: 'dark',
45455
+ bold: false,
45456
+ processLinks: true,
45457
+ allowPartialBold: true,
45458
+ },
45459
+ });
45460
+ }
45461
+ }
45462
+ // Strip parser-only options before merging into ArticleMetadata
45463
+ const { locale: _l, calloutLabels: _c, ...metadataOverrides } = options ?? {};
45464
+ return {
45465
+ elements,
45466
+ maxWidth: '900px',
45467
+ centered: true,
45468
+ theme: 'auto',
45469
+ ...metadataOverrides,
45470
+ };
45471
+ }
45472
+ function normalize(md) {
45473
+ return md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
45474
+ }
45475
+ function startsNewBlock(line) {
45476
+ return (HEADING_RE.test(line) ||
45477
+ FENCE_RE.test(line) ||
45478
+ SEPARATOR_RE.test(line) ||
45479
+ TABLE_ROW_RE.test(line) ||
45480
+ UNORDERED_RE.test(line) ||
45481
+ ORDERED_RE.test(line) ||
45482
+ CHECKLIST_RE.test(line) ||
45483
+ line.startsWith('>') ||
45484
+ BOX_DRAWING.test(line));
45485
+ }
45486
+ function processInlineBold(text) {
45487
+ return text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
45488
+ }
45489
+ function makeHeading(level, content) {
45490
+ if (level === 1) {
45491
+ return {
45492
+ type: 'title',
45493
+ props: { content, size: 'xlarge', color: 'dark', bold: true },
45494
+ };
45495
+ }
45496
+ const size = level === 2 ? 'large' : level === 3 ? 'medium' : 'small';
45497
+ return {
45498
+ type: 'subtitle',
45499
+ props: { content, size, color: 'dark', bold: true },
45500
+ };
45501
+ }
45502
+ function makeQuoteOrCallout(block, labels) {
45503
+ const first = block[0];
45504
+ const callout = first.match(CALLOUT_RE);
45505
+ const lines = block.map(l => l.replace(/^>\s?/, ''));
45506
+ if (callout) {
45507
+ const type = callout[1].toUpperCase();
45508
+ const firstLineRest = callout[2] || '';
45509
+ const rest = lines.slice(1).join(' ').trim();
45510
+ const text = processInlineBold([firstLineRest, rest].filter(Boolean).join(' ').trim());
45511
+ return makeNote(type, text, labels);
45512
+ }
45513
+ const text = processInlineBold(lines.join(' ').trim());
45514
+ return {
45515
+ type: 'quote',
45516
+ props: {
45517
+ content: text,
45518
+ size: 'medium',
45519
+ color: 'medium',
45520
+ bold: false,
45521
+ showQuoteMark: true,
45522
+ alignment: 'left',
45523
+ },
45524
+ };
45525
+ }
45526
+ function makeNote(kind, text, labels) {
45527
+ return {
45528
+ type: 'note',
45529
+ props: {
45530
+ text,
45531
+ prefix: `${labels[kind]}:`,
45532
+ color: CALLOUT_COLORS[kind],
45533
+ textColor: 'dark',
45534
+ size: 'medium',
45535
+ rounded: true,
45536
+ allowPartialBold: true,
45537
+ processLinks: true,
45538
+ },
45539
+ };
45540
+ }
45541
+ function makeTable(rows) {
45542
+ const parsed = rows
45543
+ .filter(r => !TABLE_DIVIDER_RE.test(r))
45544
+ .map(r => r
45545
+ .trim()
45546
+ .replace(/^\|/, '')
45547
+ .replace(/\|$/, '')
45548
+ .split('|')
45549
+ .map(c => c.trim()));
45550
+ if (parsed.length === 0)
45551
+ return [];
45552
+ const header = parsed[0];
45553
+ const dataRows = parsed.slice(1);
45554
+ if (dataRows.length === 0) {
45555
+ return [
45556
+ {
45557
+ type: 'paragraph',
45558
+ props: {
45559
+ content: header.join(' · '),
45560
+ size: 'medium',
45561
+ color: 'dark',
45562
+ bold: false,
45563
+ processLinks: true,
45564
+ allowPartialBold: true,
45565
+ },
45566
+ },
45567
+ ];
45568
+ }
45569
+ return dataRows.map(row => {
45570
+ const pairs = row.map((cell, idx) => {
45571
+ const key = header[idx] ?? '';
45572
+ return key ? `<strong>${key}:</strong> ${cell}` : cell;
45573
+ });
45574
+ return {
45575
+ type: 'paragraph',
45576
+ props: {
45577
+ content: pairs.join(' · '),
45578
+ size: 'medium',
45579
+ color: 'dark',
45580
+ bold: false,
45581
+ processLinks: true,
45582
+ allowPartialBold: true,
45583
+ },
45584
+ };
45585
+ });
45586
+ }
45587
+
45588
+ /**
45589
+ * `val-faq`
45590
+ *
45591
+ * Organism de preguntas frecuentes. Q&A categorizado con accordion colapsable
45592
+ * y buscador en vivo. Pensado para vistas FAQ públicas — el caller puede
45593
+ * reusar la misma data (`FaqMetadata.categories`) para emitir el structured
45594
+ * data `FAQPage` (JSON-LD) y ganar rich results en buscadores.
45595
+ *
45596
+ * Las respuestas (`FaqItem.answer`) son **Markdown** — se parsean con
45597
+ * `parseMarkdownArticle` y se renderizan vía `val-article`. Soportan links,
45598
+ * listas, negritas, tablas, etc.
45599
+ *
45600
+ * @example
45601
+ * ```html
45602
+ * <val-faq [props]="{ categories: faqCategories, searchable: true }" />
45603
+ * ```
45604
+ */
45605
+ class FaqComponent {
45606
+ constructor() {
45607
+ this.i18n = inject(I18nService);
45608
+ this.props_ = signal({ categories: [] });
45609
+ /** Término de búsqueda actual. */
45610
+ this.query = signal('');
45611
+ /**
45612
+ * Cache de respuestas parseadas (Markdown → ArticleMetadata). Se reconstruye
45613
+ * solo cuando cambian las categorías — el parseo NO corre por keystroke.
45614
+ * Keyed por id de item (los ids son únicos en toda la FAQ).
45615
+ */
45616
+ this.parsedAnswers = computed(() => {
45617
+ const locale = (this.i18n.lang() ?? 'es');
45618
+ const map = new Map();
45619
+ for (const cat of this.props_().categories ?? []) {
45620
+ for (const item of cat.items) {
45621
+ map.set(item.id, parseMarkdownArticle(item.answer, { locale }));
45622
+ }
45623
+ }
45624
+ return map;
45625
+ });
45626
+ /**
45627
+ * Categorías visibles tras el filtro. Una categoría desaparece si ninguna
45628
+ * de sus preguntas matchea. Match sobre pregunta + respuesta (raw markdown),
45629
+ * case-insensitive.
45630
+ */
45631
+ this.visibleCategories = computed(() => {
45632
+ const q = this.query().trim().toLowerCase();
45633
+ const cats = this.props_().categories ?? [];
45634
+ if (!q)
45635
+ return cats;
45636
+ return cats
45637
+ .map(cat => ({
45638
+ ...cat,
45639
+ items: cat.items.filter(it => it.question.toLowerCase().includes(q) || it.answer.toLowerCase().includes(q)),
45640
+ }))
45641
+ .filter(cat => cat.items.length > 0);
45642
+ });
45643
+ /** `true` si se debe mostrar el label de cada categoría. */
45644
+ this.showCategoryLabel = computed(() => {
45645
+ const p = this.props_();
45646
+ if (p.hideSingleCategoryLabel === false)
45647
+ return true;
45648
+ return (p.categories?.length ?? 0) > 1;
45649
+ });
45650
+ }
45651
+ set props(value) {
45652
+ if (value)
45653
+ this.props_.set(value);
45654
+ }
45655
+ get props() {
45656
+ return this.props_();
45657
+ }
45658
+ /** Respuesta parseada de un item — desde el cache. */
45659
+ answerOf(item) {
45660
+ return this.parsedAnswers().get(item.id) ?? { elements: [] };
45661
+ }
45662
+ onSearch(term) {
45663
+ this.query.set(term ?? '');
43741
45664
  }
43742
45665
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FaqComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
43743
45666
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FaqComponent, isStandalone: true, selector: "val-faq", inputs: { props: "props" }, ngImport: i0, template: `
@@ -44957,489 +46880,144 @@ class AuthBackgroundComponent {
44957
46880
  [attr.stroke-width]="wave.thickness"
44958
46881
  [class.travel-left]="wave.travelDirection === 'left'"
44959
46882
  [class.travel-right]="wave.travelDirection === 'right'"
44960
- [style.opacity]="wave.opacity"
44961
- [style.--pulse-duration]="wave.pulseDuration + 's'"
44962
- [style.--pulse-delay]="wave.pulseDelay + 's'"
44963
- [style.--travel-duration]="wave.travelDuration + 's'"
44964
- [style.--travel-delay]="wave.travelDelay + 's'"
44965
- />
44966
- }
44967
- </svg>
44968
- </div>
44969
-
44970
- <div class="content-layer">
44971
- <ng-content></ng-content>
44972
- </div>
44973
- </section>
44974
- `, isInline: true, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
44975
- }
44976
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthBackgroundComponent, decorators: [{
44977
- type: Component,
44978
- args: [{ selector: 'val-auth-background', standalone: true, imports: [CommonModule], template: `
44979
- <section class="auth-background" [style.min-height]="config().minHeight">
44980
- @if (config().showGrain) {
44981
- <div class="grain"></div>
44982
- }
44983
- @if (config().showGlass) {
44984
- <div class="glass-layer"></div>
44985
- }
44986
- <div class="wave-bg">
44987
- <svg class="waves" viewBox="0 0 1440 800" preserveAspectRatio="none">
44988
- @for (wave of waves(); track wave.id) {
44989
- <path
44990
- [attr.d]="wave.path"
44991
- [attr.stroke]="wave.color"
44992
- [attr.stroke-width]="wave.thickness"
44993
- [class.travel-left]="wave.travelDirection === 'left'"
44994
- [class.travel-right]="wave.travelDirection === 'right'"
44995
- [style.opacity]="wave.opacity"
44996
- [style.--pulse-duration]="wave.pulseDuration + 's'"
44997
- [style.--pulse-delay]="wave.pulseDelay + 's'"
44998
- [style.--travel-duration]="wave.travelDuration + 's'"
44999
- [style.--travel-delay]="wave.travelDelay + 's'"
45000
- />
45001
- }
45002
- </svg>
45003
- </div>
45004
-
45005
- <div class="content-layer">
45006
- <ng-content></ng-content>
45007
- </div>
45008
- </section>
45009
- `, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"] }]
45010
- }], ctorParameters: () => [], propDecorators: { props: [{
45011
- type: Input
45012
- }] } });
45013
-
45014
- /**
45015
- * Clave para persistir el idioma en localStorage
45016
- */
45017
- const LANG_STORAGE_KEY = 'app_lang';
45018
- /**
45019
- * Idioma por defecto
45020
- */
45021
- const DEFAULT_LANG = 'es';
45022
- /**
45023
- * Servicio mínimo de localización.
45024
- *
45025
- * - Lee el idioma desde localStorage al iniciar
45026
- * - Proporciona método para cambiar idioma (recarga la página)
45027
- * - Sin observables ni subscripciones
45028
- *
45029
- * @example
45030
- * // En un componente
45031
- * locale = inject(LocaleService);
45032
- *
45033
- * // Obtener idioma actual
45034
- * console.log(this.locale.lang); // 'es' o 'en'
45035
- *
45036
- * // Cambiar idioma (recarga la página)
45037
- * this.locale.setLang('en');
45038
- */
45039
- class LocaleService {
45040
- constructor(platformId) {
45041
- this.isBrowser = isPlatformBrowser(platformId);
45042
- this.lang = this.getStoredLang();
45043
- }
45044
- /**
45045
- * Cambia el idioma de la aplicación.
45046
- * Guarda en localStorage y recarga la página para aplicar el cambio.
45047
- *
45048
- * @param lang Nuevo idioma ('es' o 'en')
45049
- */
45050
- setLang(lang) {
45051
- if (lang === this.lang || !this.isBrowser) {
45052
- return;
45053
- }
45054
- localStorage.setItem(LANG_STORAGE_KEY, lang);
45055
- window.location.reload();
45056
- }
45057
- /**
45058
- * Obtiene el idioma almacenado en localStorage.
45059
- * Si no existe o no es válido, retorna el idioma por defecto.
45060
- * En SSR (sin localStorage) retorna el default.
45061
- */
45062
- getStoredLang() {
45063
- if (!this.isBrowser) {
45064
- return DEFAULT_LANG;
45065
- }
45066
- const stored = localStorage.getItem(LANG_STORAGE_KEY);
45067
- if (stored === 'es' || stored === 'en') {
45068
- return stored;
45069
- }
45070
- return DEFAULT_LANG;
45071
- }
45072
- /**
45073
- * Verifica si el idioma actual es español
45074
- */
45075
- get isSpanish() {
45076
- return this.lang === 'es';
45077
- }
45078
- /**
45079
- * Verifica si el idioma actual es inglés
45080
- */
45081
- get isEnglish() {
45082
- return this.lang === 'en';
45083
- }
45084
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
45085
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, providedIn: 'root' }); }
45086
- }
45087
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, decorators: [{
45088
- type: Injectable,
45089
- args: [{ providedIn: 'root' }]
45090
- }], ctorParameters: () => [{ type: Object, decorators: [{
45091
- type: Inject,
45092
- args: [PLATFORM_ID]
45093
- }] }] });
45094
-
45095
- // Types for valtech-components services
45096
- // ValtechConfig and LangProvider have been removed in v3.0.0
45097
- // Use LocaleService for language management instead
45098
-
45099
- /**
45100
- * Default modal sizes.
45101
- */
45102
- const MODAL_SIZES = {
45103
- small: { width: '320px', height: '220px' },
45104
- medium: { width: '500px', height: '400px' },
45105
- large: { width: '800px', height: '80vh' },
45106
- full: { width: '100%', height: '100%' },
45107
- auto: { width: 'auto', height: '50vh' },
45108
- };
45109
- /**
45110
- * Default modal buttons.
45111
- */
45112
- const DEFAULT_MODAL_CONFIRM_BUTTON = {
45113
- text: 'Confirm',
45114
- role: 'confirm',
45115
- color: 'primary',
45116
- fill: 'solid',
45117
- };
45118
- const DEFAULT_MODAL_CANCEL_BUTTON = {
45119
- text: 'Cancel',
45120
- role: 'cancel',
45121
- color: 'medium',
45122
- fill: 'clear',
45123
- };
45124
-
45125
- /**
45126
- * Internal component for simple content modals.
45127
- * Used by ModalService.openSimple()
45128
- *
45129
- * Header + título + close button delegados a `val-modal-shell` (estándar
45130
- * canónico — ver Regla #5 en `frontend/CLAUDE.md`). El cuerpo (innerHTML) +
45131
- * footer con botones quedan locales.
45132
- */
45133
- class SimpleModalContentComponent {
45134
- constructor() {
45135
- this.title = '';
45136
- this.content = '';
45137
- this.buttons = [];
45138
- this.showCloseButton = true;
45139
- }
45140
- async onButtonClick(button) {
45141
- if (button.handler) {
45142
- const result = await button.handler();
45143
- // If handler returns false, don't dismiss
45144
- if (result === false) {
45145
- return;
45146
- }
45147
- }
45148
- this._modalRef?.dismiss(undefined, button.role || 'confirm');
45149
- }
45150
- onDismiss(role) {
45151
- this._modalRef?.dismiss(undefined, role);
45152
- }
45153
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleModalContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
45154
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SimpleModalContentComponent, isStandalone: true, selector: "val-simple-modal-content", inputs: { title: "title", content: "content", buttons: "buttons", showCloseButton: "showCloseButton", _modalRef: "_modalRef" }, ngImport: i0, template: `
45155
- <val-modal-shell [title]="title" [showClose]="showCloseButton" (close)="onDismiss('close')">
45156
- <div [innerHTML]="content"></div>
45157
- </val-modal-shell>
45158
-
45159
- @if (buttons && buttons.length > 0) {
45160
- <ion-footer>
45161
- <ion-toolbar>
45162
- <div class="button-container">
45163
- @for (button of buttons; track button.text) {
45164
- <ion-button
45165
- [color]="button.color || 'medium'"
45166
- [fill]="button.fill || 'solid'"
45167
- [size]="button.size || 'default'"
45168
- [disabled]="button.disabled || button.loading"
45169
- [class]="button.cssClass"
45170
- (click)="onButtonClick(button)"
45171
- >
45172
- @if (button.loading) {
45173
- <ion-spinner name="circular"></ion-spinner>
45174
- } @else {
45175
- {{ button.text }}
45176
- }
45177
- </ion-button>
45178
- }
45179
- </div>
45180
- </ion-toolbar>
45181
- </ion-footer>
45182
- }
45183
- `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}ion-footer{flex-shrink:0}.button-container{display:flex;justify-content:flex-end;gap:8px;padding:8px 0}ion-footer ion-toolbar{--padding-start: 16px;--padding-end: 16px}:host ::ng-deep ul,:host ::ng-deep ol{padding-left:20px;margin:8px 0}:host ::ng-deep li{margin:4px 0}:host ::ng-deep p{margin:8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { 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: IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ModalShellComponent, selector: "val-modal-shell", inputs: ["title", "subtitle", "closeLabel", "showClose"], outputs: ["close"] }] }); }
46883
+ [style.opacity]="wave.opacity"
46884
+ [style.--pulse-duration]="wave.pulseDuration + 's'"
46885
+ [style.--pulse-delay]="wave.pulseDelay + 's'"
46886
+ [style.--travel-duration]="wave.travelDuration + 's'"
46887
+ [style.--travel-delay]="wave.travelDelay + 's'"
46888
+ />
46889
+ }
46890
+ </svg>
46891
+ </div>
46892
+
46893
+ <div class="content-layer">
46894
+ <ng-content></ng-content>
46895
+ </div>
46896
+ </section>
46897
+ `, isInline: true, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
45184
46898
  }
45185
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SimpleModalContentComponent, decorators: [{
46899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthBackgroundComponent, decorators: [{
45186
46900
  type: Component,
45187
- args: [{ selector: 'val-simple-modal-content', standalone: true, imports: [CommonModule, IonToolbar, IonButton, IonFooter, IonSpinner, ModalShellComponent], template: `
45188
- <val-modal-shell [title]="title" [showClose]="showCloseButton" (close)="onDismiss('close')">
45189
- <div [innerHTML]="content"></div>
45190
- </val-modal-shell>
46901
+ args: [{ selector: 'val-auth-background', standalone: true, imports: [CommonModule], template: `
46902
+ <section class="auth-background" [style.min-height]="config().minHeight">
46903
+ @if (config().showGrain) {
46904
+ <div class="grain"></div>
46905
+ }
46906
+ @if (config().showGlass) {
46907
+ <div class="glass-layer"></div>
46908
+ }
46909
+ <div class="wave-bg">
46910
+ <svg class="waves" viewBox="0 0 1440 800" preserveAspectRatio="none">
46911
+ @for (wave of waves(); track wave.id) {
46912
+ <path
46913
+ [attr.d]="wave.path"
46914
+ [attr.stroke]="wave.color"
46915
+ [attr.stroke-width]="wave.thickness"
46916
+ [class.travel-left]="wave.travelDirection === 'left'"
46917
+ [class.travel-right]="wave.travelDirection === 'right'"
46918
+ [style.opacity]="wave.opacity"
46919
+ [style.--pulse-duration]="wave.pulseDuration + 's'"
46920
+ [style.--pulse-delay]="wave.pulseDelay + 's'"
46921
+ [style.--travel-duration]="wave.travelDuration + 's'"
46922
+ [style.--travel-delay]="wave.travelDelay + 's'"
46923
+ />
46924
+ }
46925
+ </svg>
46926
+ </div>
45191
46927
 
45192
- @if (buttons && buttons.length > 0) {
45193
- <ion-footer>
45194
- <ion-toolbar>
45195
- <div class="button-container">
45196
- @for (button of buttons; track button.text) {
45197
- <ion-button
45198
- [color]="button.color || 'medium'"
45199
- [fill]="button.fill || 'solid'"
45200
- [size]="button.size || 'default'"
45201
- [disabled]="button.disabled || button.loading"
45202
- [class]="button.cssClass"
45203
- (click)="onButtonClick(button)"
45204
- >
45205
- @if (button.loading) {
45206
- <ion-spinner name="circular"></ion-spinner>
45207
- } @else {
45208
- {{ button.text }}
45209
- }
45210
- </ion-button>
45211
- }
45212
- </div>
45213
- </ion-toolbar>
45214
- </ion-footer>
45215
- }
45216
- `, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}ion-footer{flex-shrink:0}.button-container{display:flex;justify-content:flex-end;gap:8px;padding:8px 0}ion-footer ion-toolbar{--padding-start: 16px;--padding-end: 16px}:host ::ng-deep ul,:host ::ng-deep ol{padding-left:20px;margin:8px 0}:host ::ng-deep li{margin:4px 0}:host ::ng-deep p{margin:8px 0}\n"] }]
45217
- }], propDecorators: { title: [{
45218
- type: Input
45219
- }], content: [{
45220
- type: Input
45221
- }], buttons: [{
45222
- type: Input
45223
- }], showCloseButton: [{
45224
- type: Input
45225
- }], _modalRef: [{
46928
+ <div class="content-layer">
46929
+ <ng-content></ng-content>
46930
+ </div>
46931
+ </section>
46932
+ `, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"] }]
46933
+ }], ctorParameters: () => [], propDecorators: { props: [{
45226
46934
  type: Input
45227
46935
  }] } });
45228
46936
 
45229
46937
  /**
45230
- * ModalService
45231
- *
45232
- * A service for creating and managing modals with various configurations.
46938
+ * Clave para persistir el idioma en localStorage
46939
+ */
46940
+ const LANG_STORAGE_KEY = 'app_lang';
46941
+ /**
46942
+ * Idioma por defecto
46943
+ */
46944
+ const DEFAULT_LANG = 'es';
46945
+ /**
46946
+ * Servicio mínimo de localización.
45233
46947
  *
45234
- * @example Basic component modal
45235
- * ```typescript
45236
- * const result = await modalService.open({
45237
- * component: MyFormComponent,
45238
- * componentProps: { userId: 123 },
45239
- * header: { title: 'Edit User', showCloseButton: true },
45240
- * size: 'medium'
45241
- * });
46948
+ * - Lee el idioma desde localStorage al iniciar
46949
+ * - Proporciona método para cambiar idioma (recarga la página)
46950
+ * - Sin observables ni subscripciones
45242
46951
  *
45243
- * if (result.role === 'confirm') {
45244
- * console.log('Saved:', result.data);
45245
- * }
45246
- * ```
46952
+ * @example
46953
+ * // En un componente
46954
+ * locale = inject(LocaleService);
45247
46955
  *
45248
- * @example Simple content modal
45249
- * ```typescript
45250
- * await modalService.openSimple({
45251
- * title: 'Terms of Service',
45252
- * content: '<p>Terms content here...</p>',
45253
- * size: 'large',
45254
- * showCloseButton: true
45255
- * });
45256
- * ```
46956
+ * // Obtener idioma actual
46957
+ * console.log(this.locale.lang); // 'es' o 'en'
45257
46958
  *
45258
- * @example Sheet modal (mobile)
45259
- * ```typescript
45260
- * const result = await modalService.openSheet({
45261
- * component: FilterComponent,
45262
- * breakpoints: {
45263
- * initialBreakpoint: 0.5,
45264
- * breakpoints: [0, 0.5, 1],
45265
- * showHandle: true
45266
- * }
45267
- * });
45268
- * ```
46959
+ * // Cambiar idioma (recarga la página)
46960
+ * this.locale.setLang('en');
45269
46961
  */
45270
- class ModalService {
45271
- constructor(modalController) {
45272
- this.modalController = modalController;
46962
+ class LocaleService {
46963
+ constructor(platformId) {
46964
+ this.isBrowser = isPlatformBrowser(platformId);
46965
+ this.lang = this.getStoredLang();
45273
46966
  }
45274
46967
  /**
45275
- * Opens a modal with a component.
45276
- * @param options - Modal configuration
45277
- * @returns Promise resolving to the modal result
46968
+ * Cambia el idioma de la aplicación.
46969
+ * Guarda en localStorage y recarga la página para aplicar el cambio.
46970
+ *
46971
+ * @param lang Nuevo idioma ('es' o 'en')
45278
46972
  */
45279
- async open(options) {
45280
- // A sheet modal (breakpoints present) gets its height from the breakpoints
45281
- // and must span the full viewport width. Applying a size preset's
45282
- // `--width`/`--height` here collapses the sheet (e.g. `auto` → shrink to
45283
- // content) or fights the breakpoint height. So when it's a sheet we skip
45284
- // the size preset entirely.
45285
- const isSheet = !!options.breakpoints?.breakpoints?.length;
45286
- const size = isSheet ? undefined : MODAL_SIZES[options.size || 'medium'];
45287
- const cssClasses = this.buildCssClasses(options);
45288
- const modal = await this.modalController.create({
45289
- component: options.component,
45290
- componentProps: {
45291
- ...options.componentProps,
45292
- // Pass modal reference for dismiss from within component
45293
- _modalRef: {
45294
- dismiss: (data, role) => modal.dismiss(data, role),
45295
- },
45296
- },
45297
- id: options.id,
45298
- showBackdrop: options.showBackdrop ?? true,
45299
- backdropDismiss: options.backdropDismiss ?? true,
45300
- keyboardClose: options.keyboardClose ?? true,
45301
- cssClass: cssClasses,
45302
- mode: options.mode,
45303
- animated: options.animated ?? true,
45304
- canDismiss: this.normalizeCanDismiss(options.canDismiss ?? true),
45305
- presentingElement: options.presentingElement,
45306
- // Sheet modal options
45307
- initialBreakpoint: options.breakpoints?.initialBreakpoint,
45308
- breakpoints: options.breakpoints?.breakpoints,
45309
- handle: options.breakpoints?.showHandle,
45310
- handleBehavior: options.breakpoints?.handleBehavior,
45311
- });
45312
- // Apply custom size styles. Skipped for sheet modals — their dimensions
45313
- // come from the breakpoints (Ionic spans the sheet full-width and uses the
45314
- // breakpoint values for height).
45315
- if (!isSheet && (options.width || options.height || size)) {
45316
- const modalEl = modal;
45317
- modalEl.style.setProperty('--width', options.width || size?.width || 'auto');
45318
- modalEl.style.setProperty('--height', options.height || size?.height || '50vh');
46973
+ setLang(lang) {
46974
+ if (lang === this.lang || !this.isBrowser) {
46975
+ return;
45319
46976
  }
45320
- await modal.present();
45321
- const { data, role } = await modal.onDidDismiss();
45322
- return { data, role };
45323
- }
45324
- /**
45325
- * Opens a simple content modal without a custom component.
45326
- * Uses Ionic's alert-like styling but with modal capabilities.
45327
- * @param options - Simple modal configuration
45328
- * @returns Promise resolving to the modal result
45329
- */
45330
- async openSimple(options) {
45331
- return this.open({
45332
- component: SimpleModalContentComponent,
45333
- componentProps: {
45334
- title: options.title,
45335
- content: options.content,
45336
- buttons: options.buttons || [DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON],
45337
- showCloseButton: options.showCloseButton ?? true,
45338
- },
45339
- size: options.size || 'medium',
45340
- backdropDismiss: options.backdropDismiss ?? true,
45341
- cssClass: ['simple-modal', options.cssClass].filter(Boolean).join(' '),
45342
- });
45343
- }
45344
- /**
45345
- * Opens a confirmation modal.
45346
- * @param title - Modal title
45347
- * @param message - Modal message
45348
- * @param confirmButton - Confirm button config (optional)
45349
- * @param cancelButton - Cancel button config (optional)
45350
- * @returns Promise resolving to true if confirmed
45351
- */
45352
- async confirm(title, message, confirmButton, cancelButton) {
45353
- const result = await this.openSimple({
45354
- title,
45355
- content: message,
45356
- size: 'small',
45357
- showCloseButton: false,
45358
- backdropDismiss: false,
45359
- buttons: [
45360
- { ...DEFAULT_MODAL_CANCEL_BUTTON, ...cancelButton },
45361
- { ...DEFAULT_MODAL_CONFIRM_BUTTON, ...confirmButton },
45362
- ],
45363
- });
45364
- return result.role === 'confirm';
45365
- }
45366
- /**
45367
- * Opens a sheet modal (iOS-style bottom sheet).
45368
- * @param options - Modal configuration with sheet-specific options
45369
- * @returns Promise resolving to the modal result
45370
- */
45371
- async openSheet(options) {
45372
- return this.open({
45373
- ...options,
45374
- breakpoints: {
45375
- initialBreakpoint: options.initialBreakpoint ?? 0.5,
45376
- breakpoints: options.breakpoints ?? [0, 0.5, 1],
45377
- showHandle: options.showHandle ?? true,
45378
- },
45379
- cssClass: ['sheet-modal', options.cssClass].filter(Boolean).join(' '),
45380
- });
46977
+ localStorage.setItem(LANG_STORAGE_KEY, lang);
46978
+ window.location.reload();
45381
46979
  }
45382
46980
  /**
45383
- * Opens a fullscreen modal.
45384
- * @param options - Modal configuration
45385
- * @returns Promise resolving to the modal result
46981
+ * Obtiene el idioma almacenado en localStorage.
46982
+ * Si no existe o no es válido, retorna el idioma por defecto.
46983
+ * En SSR (sin localStorage) retorna el default.
45386
46984
  */
45387
- async openFullscreen(options) {
45388
- return this.open({
45389
- ...options,
45390
- size: 'full',
45391
- cssClass: ['fullscreen-modal', options.cssClass].filter(Boolean).join(' '),
45392
- });
46985
+ getStoredLang() {
46986
+ if (!this.isBrowser) {
46987
+ return DEFAULT_LANG;
46988
+ }
46989
+ const stored = localStorage.getItem(LANG_STORAGE_KEY);
46990
+ if (stored === 'es' || stored === 'en') {
46991
+ return stored;
46992
+ }
46993
+ return DEFAULT_LANG;
45393
46994
  }
45394
46995
  /**
45395
- * Dismisses the top-most modal.
45396
- * @param data - Optional data to return
45397
- * @param role - Optional role
46996
+ * Verifica si el idioma actual es español
45398
46997
  */
45399
- async dismiss(data, role) {
45400
- return this.modalController.dismiss(data, role);
46998
+ get isSpanish() {
46999
+ return this.lang === 'es';
45401
47000
  }
45402
47001
  /**
45403
- * Gets the top-most modal.
47002
+ * Verifica si el idioma actual es inglés
45404
47003
  */
45405
- async getTop() {
45406
- return this.modalController.getTop();
45407
- }
45408
- normalizeCanDismiss(canDismiss) {
45409
- if (typeof canDismiss === 'boolean') {
45410
- return canDismiss;
45411
- }
45412
- // Wrap the function to ensure it returns a Promise
45413
- return async (data, role) => {
45414
- const result = canDismiss();
45415
- return Promise.resolve(result);
45416
- };
45417
- }
45418
- buildCssClasses(options) {
45419
- const classes = ['val-modal'];
45420
- if (options.size) {
45421
- classes.push(`modal-${options.size}`);
45422
- }
45423
- if (options.header) {
45424
- classes.push('modal-with-header');
45425
- }
45426
- if (options.footer) {
45427
- classes.push('modal-with-footer');
45428
- }
45429
- if (options.cssClass) {
45430
- classes.push(options.cssClass);
45431
- }
45432
- return classes.join(' ');
47004
+ get isEnglish() {
47005
+ return this.lang === 'en';
45433
47006
  }
45434
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, deps: [{ token: i2$1.ModalController }], target: i0.ɵɵFactoryTarget.Injectable }); }
45435
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, providedIn: 'root' }); }
47007
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
47008
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, providedIn: 'root' }); }
45436
47009
  }
45437
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ModalService, decorators: [{
47010
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LocaleService, decorators: [{
45438
47011
  type: Injectable,
45439
- args: [{
45440
- providedIn: 'root',
45441
- }]
45442
- }], ctorParameters: () => [{ type: i2$1.ModalController }] });
47012
+ args: [{ providedIn: 'root' }]
47013
+ }], ctorParameters: () => [{ type: Object, decorators: [{
47014
+ type: Inject,
47015
+ args: [PLATFORM_ID]
47016
+ }] }] });
47017
+
47018
+ // Types for valtech-components services
47019
+ // ValtechConfig and LangProvider have been removed in v3.0.0
47020
+ // Use LocaleService for language management instead
45443
47021
 
45444
47022
  class MetaService {
45445
47023
  constructor() {
@@ -45738,177 +47316,6 @@ function beautifyLegalArticle(article) {
45738
47316
  };
45739
47317
  }
45740
47318
 
45741
- /**
45742
- * Standard cross-app site links (env-aware)
45743
- *
45744
- * En el factory MyValtech cada producto satélite (showcase, sigify, …) enlaza a
45745
- * `web` —la landing de marca— para soporte, contacto y contenido legal. `web`
45746
- * es el dueño canónico de esas rutas. Este módulo centraliza:
45747
- *
45748
- * 1. El mapa ambiente → base URL de `web` (`VALTECH_WEB_BASE_URLS`).
45749
- * 2. El catálogo de paths estándar que viven en `web` (`VALTECH_SITE_PATHS`).
45750
- *
45751
- * El ambiente se decide en BUILD TIME (campo `valtechEnv` en el `environment*.ts`
45752
- * de cada app), igual que `apiUrl`. Es el mecanismo más confiable: detectar por
45753
- * hostname en runtime se rompe con dominios custom / `web.app` / variantes de
45754
- * localhost.
45755
- *
45756
- * La resolución real (join baseUrl + path, abrir en tab nueva, `?lang=`) la hace
45757
- * `LegalLinkService`; este módulo solo le entrega el `baseUrl` correcto por env.
45758
- *
45759
- * @see provideValtechSite — wiring de una sola línea para apps satélite.
45760
- */
45761
- /**
45762
- * Base URL de `web` (landing de marca) por ambiente.
45763
- *
45764
- * - `local` → `ng serve` de `web` (puerto 8100).
45765
- * - `dev` → proyecto Firebase `myvaltech-dev`.
45766
- * - `prod` → dominio custom de producción.
45767
- *
45768
- * Sin trailing slash (lo asume `LegalLinkService`).
45769
- */
45770
- const VALTECH_WEB_BASE_URLS = {
45771
- local: 'http://localhost:8100',
45772
- dev: 'https://myvaltech-dev.web.app',
45773
- prod: 'https://myvaltech.com',
45774
- };
45775
- /**
45776
- * Resuelve la base URL de `web` para un ambiente.
45777
- *
45778
- * Fallback a `prod` cuando el env es desconocido o falta: ante la duda, apuntar
45779
- * a producción es lo seguro (nunca enlaza a un host local/dev inexistente).
45780
- */
45781
- function resolveWebBaseUrl(env) {
45782
- if (env && env in VALTECH_WEB_BASE_URLS) {
45783
- return VALTECH_WEB_BASE_URLS[env];
45784
- }
45785
- return VALTECH_WEB_BASE_URLS.prod;
45786
- }
45787
- /**
45788
- * Catálogo de paths estándar que viven en `web`. Las apps resuelven estos
45789
- * contra la base URL del ambiente vía `LegalLinkService.resolve(path)`.
45790
- *
45791
- * Mantener alineado con las rutas reales de `web/src/app/app.routes.ts` y con
45792
- * `VALTECH_COMPANY_LINKS` (footer).
45793
- */
45794
- const VALTECH_SITE_PATHS = {
45795
- home: '/',
45796
- support: '/contact',
45797
- contact: '/contact',
45798
- faq: '/faq',
45799
- about: '/about',
45800
- terms: '/legal/terms',
45801
- privacy: '/legal/privacy',
45802
- cookies: '/legal/cookies',
45803
- legalNotice: '/legal/legal-notice',
45804
- };
45805
-
45806
- const VALTECH_LEGAL_CONFIG = new InjectionToken('VALTECH_LEGAL_CONFIG');
45807
- /**
45808
- * Resolves legal/site paths against a configurable main-site base URL.
45809
- *
45810
- * - **Main site mode**: no `provideValtechLegal` called → `resolve('/legal/terms')` returns `/legal/terms` (relative).
45811
- * - **Satellite mode**: `provideValtechLegal({ baseUrl: 'https://myvaltech.com' })` → `resolve('/legal/terms')` returns `'https://myvaltech.com/legal/terms'` and `isExternal('/legal/terms')` returns `true`.
45812
- *
45813
- * Absolute URLs passed in are returned unchanged (no double-prefix).
45814
- */
45815
- class LegalLinkService {
45816
- constructor() {
45817
- this.config = inject(VALTECH_LEGAL_CONFIG, { optional: true }) ?? {};
45818
- }
45819
- /** Effective base URL (null if running as main site). */
45820
- get baseUrl() {
45821
- return this.config.baseUrl?.replace(/\/$/, '') ?? null;
45822
- }
45823
- /** Whether resolved external links should open in a new tab. */
45824
- get openInNewTab() {
45825
- return this.config.openInNewTab ?? Boolean(this.config.baseUrl);
45826
- }
45827
- /**
45828
- * Returns the URL to use for a given internal path. Absolute URLs pass through.
45829
- *
45830
- * When `locale` is provided AND the resolved URL would be cross-origin, appends
45831
- * `?lang={locale}` so the main site can render the legal doc in the user's
45832
- * active language (instead of its default). Locale is omitted for same-origin
45833
- * resolves since the main site reads locale from its own `LocaleService`.
45834
- *
45835
- * @example
45836
- * resolve('/legal/terms')
45837
- * // main site: '/legal/terms'
45838
- * // satellite: 'https://myvaltech.com/legal/terms'
45839
- *
45840
- * resolve('/legal/terms', { locale: 'pt' })
45841
- * // main site: '/legal/terms' (locale stays implicit)
45842
- * // satellite: 'https://myvaltech.com/legal/terms?lang=pt'
45843
- *
45844
- * resolve('https://x.com/y') // unchanged
45845
- */
45846
- resolve(path, options) {
45847
- if (!path)
45848
- return path;
45849
- if (/^https?:\/\//i.test(path))
45850
- return path;
45851
- const base = this.baseUrl;
45852
- if (!base)
45853
- return path;
45854
- const url = `${base}${path.startsWith('/') ? '' : '/'}${path}`;
45855
- if (options?.locale) {
45856
- const sep = url.includes('?') ? '&' : '?';
45857
- return `${url}${sep}lang=${encodeURIComponent(options.locale)}`;
45858
- }
45859
- return url;
45860
- }
45861
- /** `true` if the path would be resolved to a cross-origin URL. */
45862
- isExternal(path) {
45863
- if (/^https?:\/\//i.test(path))
45864
- return true;
45865
- return this.baseUrl !== null;
45866
- }
45867
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
45868
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, providedIn: 'root' }); }
45869
- }
45870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LegalLinkService, decorators: [{
45871
- type: Injectable,
45872
- args: [{ providedIn: 'root' }]
45873
- }] });
45874
- /**
45875
- * Wires `LegalLinkService` for satellite apps. Omit in the main site (the one
45876
- * that hosts the canonical /legal/* routes) so links stay relative.
45877
- *
45878
- * @example
45879
- * // main.ts of showcase / sigify / etc.
45880
- * provideValtechLegal({
45881
- * baseUrl: 'https://myvaltech.com',
45882
- * openInNewTab: true,
45883
- * }),
45884
- */
45885
- function provideValtechLegal(config) {
45886
- return { provide: VALTECH_LEGAL_CONFIG, useValue: config };
45887
- }
45888
- /**
45889
- * Wiring de una sola línea para que una app satélite apunte sus links de sitio
45890
- * (legal, soporte, contacto, …) a `web` —la landing de marca— en el ambiente
45891
- * correcto. Resuelve el `baseUrl` desde `VALTECH_WEB_BASE_URLS` según el `env`
45892
- * baked en build-time (`environment.valtechEnv`) y delega en `provideValtechLegal`.
45893
- *
45894
- * Ambiente desconocido/ausente → fallback a `prod` (ver `resolveWebBaseUrl`).
45895
- * El main site (`web`) NO llama esto — sus links quedan relativos.
45896
- *
45897
- * @example
45898
- * // main.ts de showcase / sigify / …
45899
- * import { environment } from './environments/environment';
45900
- * provideValtechSite(environment.valtechEnv), // 'local' | 'dev' | 'prod'
45901
- *
45902
- * @see resolveWebBaseUrl
45903
- * @see VALTECH_SITE_PATHS — catálogo de paths estándar que viven en `web`.
45904
- */
45905
- function provideValtechSite(env, options) {
45906
- return provideValtechLegal({
45907
- baseUrl: resolveWebBaseUrl(env),
45908
- openInNewTab: options?.openInNewTab ?? true,
45909
- });
45910
- }
45911
-
45912
47319
  class OrgService {
45913
47320
  constructor(config, http) {
45914
47321
  this.config = config;
@@ -54293,5 +55700,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
54293
55700
  * Generated bundle index. Do not edit.
54294
55701
  */
54295
55702
 
54296
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CtaCardComponent, 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, DEFAULT_SPLASH_SCREEN_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, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, 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, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, 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, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, 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, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, 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_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_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, 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, provideSplashScreen, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
55703
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CONTENT_CARD_DEFAULTS, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentCardComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CountdownComponent, CtaCardComponent, 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, DEFAULT_SPLASH_SCREEN_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, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, 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, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INFO_CARD_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoCardComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, 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, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, 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, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, SplashScreenService, StatsBarComponent, 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_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_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, 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, provideSplashScreen, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
54297
55704
  //# sourceMappingURL=valtech-components.mjs.map