valtech-components 2.0.992 → 2.0.994
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.
- package/esm2022/lib/components/molecules/comment/comment.component.mjs +3 -3
- package/esm2022/lib/components/organisms/notifications-view/notifications-view.component.mjs +736 -0
- package/esm2022/lib/components/organisms/notifications-view/notifications-view.i18n.mjs +76 -0
- package/esm2022/lib/components/organisms/notifications-view/notifications.routes.mjs +32 -0
- package/esm2022/lib/components/organisms/notifications-view/types.mjs +2 -0
- package/esm2022/lib/components/organisms/profile-view/profile-view.component.mjs +16 -6
- package/esm2022/lib/services/auth/auth.service.mjs +14 -1
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/preferences/preferences.service.mjs +1 -10
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +4 -1
- package/fesm2022/valtech-components.mjs +1012 -180
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/notifications-view/notifications-view.component.d.ts +168 -0
- package/lib/components/organisms/notifications-view/notifications-view.i18n.d.ts +9 -0
- package/lib/components/organisms/notifications-view/notifications.routes.d.ts +27 -0
- package/lib/components/organisms/notifications-view/types.d.ts +52 -0
- package/lib/services/auth/auth.service.d.ts +7 -1
- package/lib/services/auth/types.d.ts +23 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +3 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
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
|
-
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,
|
|
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, race, timer, shareReplay, forkJoin } from 'rxjs';
|
|
4
4
|
import { catchError, switchMap, finalize, filter, take, map as map$1, tap, distinctUntilChanged as distinctUntilChanged$1, retry, timeout, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
5
5
|
import * as i1$3 from '@angular/common/http';
|
|
6
6
|
import { provideHttpClient, withInterceptors, HttpClient, HttpErrorResponse } 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, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonRippleEffect, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, IonDatetimeButton, 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, laptopOutline, swapHorizontalOutline, 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, swapHorizontalOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, notificationsOffOutline, 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.
|
|
57
|
+
const VERSION = '2.0.994';
|
|
58
58
|
|
|
59
59
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
60
60
|
let isRefreshing = false;
|
|
@@ -8658,6 +8658,19 @@ class AuthService {
|
|
|
8658
8658
|
.post(`${this.baseUrl}/reset-password`, request)
|
|
8659
8659
|
.pipe(catchError(error => this.handleAuthError(error)));
|
|
8660
8660
|
}
|
|
8661
|
+
/**
|
|
8662
|
+
* Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
|
|
8663
|
+
* la contraseña con el token del enlace de email. En éxito el backend devuelve
|
|
8664
|
+
* tokens de auto-signin → el usuario queda logueado (mismo flujo que verify-email).
|
|
8665
|
+
*/
|
|
8666
|
+
activateAccount(request) {
|
|
8667
|
+
this.stateService.clearError();
|
|
8668
|
+
return this.http.post(`${this.baseUrl}/activate`, request).pipe(tap(response => {
|
|
8669
|
+
if (response.activated && response.accessToken) {
|
|
8670
|
+
this.handleSuccessfulAuth(response);
|
|
8671
|
+
}
|
|
8672
|
+
}), catchError(error => this.handleAuthError(error)));
|
|
8673
|
+
}
|
|
8661
8674
|
/**
|
|
8662
8675
|
* Cambia la contraseña del usuario autenticado.
|
|
8663
8676
|
* Requiere la contraseña actual para verificación.
|
|
@@ -21657,7 +21670,7 @@ class CommentComponent {
|
|
|
21657
21670
|
</div>
|
|
21658
21671
|
}
|
|
21659
21672
|
</div>
|
|
21660
|
-
`, isInline: true, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-padding: 16px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #9e9e9e;--ion-color-medium-rgb: 158, 158, 158;--ion-color-medium-contrast: #000000;--ion-color-medium-contrast-rgb: 0, 0, 0;--ion-color-medium-shade: #8b8b8b;--ion-color-medium-tint: #a8a8a8;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-medium)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}:host{display:block}.comment{--indent-size: 24px;--max-indent: 72px;padding:12px 0}.comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.08);border-radius:8px;padding:12px;margin:-4px}.comment.pinned{border-left:3px solid var(--ion-color-primary);padding-left:12px}.comment.collapsed{opacity:.7}.pinned-indicator{display:flex;align-items:center;gap:4px;font-size:.75rem;color:var(--ion-color-primary);margin-bottom:8px;padding-left:44px}.pinned-indicator ion-icon{font-size:.875rem}.comment-main{display:flex;gap:12px}.comment-avatar{flex-shrink:0;cursor:pointer}.comment-avatar val-avatar{--size: 36px}.comment-avatar .avatar-placeholder{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--ion-color-primary),var(--ion-color-secondary));display:flex;align-items:center;justify-content:center;color:#fff;font-size:.875rem;font-weight:600}.comment-body{flex:1;min-width:0}.comment-header{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:4px}.author-info{display:flex;align-items:center;gap:4px;cursor:pointer}.author-info:hover .author-name{text-decoration:underline}.author-name{font-weight:600;font-size:.875rem;color:var(--ion-text-color)}.verified-badge{color:var(--ion-color-primary);font-size:1rem}.author-username{font-size:.8125rem;color:var(--ion-color-medium)}.comment-meta{display:flex;align-items:center;gap:6px;margin-left:auto}.timestamp{font-size:.75rem;color:var(--ion-color-medium)}.edited-label{font-size:.6875rem;color:var(--ion-color-medium);font-style:italic}.more-button{--padding-start: 4px;--padding-end: 4px;margin:0;height:28px}.more-button ion-icon{font-size:1.125rem;color:var(--ion-color-medium)}.menu-divider{height:1px;background-color:var(--ion-color-light);margin:4px 0}.comment-content{font-size:.875rem;line-height:1.5;color:var(--ion-text-color);word-wrap:break-word;overflow-wrap:break-word}.comment-content :global(.comment-link){color:var(--ion-color-primary);text-decoration:none}.comment-content :global(.comment-link):hover{text-decoration:underline}.comment-content :global(.comment-mention){color:var(--ion-color-primary);font-weight:500;cursor:pointer}.comment-content :global(.comment-mention):hover{text-decoration:underline}.comment-content :global(.comment-hashtag){color:var(--ion-color-primary);cursor:pointer}.comment-content :global(.comment-hashtag):hover{text-decoration:underline}.comment-reactions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.reaction-button{display:flex;align-items:center;gap:4px;background:none;border:none;padding:4px 8px;border-radius:16px;cursor:pointer;font-size:.8125rem;color:var(--reaction-color, var(--ion-color-medium));transition:all .2s ease;background-color:transparent}.reaction-button:hover{background-color:rgba(var(--ion-color-medium-rgb),.1)}.reaction-button.active{background-color:rgba(var(--ion-color-primary-rgb),.1)}.reaction-button.active ion-icon{transform:scale(1.1)}.reaction-button ion-icon{font-size:1.125rem;transition:transform .2s ease}.reaction-button .reaction-count{font-weight:500}.comment-actions{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;margin-left:-8px}.comment-actions ion-button{--padding-start: 8px;--padding-end: 8px;font-size:.8125rem;height:32px;text-transform:none;font-weight:500}.expand-button,.collapse-button,.load-more-button,.view-replies-button{display:flex;align-items:center;gap:6px;background:none;border:none;padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);border-radius:8px;transition:background-color .2s}.expand-button:hover,.collapse-button:hover,.load-more-button:hover,.view-replies-button:hover{background-color:rgba(var(--ion-color-primary-rgb),.1)}.expand-button ion-icon,.collapse-button ion-icon,.load-more-button ion-icon,.view-replies-button ion-icon{font-size:1rem}.expand-button{margin-top:4px;color:var(--ion-color-medium)}.comment-children{margin-top:12px;padding-left:min(var(--indent-size),var(--max-indent));border-left:2px solid var(--ion-color-light-shade)}.comment-children val-comment{display:block}.comment-children val-comment:first-of-type .comment{padding-top:0}.load-more-button,.view-replies-button{margin-top:8px;margin-left:0}ion-popover{--width: 200px}ion-popover ion-list{padding:4px 0}ion-popover ion-item{--min-height: 40px;--padding-start: 12px;font-size:.875rem}ion-popover ion-item ion-icon{font-size:1.125rem;margin-right:8px}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 2px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 2px);height:max(28px,36px - 2px);font-size:max(.6875rem,.8125rem)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 4px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 4px);height:max(28px,36px - 4px);font-size:max(.6875rem,.75rem)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 6px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 6px);height:max(28px,36px - 6px);font-size:max(.6875rem,.6875rem)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 8px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 8px);height:max(28px,36px - 8px);font-size:max(.6875rem,.625rem)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 10px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 10px);height:max(28px,36px - 10px);font-size:max(.6875rem,.5625rem)}:host-context(.dark) .comment.highlighted,:host-context(.ion-palette-dark) .comment.highlighted,:host-context(html.ion-palette-dark) .comment.highlighted,:host-context(body.dark) .comment.highlighted,:host-context([data-theme=dark]) .comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.12)}:host-context(.dark) .avatar-placeholder,:host-context(.ion-palette-dark) .avatar-placeholder,:host-context(html.ion-palette-dark) .avatar-placeholder,:host-context(body.dark) .avatar-placeholder,:host-context([data-theme=dark]) .avatar-placeholder{background:linear-gradient(135deg,var(--ion-color-primary-shade),var(--ion-color-secondary-shade))}\n"], dependencies: [{ kind: "component", type: CommentComponent, selector: "val-comment", inputs: ["props"], outputs: ["authorClick", "reactionClick", "actionClick", "menuItemClick", "loadMoreClick", "collapseToggle"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonPopover, selector: "ion-popover" }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: AvatarComponent, selector: "val-avatar", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
|
|
21673
|
+
`, isInline: true, styles: [":host{display:block}.comment{--indent-size: 24px;--max-indent: 72px;padding:12px 0}.comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.08);border-radius:8px;padding:12px;margin:-4px}.comment.pinned{border-left:3px solid var(--ion-color-primary);padding-left:12px}.comment.collapsed{opacity:.7}.pinned-indicator{display:flex;align-items:center;gap:4px;font-size:.75rem;color:var(--ion-color-primary);margin-bottom:8px;padding-left:44px}.pinned-indicator ion-icon{font-size:.875rem}.comment-main{display:flex;gap:12px}.comment-avatar{flex-shrink:0;cursor:pointer}.comment-avatar val-avatar{--size: 36px}.comment-avatar .avatar-placeholder{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--ion-color-primary),var(--ion-color-secondary));display:flex;align-items:center;justify-content:center;color:#fff;font-size:.875rem;font-weight:600}.comment-body{flex:1;min-width:0}.comment-header{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:4px}.author-info{display:flex;align-items:center;gap:4px;cursor:pointer}.author-info:hover .author-name{text-decoration:underline}.author-name{font-weight:600;font-size:.875rem;color:var(--ion-text-color)}.verified-badge{color:var(--ion-color-primary);font-size:1rem}.author-username{font-size:.8125rem;color:var(--ion-color-medium)}.comment-meta{display:flex;align-items:center;gap:6px;margin-left:auto}.timestamp{font-size:.75rem;color:var(--ion-color-medium)}.edited-label{font-size:.6875rem;color:var(--ion-color-medium);font-style:italic}.more-button{--padding-start: 4px;--padding-end: 4px;margin:0;height:28px}.more-button ion-icon{font-size:1.125rem;color:var(--ion-color-medium)}.menu-divider{height:1px;background-color:var(--ion-color-light);margin:4px 0}.comment-content{font-size:.875rem;line-height:1.5;color:var(--ion-text-color);word-wrap:break-word;overflow-wrap:break-word}.comment-content :global(.comment-link){color:var(--ion-color-primary);text-decoration:none}.comment-content :global(.comment-link):hover{text-decoration:underline}.comment-content :global(.comment-mention){color:var(--ion-color-primary);font-weight:500;cursor:pointer}.comment-content :global(.comment-mention):hover{text-decoration:underline}.comment-content :global(.comment-hashtag){color:var(--ion-color-primary);cursor:pointer}.comment-content :global(.comment-hashtag):hover{text-decoration:underline}.comment-reactions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.reaction-button{display:flex;align-items:center;gap:4px;background:none;border:none;padding:4px 8px;border-radius:16px;cursor:pointer;font-size:.8125rem;color:var(--reaction-color, var(--ion-color-medium));transition:all .2s ease;background-color:transparent}.reaction-button:hover{background-color:rgba(var(--ion-color-medium-rgb),.1)}.reaction-button.active{background-color:rgba(var(--ion-color-primary-rgb),.1)}.reaction-button.active ion-icon{transform:scale(1.1)}.reaction-button ion-icon{font-size:1.125rem;transition:transform .2s ease}.reaction-button .reaction-count{font-weight:500}.comment-actions{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;margin-left:-8px}.comment-actions ion-button{--padding-start: 8px;--padding-end: 8px;font-size:.8125rem;height:32px;text-transform:none;font-weight:500}.expand-button,.collapse-button,.load-more-button,.view-replies-button{display:flex;align-items:center;gap:6px;background:none;border:none;padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);border-radius:8px;transition:background-color .2s}.expand-button:hover,.collapse-button:hover,.load-more-button:hover,.view-replies-button:hover{background-color:rgba(var(--ion-color-primary-rgb),.1)}.expand-button ion-icon,.collapse-button ion-icon,.load-more-button ion-icon,.view-replies-button ion-icon{font-size:1rem}.expand-button{margin-top:4px;color:var(--ion-color-medium)}.comment-children{margin-top:12px;padding-left:min(var(--indent-size),var(--max-indent));border-left:2px solid var(--ion-color-light-shade)}.comment-children val-comment{display:block}.comment-children val-comment:first-of-type .comment{padding-top:0}.load-more-button,.view-replies-button{margin-top:8px;margin-left:0}ion-popover{--width: 200px}ion-popover ion-list{padding:4px 0}ion-popover ion-item{--min-height: 40px;--padding-start: 12px;font-size:.875rem}ion-popover ion-item ion-icon{font-size:1.125rem;margin-right:8px}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 2px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 2px);height:max(28px,36px - 2px);font-size:max(11px,14px - 1px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 4px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 4px);height:max(28px,36px - 4px);font-size:max(11px,14px - 2px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 6px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 6px);height:max(28px,36px - 6px);font-size:max(11px,14px - 3px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 8px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 8px);height:max(28px,36px - 8px);font-size:max(11px,14px - 4px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 10px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 10px);height:max(28px,36px - 10px);font-size:max(11px,14px - 5px)}:host-context(.dark) .comment.highlighted,:host-context(.ion-palette-dark) .comment.highlighted,:host-context(html.ion-palette-dark) .comment.highlighted,:host-context(body.dark) .comment.highlighted,:host-context([data-theme=dark]) .comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.12)}:host-context(.dark) .avatar-placeholder,:host-context(.ion-palette-dark) .avatar-placeholder,:host-context(html.ion-palette-dark) .avatar-placeholder,:host-context(body.dark) .avatar-placeholder,:host-context([data-theme=dark]) .avatar-placeholder{background:linear-gradient(135deg,var(--ion-color-primary-shade),var(--ion-color-secondary-shade))}\n"], dependencies: [{ kind: "component", type: CommentComponent, selector: "val-comment", inputs: ["props"], outputs: ["authorClick", "reactionClick", "actionClick", "menuItemClick", "loadMoreClick", "collapseToggle"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: 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: IonPopover, selector: "ion-popover" }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: AvatarComponent, selector: "val-avatar", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
|
|
21661
21674
|
}
|
|
21662
21675
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CommentComponent, decorators: [{
|
|
21663
21676
|
type: Component,
|
|
@@ -21847,7 +21860,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
21847
21860
|
</div>
|
|
21848
21861
|
}
|
|
21849
21862
|
</div>
|
|
21850
|
-
`, styles: ["
|
|
21863
|
+
`, styles: [":host{display:block}.comment{--indent-size: 24px;--max-indent: 72px;padding:12px 0}.comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.08);border-radius:8px;padding:12px;margin:-4px}.comment.pinned{border-left:3px solid var(--ion-color-primary);padding-left:12px}.comment.collapsed{opacity:.7}.pinned-indicator{display:flex;align-items:center;gap:4px;font-size:.75rem;color:var(--ion-color-primary);margin-bottom:8px;padding-left:44px}.pinned-indicator ion-icon{font-size:.875rem}.comment-main{display:flex;gap:12px}.comment-avatar{flex-shrink:0;cursor:pointer}.comment-avatar val-avatar{--size: 36px}.comment-avatar .avatar-placeholder{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--ion-color-primary),var(--ion-color-secondary));display:flex;align-items:center;justify-content:center;color:#fff;font-size:.875rem;font-weight:600}.comment-body{flex:1;min-width:0}.comment-header{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:4px}.author-info{display:flex;align-items:center;gap:4px;cursor:pointer}.author-info:hover .author-name{text-decoration:underline}.author-name{font-weight:600;font-size:.875rem;color:var(--ion-text-color)}.verified-badge{color:var(--ion-color-primary);font-size:1rem}.author-username{font-size:.8125rem;color:var(--ion-color-medium)}.comment-meta{display:flex;align-items:center;gap:6px;margin-left:auto}.timestamp{font-size:.75rem;color:var(--ion-color-medium)}.edited-label{font-size:.6875rem;color:var(--ion-color-medium);font-style:italic}.more-button{--padding-start: 4px;--padding-end: 4px;margin:0;height:28px}.more-button ion-icon{font-size:1.125rem;color:var(--ion-color-medium)}.menu-divider{height:1px;background-color:var(--ion-color-light);margin:4px 0}.comment-content{font-size:.875rem;line-height:1.5;color:var(--ion-text-color);word-wrap:break-word;overflow-wrap:break-word}.comment-content :global(.comment-link){color:var(--ion-color-primary);text-decoration:none}.comment-content :global(.comment-link):hover{text-decoration:underline}.comment-content :global(.comment-mention){color:var(--ion-color-primary);font-weight:500;cursor:pointer}.comment-content :global(.comment-mention):hover{text-decoration:underline}.comment-content :global(.comment-hashtag){color:var(--ion-color-primary);cursor:pointer}.comment-content :global(.comment-hashtag):hover{text-decoration:underline}.comment-reactions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.reaction-button{display:flex;align-items:center;gap:4px;background:none;border:none;padding:4px 8px;border-radius:16px;cursor:pointer;font-size:.8125rem;color:var(--reaction-color, var(--ion-color-medium));transition:all .2s ease;background-color:transparent}.reaction-button:hover{background-color:rgba(var(--ion-color-medium-rgb),.1)}.reaction-button.active{background-color:rgba(var(--ion-color-primary-rgb),.1)}.reaction-button.active ion-icon{transform:scale(1.1)}.reaction-button ion-icon{font-size:1.125rem;transition:transform .2s ease}.reaction-button .reaction-count{font-weight:500}.comment-actions{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;margin-left:-8px}.comment-actions ion-button{--padding-start: 8px;--padding-end: 8px;font-size:.8125rem;height:32px;text-transform:none;font-weight:500}.expand-button,.collapse-button,.load-more-button,.view-replies-button{display:flex;align-items:center;gap:6px;background:none;border:none;padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--ion-color-primary);border-radius:8px;transition:background-color .2s}.expand-button:hover,.collapse-button:hover,.load-more-button:hover,.view-replies-button:hover{background-color:rgba(var(--ion-color-primary-rgb),.1)}.expand-button ion-icon,.collapse-button ion-icon,.load-more-button ion-icon,.view-replies-button ion-icon{font-size:1rem}.expand-button{margin-top:4px;color:var(--ion-color-medium)}.comment-children{margin-top:12px;padding-left:min(var(--indent-size),var(--max-indent));border-left:2px solid var(--ion-color-light-shade)}.comment-children val-comment{display:block}.comment-children val-comment:first-of-type .comment{padding-top:0}.load-more-button,.view-replies-button{margin-top:8px;margin-left:0}ion-popover{--width: 200px}ion-popover ion-list{padding:4px 0}ion-popover ion-item{--min-height: 40px;--padding-start: 12px;font-size:.875rem}ion-popover ion-item ion-icon{font-size:1.125rem;margin-right:8px}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 2px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 2px);height:max(28px,36px - 2px);font-size:max(11px,14px - 1px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 4px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 4px);height:max(28px,36px - 4px);font-size:max(11px,14px - 2px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 6px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 6px);height:max(28px,36px - 6px);font-size:max(11px,14px - 3px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 8px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 8px);height:max(28px,36px - 8px);font-size:max(11px,14px - 4px)}:host-context(val-comment) .comment-avatar val-avatar{--size: max(28px, 36px - 10px)}:host-context(val-comment) .comment-avatar .avatar-placeholder{width:max(28px,36px - 10px);height:max(28px,36px - 10px);font-size:max(11px,14px - 5px)}:host-context(.dark) .comment.highlighted,:host-context(.ion-palette-dark) .comment.highlighted,:host-context(html.ion-palette-dark) .comment.highlighted,:host-context(body.dark) .comment.highlighted,:host-context([data-theme=dark]) .comment.highlighted{background-color:rgba(var(--ion-color-primary-rgb),.12)}:host-context(.dark) .avatar-placeholder,:host-context(.ion-palette-dark) .avatar-placeholder,:host-context(html.ion-palette-dark) .avatar-placeholder,:host-context(body.dark) .avatar-placeholder,:host-context([data-theme=dark]) .avatar-placeholder{background:linear-gradient(135deg,var(--ion-color-primary-shade),var(--ion-color-secondary-shade))}\n"] }]
|
|
21851
21864
|
}], propDecorators: { props: [{
|
|
21852
21865
|
type: Input
|
|
21853
21866
|
}], authorClick: [{
|
|
@@ -41737,7 +41750,7 @@ const PROFILE_VIEW_I18N = {
|
|
|
41737
41750
|
const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
|
|
41738
41751
|
const HANDLE_MIN = 3;
|
|
41739
41752
|
const HANDLE_MAX = 20;
|
|
41740
|
-
const DEFAULT_NAMESPACE$
|
|
41753
|
+
const DEFAULT_NAMESPACE$d = 'Settings.Profile';
|
|
41741
41754
|
/**
|
|
41742
41755
|
* `val-profile-view` — vista Perfil full-feature autocontenida (organism).
|
|
41743
41756
|
*
|
|
@@ -41787,7 +41800,7 @@ class ProfileViewComponent {
|
|
|
41787
41800
|
showPhone: merged.showPhone ?? true,
|
|
41788
41801
|
showShareCta: merged.showShareCta ?? true,
|
|
41789
41802
|
profileBaseUrl: merged.profileBaseUrl ?? '',
|
|
41790
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
41803
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
|
|
41791
41804
|
onSaved: merged.onSaved,
|
|
41792
41805
|
onAvatarUploaded: merged.onAvatarUploaded,
|
|
41793
41806
|
};
|
|
@@ -42263,12 +42276,17 @@ class ProfileViewComponent {
|
|
|
42263
42276
|
|
|
42264
42277
|
<val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
|
|
42265
42278
|
|
|
42266
|
-
|
|
42267
|
-
|
|
42279
|
+
@if (resolvedConfig().showShareCta && p.handle) {
|
|
42280
|
+
<section class="settings-section share-cta-section">
|
|
42281
|
+
<val-cta-card [props]="shareCta()" (onAction)="shareOpen.set(true)" />
|
|
42282
|
+
</section>
|
|
42283
|
+
}
|
|
42268
42284
|
}
|
|
42269
42285
|
}
|
|
42270
42286
|
</div>
|
|
42271
|
-
|
|
42287
|
+
|
|
42288
|
+
<val-share-profile-modal [isOpen]="shareOpen()" [props]="shareModal()" (dismissed)="shareOpen.set(false)" />
|
|
42289
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.page-header h1{font-size:22px;font-weight:700;margin:0 0 4px}.hint{font-size:13px;opacity:.7;margin:0 0 12px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.avatar-section{display:flex;align-items:center;gap:16px}.avatar-meta{display:flex;flex-direction:column;gap:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: AvatarUploadComponent, selector: "val-avatar-upload", inputs: ["props"], outputs: ["uploaded", "error", "uploadStart"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: CtaCardComponent, selector: "val-cta-card", inputs: ["props"], outputs: ["onAction"] }, { kind: "component", type: ShareProfileModalComponent, selector: "val-share-profile-modal", inputs: ["isOpen", "props"], outputs: ["dismissed", "shared"] }] }); }
|
|
42272
42290
|
}
|
|
42273
42291
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileViewComponent, decorators: [{
|
|
42274
42292
|
type: Component,
|
|
@@ -42344,11 +42362,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
42344
42362
|
|
|
42345
42363
|
<val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
|
|
42346
42364
|
|
|
42347
|
-
|
|
42348
|
-
|
|
42365
|
+
@if (resolvedConfig().showShareCta && p.handle) {
|
|
42366
|
+
<section class="settings-section share-cta-section">
|
|
42367
|
+
<val-cta-card [props]="shareCta()" (onAction)="shareOpen.set(true)" />
|
|
42368
|
+
</section>
|
|
42369
|
+
}
|
|
42349
42370
|
}
|
|
42350
42371
|
}
|
|
42351
42372
|
</div>
|
|
42373
|
+
|
|
42374
|
+
<val-share-profile-modal [isOpen]="shareOpen()" [props]="shareModal()" (dismissed)="shareOpen.set(false)" />
|
|
42352
42375
|
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.page-header h1{font-size:22px;font-weight:700;margin:0 0 4px}.hint{font-size:13px;opacity:.7;margin:0 0 12px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.avatar-section{display:flex;align-items:center;gap:16px}.avatar-meta{display:flex;flex-direction:column;gap:2px}\n"] }]
|
|
42353
42376
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
42354
42377
|
type: Input
|
|
@@ -42479,15 +42502,6 @@ class PreferencesService {
|
|
|
42479
42502
|
this._updateInFlight = false;
|
|
42480
42503
|
const destroyRef = inject(DestroyRef);
|
|
42481
42504
|
destroyRef.onDestroy(() => this.unbind());
|
|
42482
|
-
// Seed `_fontSize` con el valor que FontSizeService ya restauró desde
|
|
42483
|
-
// localStorage en su constructor (providedIn root → ya corrió al inyectarlo
|
|
42484
|
-
// acá). Sin esto, el signal arranca en 'medium' y el effect de abajo —que
|
|
42485
|
-
// dispara al marcarse `synced`— forzaría 'medium' pisando el tamaño
|
|
42486
|
-
// restaurado cuando el snapshot Firestore NO trae `fontSize` (user sin ese
|
|
42487
|
-
// campo en el mirror). Bug: cambias el tamaño, refrescas y se pierde.
|
|
42488
|
-
if (this.fontSizeService) {
|
|
42489
|
-
this._fontSize.set(this.fontSizeService.size());
|
|
42490
|
-
}
|
|
42491
42505
|
// Auto-bind al user actual. effect() corre en injection context (constructor OK).
|
|
42492
42506
|
// allowSignalWrites: bindToUser/unbind escriben `_synced` (señal interna sin
|
|
42493
42507
|
// dependencias del propio effect) → seguro y necesario.
|
|
@@ -42702,7 +42716,7 @@ const PREFERENCES_VIEW_I18N = {
|
|
|
42702
42716
|
},
|
|
42703
42717
|
};
|
|
42704
42718
|
|
|
42705
|
-
const DEFAULT_NAMESPACE$
|
|
42719
|
+
const DEFAULT_NAMESPACE$c = 'Settings.General';
|
|
42706
42720
|
const DEFAULT_LANGUAGES = ['es', 'en'];
|
|
42707
42721
|
/**
|
|
42708
42722
|
* `val-preferences-view` — vista de preferencias de UI cross-app (tema +
|
|
@@ -42739,7 +42753,7 @@ class PreferencesViewComponent {
|
|
|
42739
42753
|
showLanguage: merged.showLanguage ?? true,
|
|
42740
42754
|
showFontSize: merged.showFontSize ?? true,
|
|
42741
42755
|
supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
|
|
42742
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
42756
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$c,
|
|
42743
42757
|
onThemeChanged: merged.onThemeChanged,
|
|
42744
42758
|
onLanguageChanged: merged.onLanguageChanged,
|
|
42745
42759
|
onFontSizeChanged: merged.onFontSizeChanged,
|
|
@@ -44551,7 +44565,7 @@ const SECURITY_VIEW_I18N = {
|
|
|
44551
44565
|
};
|
|
44552
44566
|
|
|
44553
44567
|
addIcons({ laptopOutline, phonePortraitOutline });
|
|
44554
|
-
const DEFAULT_NAMESPACE$
|
|
44568
|
+
const DEFAULT_NAMESPACE$b = 'Settings.Security';
|
|
44555
44569
|
/**
|
|
44556
44570
|
* `val-security-view` — vista Seguridad full-feature autocontenida (organism).
|
|
44557
44571
|
*
|
|
@@ -44600,7 +44614,7 @@ class SecurityViewComponent {
|
|
|
44600
44614
|
showChangeEmail: merged.showChangeEmail ?? true,
|
|
44601
44615
|
showMfa: merged.showMfa ?? true,
|
|
44602
44616
|
showPolicies: merged.showPolicies ?? true,
|
|
44603
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
44617
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$b,
|
|
44604
44618
|
handleDeepLinks: merged.handleDeepLinks ?? true,
|
|
44605
44619
|
homeRoute: merged.homeRoute ?? '/app/home',
|
|
44606
44620
|
onMfaCompleted: merged.onMfaCompleted,
|
|
@@ -45506,7 +45520,7 @@ const CREATE_ORG_MODAL_I18N = {
|
|
|
45506
45520
|
},
|
|
45507
45521
|
};
|
|
45508
45522
|
|
|
45509
|
-
const DEFAULT_NAMESPACE$
|
|
45523
|
+
const DEFAULT_NAMESPACE$a = 'CreateOrgModal';
|
|
45510
45524
|
/**
|
|
45511
45525
|
* `val-create-org-modal` — modal de creación de organización (organism
|
|
45512
45526
|
* compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
|
|
@@ -45532,10 +45546,10 @@ class CreateOrgModalComponent {
|
|
|
45532
45546
|
this.toast = inject(ToastService);
|
|
45533
45547
|
this.errors = inject(ValtechErrorService);
|
|
45534
45548
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
45535
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
45549
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$a;
|
|
45536
45550
|
this._busy = signal(false);
|
|
45537
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
45538
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
45551
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
|
|
45552
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$a, CREATE_ORG_MODAL_I18N);
|
|
45539
45553
|
}
|
|
45540
45554
|
this.formMeta = signal(this.buildFormMeta());
|
|
45541
45555
|
effect(() => {
|
|
@@ -45771,7 +45785,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
|
|
|
45771
45785
|
},
|
|
45772
45786
|
};
|
|
45773
45787
|
|
|
45774
|
-
const DEFAULT_NAMESPACE$
|
|
45788
|
+
const DEFAULT_NAMESPACE$9 = 'Settings.DeleteAccount';
|
|
45775
45789
|
/**
|
|
45776
45790
|
* `val-delete-account-modal` — modal de eliminación de cuenta (organism,
|
|
45777
45791
|
* cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
|
|
@@ -45797,7 +45811,7 @@ class DeleteAccountModalComponent {
|
|
|
45797
45811
|
this.i18n = inject(I18nService);
|
|
45798
45812
|
this.toast = inject(ToastService);
|
|
45799
45813
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
45800
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
45814
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$9;
|
|
45801
45815
|
this.hasPassword = signal(null);
|
|
45802
45816
|
this.codeSent = signal(false);
|
|
45803
45817
|
this.formState = signal(ComponentStates.ENABLED);
|
|
@@ -45864,8 +45878,8 @@ class DeleteAccountModalComponent {
|
|
|
45864
45878
|
],
|
|
45865
45879
|
actions: this.submitBtn('delete-code-submit', 'confirmCta'),
|
|
45866
45880
|
}));
|
|
45867
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
45868
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
45881
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
|
|
45882
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$9, DELETE_ACCOUNT_MODAL_I18N);
|
|
45869
45883
|
}
|
|
45870
45884
|
}
|
|
45871
45885
|
ngOnInit() {
|
|
@@ -46066,7 +46080,7 @@ const ORG_INFO_SHEET_I18N = {
|
|
|
46066
46080
|
},
|
|
46067
46081
|
};
|
|
46068
46082
|
|
|
46069
|
-
const DEFAULT_NAMESPACE$
|
|
46083
|
+
const DEFAULT_NAMESPACE$8 = 'OrgInfoSheet';
|
|
46070
46084
|
/**
|
|
46071
46085
|
* `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
|
|
46072
46086
|
* Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
|
|
@@ -46111,15 +46125,15 @@ class OrgInfoSheetComponent {
|
|
|
46111
46125
|
text: this.closeProps.text || this.resolvedCloseLabel(),
|
|
46112
46126
|
}));
|
|
46113
46127
|
addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
|
|
46114
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
46115
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
46128
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
|
|
46129
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$8, ORG_INFO_SHEET_I18N);
|
|
46116
46130
|
}
|
|
46117
46131
|
}
|
|
46118
46132
|
dismiss() {
|
|
46119
46133
|
this._modalRef?.dismiss(null, 'cancel');
|
|
46120
46134
|
}
|
|
46121
46135
|
tt(key) {
|
|
46122
|
-
return this.i18n.t(key, DEFAULT_NAMESPACE$
|
|
46136
|
+
return this.i18n.t(key, DEFAULT_NAMESPACE$8);
|
|
46123
46137
|
}
|
|
46124
46138
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
46125
46139
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: OrgInfoSheetComponent, isStandalone: true, selector: "val-org-info-sheet", inputs: { title: "title", subtitle: "subtitle", body: "body", closeLabel: "closeLabel", closeProps: "closeProps", _modalRef: "_modalRef" }, ngImport: i0, template: `
|
|
@@ -46286,7 +46300,7 @@ const SWITCH_ORG_MODAL_I18N = {
|
|
|
46286
46300
|
},
|
|
46287
46301
|
};
|
|
46288
46302
|
|
|
46289
|
-
const DEFAULT_NAMESPACE$
|
|
46303
|
+
const DEFAULT_NAMESPACE$7 = 'Settings.SwitchOrg';
|
|
46290
46304
|
addIcons({ businessOutline, checkmarkCircleOutline });
|
|
46291
46305
|
/**
|
|
46292
46306
|
* `val-switch-org-modal` — modal de cambio de organización activa (organism
|
|
@@ -46312,7 +46326,7 @@ class SwitchOrgModalComponent {
|
|
|
46312
46326
|
this.orgSwitch = inject(OrgSwitchService);
|
|
46313
46327
|
this.errors = inject(ValtechErrorService);
|
|
46314
46328
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
46315
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
46329
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$7;
|
|
46316
46330
|
this.orgs = signal([]);
|
|
46317
46331
|
this.loading = signal(false);
|
|
46318
46332
|
this.query = signal('');
|
|
@@ -46328,8 +46342,8 @@ class SwitchOrgModalComponent {
|
|
|
46328
46342
|
return list;
|
|
46329
46343
|
return list.filter(o => o.name.toLowerCase().includes(q));
|
|
46330
46344
|
});
|
|
46331
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
46332
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
46345
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
|
|
46346
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$7, SWITCH_ORG_MODAL_I18N);
|
|
46333
46347
|
}
|
|
46334
46348
|
this.loadOrgs();
|
|
46335
46349
|
}
|
|
@@ -46647,7 +46661,7 @@ const ACCOUNT_VIEW_I18N = {
|
|
|
46647
46661
|
};
|
|
46648
46662
|
|
|
46649
46663
|
addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
|
|
46650
|
-
const DEFAULT_NAMESPACE$
|
|
46664
|
+
const DEFAULT_NAMESPACE$6 = 'Settings.Account';
|
|
46651
46665
|
const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
|
|
46652
46666
|
/**
|
|
46653
46667
|
* `val-account-view` — vista Cuenta full-feature autocontenida (organism).
|
|
@@ -46688,7 +46702,7 @@ class AccountViewComponent {
|
|
|
46688
46702
|
showNewOrgCta: merged.showNewOrgCta ?? true,
|
|
46689
46703
|
showLogout: merged.showLogout ?? true,
|
|
46690
46704
|
showDeleteAccount: merged.showDeleteAccount ?? true,
|
|
46691
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$
|
|
46705
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$6,
|
|
46692
46706
|
manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
|
|
46693
46707
|
onOrgSwitched: merged.onOrgSwitched,
|
|
46694
46708
|
onOrgCreated: merged.onOrgCreated,
|
|
@@ -47413,7 +47427,7 @@ const EDIT_ORG_MODAL_I18N = {
|
|
|
47413
47427
|
},
|
|
47414
47428
|
};
|
|
47415
47429
|
|
|
47416
|
-
const DEFAULT_NAMESPACE$
|
|
47430
|
+
const DEFAULT_NAMESPACE$5 = 'EditOrgModal';
|
|
47417
47431
|
/**
|
|
47418
47432
|
* `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
|
|
47419
47433
|
* Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
|
|
@@ -47440,10 +47454,10 @@ class EditOrgModalComponent {
|
|
|
47440
47454
|
/** Organización a editar (pasada por `componentProps`). */
|
|
47441
47455
|
this.org = null;
|
|
47442
47456
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
47443
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
47457
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$5;
|
|
47444
47458
|
this.saving = signal(false);
|
|
47445
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
47446
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
47459
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
|
|
47460
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$5, EDIT_ORG_MODAL_I18N);
|
|
47447
47461
|
}
|
|
47448
47462
|
this.formMeta = signal(this.buildFormMeta());
|
|
47449
47463
|
effect(() => {
|
|
@@ -47681,7 +47695,7 @@ const INVITE_MEMBER_MODAL_I18N = {
|
|
|
47681
47695
|
},
|
|
47682
47696
|
};
|
|
47683
47697
|
|
|
47684
|
-
const DEFAULT_NAMESPACE$
|
|
47698
|
+
const DEFAULT_NAMESPACE$4 = 'InviteModal';
|
|
47685
47699
|
addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
|
|
47686
47700
|
/**
|
|
47687
47701
|
* `val-invite-member-modal` — invitación de miembros a una organización.
|
|
@@ -47712,7 +47726,7 @@ class InviteMemberModalComponent {
|
|
|
47712
47726
|
/** Ids de miembros ya existentes (excluidos de los resultados). */
|
|
47713
47727
|
this.existingMemberIds = [];
|
|
47714
47728
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
47715
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
47729
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$4;
|
|
47716
47730
|
this.query = signal('');
|
|
47717
47731
|
this.searching = signal(false);
|
|
47718
47732
|
this.searchResults = signal([]);
|
|
@@ -47759,8 +47773,8 @@ class InviteMemberModalComponent {
|
|
|
47759
47773
|
state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
47760
47774
|
}));
|
|
47761
47775
|
this.searchSubject = new Subject();
|
|
47762
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
47763
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
47776
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
|
|
47777
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$4, INVITE_MEMBER_MODAL_I18N);
|
|
47764
47778
|
}
|
|
47765
47779
|
this.loadRoles();
|
|
47766
47780
|
this.roleSub = this.roleControl.valueChanges.subscribe(v => {
|
|
@@ -48249,7 +48263,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
|
|
|
48249
48263
|
},
|
|
48250
48264
|
};
|
|
48251
48265
|
|
|
48252
|
-
const DEFAULT_NAMESPACE$
|
|
48266
|
+
const DEFAULT_NAMESPACE$3 = 'Settings.MemberDetail';
|
|
48253
48267
|
/**
|
|
48254
48268
|
* `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
|
|
48255
48269
|
* permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
|
|
@@ -48286,7 +48300,7 @@ class MemberDetailModalComponent {
|
|
|
48286
48300
|
{ id: 'admin', name: 'admin' },
|
|
48287
48301
|
];
|
|
48288
48302
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
48289
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
48303
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$3;
|
|
48290
48304
|
this.working = signal(false);
|
|
48291
48305
|
this.currentRole = signal('');
|
|
48292
48306
|
this.roleControl = new FormControl('');
|
|
@@ -48326,8 +48340,8 @@ class MemberDetailModalComponent {
|
|
|
48326
48340
|
type: 'button',
|
|
48327
48341
|
state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
48328
48342
|
}));
|
|
48329
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
48330
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
48343
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$3)) {
|
|
48344
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$3, MEMBER_DETAIL_MODAL_I18N);
|
|
48331
48345
|
}
|
|
48332
48346
|
}
|
|
48333
48347
|
ngOnInit() {
|
|
@@ -48664,7 +48678,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
|
|
|
48664
48678
|
},
|
|
48665
48679
|
};
|
|
48666
48680
|
|
|
48667
|
-
const DEFAULT_NAMESPACE$
|
|
48681
|
+
const DEFAULT_NAMESPACE$2 = 'Settings.TransferModal';
|
|
48668
48682
|
/**
|
|
48669
48683
|
* `val-transfer-ownership-modal` — selector de miembro para transferir la
|
|
48670
48684
|
* propiedad de la organización. Promovido desde `showcase` bajo el proceso de
|
|
@@ -48688,7 +48702,7 @@ class TransferOwnershipModalComponent {
|
|
|
48688
48702
|
/** Lista de miembros transferibles (todos excepto el owner actual). */
|
|
48689
48703
|
this.members = [];
|
|
48690
48704
|
/** Namespace i18n con que la vista resuelve sus textos. */
|
|
48691
|
-
this.i18nNamespace = DEFAULT_NAMESPACE$
|
|
48705
|
+
this.i18nNamespace = DEFAULT_NAMESPACE$2;
|
|
48692
48706
|
this.query = signal('');
|
|
48693
48707
|
this.selectedId = signal('');
|
|
48694
48708
|
this.filtered = computed(() => {
|
|
@@ -48700,8 +48714,8 @@ class TransferOwnershipModalComponent {
|
|
|
48700
48714
|
m.userId.toLowerCase().includes(q));
|
|
48701
48715
|
});
|
|
48702
48716
|
this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
|
|
48703
|
-
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$
|
|
48704
|
-
this.i18n.registerContent(DEFAULT_NAMESPACE$
|
|
48717
|
+
if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$2)) {
|
|
48718
|
+
this.i18n.registerContent(DEFAULT_NAMESPACE$2, TRANSFER_OWNERSHIP_MODAL_I18N);
|
|
48705
48719
|
}
|
|
48706
48720
|
}
|
|
48707
48721
|
ngOnInit() {
|
|
@@ -49034,7 +49048,7 @@ const ORGANIZATION_VIEW_I18N = {
|
|
|
49034
49048
|
},
|
|
49035
49049
|
};
|
|
49036
49050
|
|
|
49037
|
-
const DEFAULT_NAMESPACE = 'Settings.Organization';
|
|
49051
|
+
const DEFAULT_NAMESPACE$1 = 'Settings.Organization';
|
|
49038
49052
|
const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
|
|
49039
49053
|
const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
|
|
49040
49054
|
/**
|
|
@@ -49091,7 +49105,7 @@ class OrganizationViewComponent {
|
|
|
49091
49105
|
showInvite: merged.showInvite ?? true,
|
|
49092
49106
|
showTransferOwnership: merged.showTransferOwnership ?? true,
|
|
49093
49107
|
showLeave: merged.showLeave ?? true,
|
|
49094
|
-
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
|
|
49108
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
|
|
49095
49109
|
viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
|
|
49096
49110
|
leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
|
|
49097
49111
|
onOrgEdited: merged.onOrgEdited,
|
|
@@ -50098,6 +50112,942 @@ function provideValtechOrganizationRoutes(opts) {
|
|
|
50098
50112
|
];
|
|
50099
50113
|
}
|
|
50100
50114
|
|
|
50115
|
+
/**
|
|
50116
|
+
* Refreshable Stream
|
|
50117
|
+
*
|
|
50118
|
+
* Helper estándar para vistas que consumen datos de Firestore. Soporta DOS
|
|
50119
|
+
* modos — el caller decide cuál vía la `factory`:
|
|
50120
|
+
*
|
|
50121
|
+
* - **one-shot** (recomendado por defecto): `() => from(svc.getAllOnce())`.
|
|
50122
|
+
* Carga una vez y completa. Más barato — sin listener vivo.
|
|
50123
|
+
* - **real-time**: `() => svc.getAll()`. Listener Firestore que auto-actualiza.
|
|
50124
|
+
* Solo donde importa (inbox de notificaciones, badges).
|
|
50125
|
+
*
|
|
50126
|
+
* En ambos modos la primera suscripción se gatea por `firebaseAuthReady`
|
|
50127
|
+
* (sesión de Firebase Auth lista) — así el listener/lectura nunca se adjunta
|
|
50128
|
+
* antes de que `request.auth` esté disponible en las reglas de Firestore, lo
|
|
50129
|
+
* que cierra la ventana de `permission-denied` en cold start de PWA.
|
|
50130
|
+
*
|
|
50131
|
+
* El `retry` con backoff corto es solo red de seguridad para reconexiones
|
|
50132
|
+
* transitorias (no para cubrir cold start — eso lo cubre el gate de auth).
|
|
50133
|
+
*/
|
|
50134
|
+
/**
|
|
50135
|
+
* Crea un stream refrescable a partir de una factory de Observable.
|
|
50136
|
+
*
|
|
50137
|
+
* DEBE llamarse en un injection context (field initializer o constructor) —
|
|
50138
|
+
* usa `toSignal`/`toObservable`/`inject` internamente.
|
|
50139
|
+
*
|
|
50140
|
+
* @param factory - Función que produce el Observable a consumir. El caller
|
|
50141
|
+
* decide el modo: `() => from(svc.getAllOnce())` (one-shot) o
|
|
50142
|
+
* `() => svc.getAll()` (real-time).
|
|
50143
|
+
* @param options - Configuración opcional (fallback, retry, gate).
|
|
50144
|
+
*
|
|
50145
|
+
* @example
|
|
50146
|
+
* ```typescript
|
|
50147
|
+
* // one-shot (recomendado por defecto)
|
|
50148
|
+
* private readonly stream = createRefreshableStream(
|
|
50149
|
+
* () => from(this.svc.getAllOnce()),
|
|
50150
|
+
* );
|
|
50151
|
+
*
|
|
50152
|
+
* // real-time (inbox)
|
|
50153
|
+
* private readonly stream = createRefreshableStream(
|
|
50154
|
+
* () => this.notifs.getAll(50),
|
|
50155
|
+
* );
|
|
50156
|
+
*
|
|
50157
|
+
* readonly items = this.stream.data;
|
|
50158
|
+
* readonly loading = this.stream.loading;
|
|
50159
|
+
*
|
|
50160
|
+
* ionViewWillEnter() {
|
|
50161
|
+
* this.pageRefresh.register(() => this.stream.reload());
|
|
50162
|
+
* }
|
|
50163
|
+
* ```
|
|
50164
|
+
*/
|
|
50165
|
+
function createRefreshableStream(factory, options) {
|
|
50166
|
+
const injector = options?.injector ?? inject(Injector);
|
|
50167
|
+
const fallback = (options?.fallback ?? null);
|
|
50168
|
+
const retryCount = options?.retryCount ?? 4;
|
|
50169
|
+
const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
|
|
50170
|
+
const gateOnFirebaseAuth = options?.gateOnFirebaseAuth ?? true;
|
|
50171
|
+
const authGateTimeoutMs = options?.authGateTimeoutMs ?? 20000;
|
|
50172
|
+
const firstEmitTimeoutMs = options?.firstEmitTimeoutMs ?? 20000;
|
|
50173
|
+
// FirebaseService es opcional — sin él (apps sin Firebase) no se gatea.
|
|
50174
|
+
const firebase = gateOnFirebaseAuth ? injector.get(FirebaseService, null) : null;
|
|
50175
|
+
const _reload = signal(0);
|
|
50176
|
+
const _loading = signal(true);
|
|
50177
|
+
const _error = signal(false);
|
|
50178
|
+
// Gate de Firebase Auth: el stream no abre la primera suscripción hasta que
|
|
50179
|
+
// la sesión esté confirmada. `race` con un `timer` de seguridad — si la
|
|
50180
|
+
// sesión NUNCA se establece (token inválido, signInWithFirebase falló), el
|
|
50181
|
+
// gate igual se libera tras `authGateTimeoutMs` y la factory procede: si no
|
|
50182
|
+
// hay permiso terminará en estado `error`, jamás en skeleton infinito.
|
|
50183
|
+
// Tag los dos inputs del `race` para distinguir en logs cuál ganó: la sesión
|
|
50184
|
+
// de Firebase Auth (`auth-ready`) o el timeout de seguridad (`timeout`).
|
|
50185
|
+
const authGate$ = firebase
|
|
50186
|
+
? race(firebase.firebaseAuthReady$.pipe(map$1(() => 'auth-ready')), timer(authGateTimeoutMs).pipe(map$1(() => 'timeout'))).pipe(take(1), tap(winner => {
|
|
50187
|
+
if (winner === 'timeout') {
|
|
50188
|
+
console.warn(`[RefreshStream] auth gate resolved by TIMEOUT after ${authGateTimeoutMs}ms ` +
|
|
50189
|
+
'(Firebase Auth session never confirmed)');
|
|
50190
|
+
}
|
|
50191
|
+
else {
|
|
50192
|
+
console.log('[RefreshStream] auth gate resolved by firebaseAuthReady$ (session ready)');
|
|
50193
|
+
}
|
|
50194
|
+
}))
|
|
50195
|
+
: of(true);
|
|
50196
|
+
const data = toSignal(toObservable(_reload, { injector }).pipe(tap(() => {
|
|
50197
|
+
_loading.set(true);
|
|
50198
|
+
_error.set(false);
|
|
50199
|
+
}),
|
|
50200
|
+
// Esperar a Firebase Auth listo antes de cada (re)suscripción. Como
|
|
50201
|
+
// `firebaseAuthReady$` completa tras la primera emisión, las reloads
|
|
50202
|
+
// posteriores resuelven el gate de inmediato (auth ya está listo).
|
|
50203
|
+
switchMap(() => authGate$.pipe(switchMap(() => factory().pipe(tap(() => {
|
|
50204
|
+
_loading.set(false);
|
|
50205
|
+
_error.set(false);
|
|
50206
|
+
console.log('[RefreshStream] factory emitted first value — loading=false');
|
|
50207
|
+
}),
|
|
50208
|
+
// Backoff corto exponencial acotado — solo red de seguridad para
|
|
50209
|
+
// reconexiones transitorias, NO para cubrir cold start.
|
|
50210
|
+
retry({
|
|
50211
|
+
count: retryCount,
|
|
50212
|
+
delay: (_err, attempt) => timer(Math.min(retryBaseDelayMs * 2 ** (attempt - 1), 4000)),
|
|
50213
|
+
}),
|
|
50214
|
+
// Techo absoluto para la PRIMERA emisión. Si la factory no emite
|
|
50215
|
+
// ni valor ni error (servicio nunca inicializado → `EMPTY` →
|
|
50216
|
+
// silencio), `timeout` dispara un error → `catchError` → estado
|
|
50217
|
+
// `error`. Nunca skeleton perpetuo. Va DESPUÉS de `retry` para
|
|
50218
|
+
// que cuente el tiempo total, no por intento.
|
|
50219
|
+
timeout({ first: firstEmitTimeoutMs }), catchError(err => {
|
|
50220
|
+
_loading.set(false);
|
|
50221
|
+
_error.set(true);
|
|
50222
|
+
console.error(`[RefreshStream] factory failed/timed out → error state: ${err instanceof Error ? err.message : String(err)}`);
|
|
50223
|
+
return of(fallback);
|
|
50224
|
+
})))))), { initialValue: null, injector });
|
|
50225
|
+
return {
|
|
50226
|
+
data,
|
|
50227
|
+
loading: _loading.asReadonly(),
|
|
50228
|
+
error: _error.asReadonly(),
|
|
50229
|
+
reload: () => _reload.update(n => n + 1),
|
|
50230
|
+
};
|
|
50231
|
+
}
|
|
50232
|
+
|
|
50233
|
+
/**
|
|
50234
|
+
* Defaults i18n (es/en) embebidos en `val-notifications-view`. Auto-registrados
|
|
50235
|
+
* en el constructor del componente si el consumer no proveyó el namespace
|
|
50236
|
+
* (`Notifications` por default). SOLO incluye las keys que la vista usa.
|
|
50237
|
+
*
|
|
50238
|
+
* Portado del override `Notifications` (showcase) bajo el proceso de ADR-021.
|
|
50239
|
+
*/
|
|
50240
|
+
const NOTIFICATIONS_VIEW_I18N = {
|
|
50241
|
+
es: {
|
|
50242
|
+
pageTitle: 'Notificaciones',
|
|
50243
|
+
pageDescription: 'Tu bandeja de mensajes y actualizaciones',
|
|
50244
|
+
groupToday: 'Hoy',
|
|
50245
|
+
groupThisMonth: 'Este mes',
|
|
50246
|
+
groupEarlier: 'Anteriores',
|
|
50247
|
+
empty: 'Aún no tienes notificaciones',
|
|
50248
|
+
emptyHint: 'Activa las notificaciones para recibir actualizaciones, alertas y novedades importantes.',
|
|
50249
|
+
emptyCta: 'Ir a configuración',
|
|
50250
|
+
loadErrorTitle: 'No pudimos cargar tus notificaciones',
|
|
50251
|
+
loadErrorHint: 'Revisa tu conexión e inténtalo de nuevo. Tus notificaciones siguen guardadas.',
|
|
50252
|
+
offlineTitle: 'Sin conexión',
|
|
50253
|
+
offlineHint: 'No hay acceso a la red. Reconéctate e intenta de nuevo.',
|
|
50254
|
+
retryCta: 'Reintentar',
|
|
50255
|
+
pushOffTitle: 'No recibirás avisos',
|
|
50256
|
+
pushOffHint: 'Puedes leer tus mensajes aquí, pero las notificaciones push están desactivadas. Actívalas para recibir avisos en tu dispositivo.',
|
|
50257
|
+
pushOffCta: 'Activar notificaciones',
|
|
50258
|
+
loading: 'Cargando…',
|
|
50259
|
+
loadMore: 'Cargar más',
|
|
50260
|
+
seeMore: 'Ver más',
|
|
50261
|
+
seeLess: 'Ver menos',
|
|
50262
|
+
open: 'Ver',
|
|
50263
|
+
markAll: 'Marcar todas como leídas',
|
|
50264
|
+
unread: 'No leída',
|
|
50265
|
+
crossApp: 'Desde {app}',
|
|
50266
|
+
timeNow: 'ahora',
|
|
50267
|
+
timeMinutes: 'hace {n} min',
|
|
50268
|
+
timeHours: 'hace {n} h',
|
|
50269
|
+
timeDays: 'hace {n} d',
|
|
50270
|
+
markedAllSuccess: 'Notificaciones marcadas como leídas.',
|
|
50271
|
+
markedAllError: 'No se pudo marcar todas como leídas.',
|
|
50272
|
+
openError: 'No se pudo abrir la notificación.',
|
|
50273
|
+
},
|
|
50274
|
+
en: {
|
|
50275
|
+
pageTitle: 'Notifications',
|
|
50276
|
+
pageDescription: 'Your inbox of messages and updates',
|
|
50277
|
+
groupToday: 'Today',
|
|
50278
|
+
groupThisMonth: 'This month',
|
|
50279
|
+
groupEarlier: 'Earlier',
|
|
50280
|
+
empty: 'No notifications yet',
|
|
50281
|
+
emptyHint: 'Enable notifications to receive updates, alerts and important news.',
|
|
50282
|
+
emptyCta: 'Go to settings',
|
|
50283
|
+
loadErrorTitle: "We couldn't load your notifications",
|
|
50284
|
+
loadErrorHint: 'Check your connection and try again. Your notifications are still saved.',
|
|
50285
|
+
offlineTitle: 'No connection',
|
|
50286
|
+
offlineHint: 'No network access. Reconnect and try again.',
|
|
50287
|
+
retryCta: 'Retry',
|
|
50288
|
+
pushOffTitle: "You won't get alerts",
|
|
50289
|
+
pushOffHint: 'You can read your messages here, but push notifications are off. Turn them on to get alerts on your device.',
|
|
50290
|
+
pushOffCta: 'Enable notifications',
|
|
50291
|
+
loading: 'Loading…',
|
|
50292
|
+
loadMore: 'Load more',
|
|
50293
|
+
seeMore: 'See more',
|
|
50294
|
+
seeLess: 'See less',
|
|
50295
|
+
open: 'Open',
|
|
50296
|
+
markAll: 'Mark all as read',
|
|
50297
|
+
unread: 'Unread',
|
|
50298
|
+
crossApp: 'From {app}',
|
|
50299
|
+
timeNow: 'now',
|
|
50300
|
+
timeMinutes: '{n} min ago',
|
|
50301
|
+
timeHours: '{n} h ago',
|
|
50302
|
+
timeDays: '{n} d ago',
|
|
50303
|
+
markedAllSuccess: 'Notifications marked as read.',
|
|
50304
|
+
markedAllError: 'Could not mark all as read.',
|
|
50305
|
+
openError: 'Could not open notification.',
|
|
50306
|
+
},
|
|
50307
|
+
};
|
|
50308
|
+
|
|
50309
|
+
addIcons({
|
|
50310
|
+
alarmOutline,
|
|
50311
|
+
chatboxEllipsesOutline,
|
|
50312
|
+
chatbubbleOutline,
|
|
50313
|
+
checkmarkCircleOutline,
|
|
50314
|
+
checkmarkDoneOutline,
|
|
50315
|
+
cloudOfflineOutline,
|
|
50316
|
+
cogOutline,
|
|
50317
|
+
documentTextOutline,
|
|
50318
|
+
notificationsOffOutline,
|
|
50319
|
+
notificationsOutline,
|
|
50320
|
+
openOutline,
|
|
50321
|
+
personOutline,
|
|
50322
|
+
});
|
|
50323
|
+
const DEFAULT_NAMESPACE = 'Notifications';
|
|
50324
|
+
const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
|
|
50325
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
50326
|
+
const DEFAULT_MAX_LOAD = 50;
|
|
50327
|
+
/**
|
|
50328
|
+
* `val-notifications-view` — inbox de notificaciones full-feature autocontenido
|
|
50329
|
+
* (organism). Promovido desde `showcase` bajo el proceso de ADR-021.
|
|
50330
|
+
*
|
|
50331
|
+
* Separado de `/app/settings/notifications` (que controla permiso FCM + master
|
|
50332
|
+
* toggle). Aquí el user lee/abre/marca-leídas sus mensajes.
|
|
50333
|
+
*
|
|
50334
|
+
* - **Stream real-time** vía `NotificationsService.getAll(maxLoad)` envuelto en
|
|
50335
|
+
* `createRefreshableStream` (gateado por `firebaseAuthReady`, retry corto,
|
|
50336
|
+
* `firstEmitTimeoutMs: 15000`). Estados: loading (skeleton), error
|
|
50337
|
+
* (`val-empty-state` + reintentar), empty (`val-empty-state` + CTA), data
|
|
50338
|
+
* (lista agrupada Hoy / Este mes / Anteriores).
|
|
50339
|
+
* - **Abrir** una notif → `NotificationActionService.open()` (markRead +
|
|
50340
|
+
* switch-org + navegar/handoff cross-app). Hook `onNotificationOpen` opcional.
|
|
50341
|
+
* - **Marcar todas como leídas** (gateado por `enableMarkAll`).
|
|
50342
|
+
* - **CTA push-off** cuando el permiso del navegador no está concedido (gateado
|
|
50343
|
+
* por `showPushOffCta`); navega a `settingsNotificationsPath`.
|
|
50344
|
+
* - **Paginación soft** — slice local (`pageSize`, +`pageSize` por click) sobre el
|
|
50345
|
+
* cache del stream; el tope de fetch es `maxLoad`.
|
|
50346
|
+
* - **i18n auto-registrado** es/en bajo `i18nNamespace` (default `Notifications`).
|
|
50347
|
+
*
|
|
50348
|
+
* NO renderiza ion-content — vive dentro de val-page-wrapper. `Router` y
|
|
50349
|
+
* `ActivatedRoute` se inyectan `{ optional: true }` → funciona sin router
|
|
50350
|
+
* (embebida / Storybook). El forward-compat icon resolver lee `notif.payload.icon`
|
|
50351
|
+
* si existe (`{ type, value, color? }`), cae a un mapa por `notif.type`, luego a
|
|
50352
|
+
* `notifications-outline`.
|
|
50353
|
+
*/
|
|
50354
|
+
class NotificationsViewComponent {
|
|
50355
|
+
/** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
|
|
50356
|
+
get ns() {
|
|
50357
|
+
return this.resolvedConfig().i18nNamespace;
|
|
50358
|
+
}
|
|
50359
|
+
/** Paso de paginación soft (revela +N por "cargar más"). */
|
|
50360
|
+
get pageStep() {
|
|
50361
|
+
return this.resolvedConfig().pageSize;
|
|
50362
|
+
}
|
|
50363
|
+
constructor() {
|
|
50364
|
+
this.nav = inject(NavigationService);
|
|
50365
|
+
this.i18n = inject(I18nService);
|
|
50366
|
+
this.notifs = inject(NotificationsService);
|
|
50367
|
+
this.action = inject(NotificationActionService);
|
|
50368
|
+
this.toast = inject(ToastService);
|
|
50369
|
+
this.errors = inject(ValtechErrorService);
|
|
50370
|
+
this.auth = inject(AuthService);
|
|
50371
|
+
this.pageRefresh = inject(PageRefreshService);
|
|
50372
|
+
// `ActivatedRoute` opcional → la vista funciona embebida / en Storybook sin
|
|
50373
|
+
// router (solo lo usa para leer el route data de config como fallback). La
|
|
50374
|
+
// navegación va por `NavigationService` (wrapper opcional sobre `Router`).
|
|
50375
|
+
this.route = inject(ActivatedRoute, { optional: true });
|
|
50376
|
+
this.resolvedConfig = computed(() => {
|
|
50377
|
+
const fromRoute = (this.route?.snapshot.data['notificationsConfig'] ?? {});
|
|
50378
|
+
const merged = { ...fromRoute, ...(this.config ?? {}) };
|
|
50379
|
+
return {
|
|
50380
|
+
showPushOffCta: merged.showPushOffCta ?? true,
|
|
50381
|
+
enableMarkAll: merged.enableMarkAll ?? true,
|
|
50382
|
+
settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
|
|
50383
|
+
i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
|
|
50384
|
+
pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
|
|
50385
|
+
maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
|
|
50386
|
+
onNotificationOpen: merged.onNotificationOpen,
|
|
50387
|
+
};
|
|
50388
|
+
});
|
|
50389
|
+
this._expanded = signal(new Set());
|
|
50390
|
+
this._pageSize = signal(this.resolvedConfig().pageSize);
|
|
50391
|
+
this._bulkBusy = signal(false);
|
|
50392
|
+
this._loadingMore = signal(false);
|
|
50393
|
+
/**
|
|
50394
|
+
* Push activado = permiso del navegador en `granted`. Si NO está activado,
|
|
50395
|
+
* el user puede leer el inbox (Firestore) pero NO recibirá push en su
|
|
50396
|
+
* dispositivo → mostramos una card que lo invita a ir a configuración.
|
|
50397
|
+
* Se evalúa en cada montaje de la vista.
|
|
50398
|
+
*/
|
|
50399
|
+
this.pushEnabled = signal(this.auth.getNotificationPermissionState() === 'granted');
|
|
50400
|
+
/**
|
|
50401
|
+
* Stream firestore real-time vía el helper estándar `createRefreshableStream`
|
|
50402
|
+
* (modo real-time: factory devuelve un listener vivo `notifs.getAll(maxLoad)`).
|
|
50403
|
+
*
|
|
50404
|
+
* El helper gatea la primera suscripción por `firebaseAuthReady`, aplica un
|
|
50405
|
+
* `retry` corto con backoff y, tras agotar reintentos, emite el fallback (`[]`)
|
|
50406
|
+
* y marca `error` — estado de error en UI, no skeleton infinito. `reload()`
|
|
50407
|
+
* re-suscribe el stream completo (lo usa el pull-to-refresh).
|
|
50408
|
+
*/
|
|
50409
|
+
this._stream = createRefreshableStream(() => this.notifs.getAll(this.resolvedConfig().maxLoad), {
|
|
50410
|
+
fallback: [],
|
|
50411
|
+
// `notifs.getAll()` YA gatea internamente por `firebaseAuthReady`. Si el
|
|
50412
|
+
// stream también gateara, serían DOS esperas de auth apiladas.
|
|
50413
|
+
gateOnFirebaseAuth: false,
|
|
50414
|
+
// Cold start de PWA en iOS puede pasar de 8s en redes lentas; con 15s
|
|
50415
|
+
// evitamos el falso estado de error.
|
|
50416
|
+
firstEmitTimeoutMs: 15000,
|
|
50417
|
+
});
|
|
50418
|
+
/** True si el stream falló tras agotar reintentos → muestra estado de error. */
|
|
50419
|
+
this.streamError = this._stream.error;
|
|
50420
|
+
this.loading = computed(() => this._stream.data() === null);
|
|
50421
|
+
/**
|
|
50422
|
+
* Notifs reales mostradas. Filtramos docs sin título Y sin body — son ruido
|
|
50423
|
+
* (típicamente sentinel/system writes pre-schema con title/body opcionales).
|
|
50424
|
+
*/
|
|
50425
|
+
this.allNotifs = computed(() => (this._stream.data() ?? []).filter(n => !!(n.title && n.title.trim()) || !!(n.body && n.body.trim())));
|
|
50426
|
+
this.hasMore = computed(() => this.allNotifs().length > this._pageSize());
|
|
50427
|
+
this.showMarkAll = computed(() => this.resolvedConfig().enableMarkAll && !this.loading() && this.allNotifs().some(n => !n.isRead));
|
|
50428
|
+
/** Sub-conjunto paginado, agrupado por período. */
|
|
50429
|
+
this.groups = computed(() => {
|
|
50430
|
+
const items = this.allNotifs().slice(0, this._pageSize());
|
|
50431
|
+
const now = new Date();
|
|
50432
|
+
const buckets = {
|
|
50433
|
+
today: [],
|
|
50434
|
+
thisMonth: [],
|
|
50435
|
+
earlier: [],
|
|
50436
|
+
};
|
|
50437
|
+
for (const n of items) {
|
|
50438
|
+
buckets[this.bucketOf(n, now)].push(n);
|
|
50439
|
+
}
|
|
50440
|
+
const out = [];
|
|
50441
|
+
if (buckets.today.length) {
|
|
50442
|
+
out.push({ key: 'today', label: this.tt('groupToday'), items: buckets.today });
|
|
50443
|
+
}
|
|
50444
|
+
if (buckets.thisMonth.length) {
|
|
50445
|
+
out.push({
|
|
50446
|
+
key: 'thisMonth',
|
|
50447
|
+
label: this.tt('groupThisMonth'),
|
|
50448
|
+
items: buckets.thisMonth,
|
|
50449
|
+
});
|
|
50450
|
+
}
|
|
50451
|
+
if (buckets.earlier.length) {
|
|
50452
|
+
out.push({ key: 'earlier', label: this.tt('groupEarlier'), items: buckets.earlier });
|
|
50453
|
+
}
|
|
50454
|
+
return out;
|
|
50455
|
+
});
|
|
50456
|
+
this.pageTitle = computed(() => this.tt('pageTitle'));
|
|
50457
|
+
this.pageDescription = computed(() => this.tt('pageDescription'));
|
|
50458
|
+
// CTA "activa las notificaciones push" — visible cuando pushEnabled() es false.
|
|
50459
|
+
this.ctaTitle = computed(() => this.tt('pushOffTitle'));
|
|
50460
|
+
this.ctaHint = computed(() => this.tt('pushOffHint'));
|
|
50461
|
+
this.ctaButtonProps = computed(() => ({
|
|
50462
|
+
text: this.tt('pushOffCta'),
|
|
50463
|
+
color: 'dark',
|
|
50464
|
+
fill: 'outline',
|
|
50465
|
+
size: 'small',
|
|
50466
|
+
shape: 'round',
|
|
50467
|
+
type: 'button',
|
|
50468
|
+
}));
|
|
50469
|
+
/**
|
|
50470
|
+
* Estado de error de carga: convierte el error del stream en
|
|
50471
|
+
* `EmptyStateMetadata` via el helper canónico `createErrorStateProps`.
|
|
50472
|
+
* El helper decide variante (`offline` vs `error`) por `isNetwork`.
|
|
50473
|
+
* Null cuando el stream no ha fallado.
|
|
50474
|
+
*/
|
|
50475
|
+
this.errorState = computed(() => {
|
|
50476
|
+
this.i18n.lang();
|
|
50477
|
+
const err = this.streamError();
|
|
50478
|
+
if (!err)
|
|
50479
|
+
return null;
|
|
50480
|
+
return createErrorStateProps(err, {
|
|
50481
|
+
title: {
|
|
50482
|
+
offline: this.tt('offlineTitle'),
|
|
50483
|
+
error: this.tt('loadErrorTitle'),
|
|
50484
|
+
},
|
|
50485
|
+
description: {
|
|
50486
|
+
offline: this.tt('offlineHint'),
|
|
50487
|
+
error: interpretError(err).message || this.tt('loadErrorHint'),
|
|
50488
|
+
},
|
|
50489
|
+
retryLabel: this.tt('retryCta'),
|
|
50490
|
+
onRetry: () => this._stream.reload(),
|
|
50491
|
+
retrying: this.loading(),
|
|
50492
|
+
});
|
|
50493
|
+
});
|
|
50494
|
+
/**
|
|
50495
|
+
* Estado vacío (lista real = 0 notificaciones, sin error de carga).
|
|
50496
|
+
* Se construye manualmente con `variant: 'empty'` — sin helper de error.
|
|
50497
|
+
*/
|
|
50498
|
+
this.emptyStateProps = computed(() => {
|
|
50499
|
+
this.i18n.lang();
|
|
50500
|
+
return {
|
|
50501
|
+
variant: 'empty',
|
|
50502
|
+
icon: 'notifications-off-outline',
|
|
50503
|
+
title: this.tt('empty'),
|
|
50504
|
+
description: this.tt('emptyHint'),
|
|
50505
|
+
action: {
|
|
50506
|
+
label: this.tt('emptyCta'),
|
|
50507
|
+
handler: () => this.onGoToSettings(),
|
|
50508
|
+
},
|
|
50509
|
+
};
|
|
50510
|
+
});
|
|
50511
|
+
this.seeMore = computed(() => this.tt('seeMore'));
|
|
50512
|
+
this.seeLess = computed(() => this.tt('seeLess'));
|
|
50513
|
+
this.markAllButtonProps = computed(() => ({
|
|
50514
|
+
text: this.tt('markAll'),
|
|
50515
|
+
color: 'dark',
|
|
50516
|
+
fill: 'outline',
|
|
50517
|
+
size: 'small',
|
|
50518
|
+
shape: 'round',
|
|
50519
|
+
type: 'button',
|
|
50520
|
+
icon: { name: 'checkmark-done-outline', slot: 'start' },
|
|
50521
|
+
state: this._bulkBusy() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
50522
|
+
}));
|
|
50523
|
+
this.loadMoreButtonProps = computed(() => ({
|
|
50524
|
+
text: this.tt('loadMore'),
|
|
50525
|
+
color: 'dark',
|
|
50526
|
+
fill: 'clear',
|
|
50527
|
+
size: 'default',
|
|
50528
|
+
shape: 'round',
|
|
50529
|
+
type: 'button',
|
|
50530
|
+
state: this._loadingMore() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
50531
|
+
}));
|
|
50532
|
+
const ns = this.ns;
|
|
50533
|
+
if (!this.i18n.hasNamespace(ns)) {
|
|
50534
|
+
this.i18n.registerContent(ns, NOTIFICATIONS_VIEW_I18N);
|
|
50535
|
+
}
|
|
50536
|
+
this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
|
|
50537
|
+
// Re-aplicar header en cambio de idioma.
|
|
50538
|
+
effect(() => {
|
|
50539
|
+
this.i18n.lang();
|
|
50540
|
+
this.nav.setBackHeader('pageTitle', this.ns, { withMenu: true });
|
|
50541
|
+
}, { allowSignalWrites: true });
|
|
50542
|
+
}
|
|
50543
|
+
/**
|
|
50544
|
+
* Registra el pull-to-refresh estándar. Usamos `ngOnInit`/`ngOnDestroy`
|
|
50545
|
+
* (NO `ionViewWillEnter`) porque `val-page-wrapper` usa `<router-outlet>`
|
|
50546
|
+
* plano — los hooks `ionView*` solo disparan dentro de `<ion-router-outlet>`.
|
|
50547
|
+
*/
|
|
50548
|
+
ngOnInit() {
|
|
50549
|
+
this.pageRefresh.register(() => this.reload());
|
|
50550
|
+
}
|
|
50551
|
+
/** Quita el handler al destruir la vista — el refresher desaparece. */
|
|
50552
|
+
ngOnDestroy() {
|
|
50553
|
+
this.pageRefresh.unregister();
|
|
50554
|
+
}
|
|
50555
|
+
/**
|
|
50556
|
+
* Refresh manual: re-suscribe el stream Firestore (listener nuevo) vía el
|
|
50557
|
+
* helper. Resuelve tras un margen breve — el stream es real-time y re-emite
|
|
50558
|
+
* en milisegundos; el delay sólo mantiene el spinner visible lo justo.
|
|
50559
|
+
*/
|
|
50560
|
+
async reload() {
|
|
50561
|
+
this._stream.reload();
|
|
50562
|
+
await new Promise(r => setTimeout(r, 700));
|
|
50563
|
+
}
|
|
50564
|
+
// ---------------------------------------------------------------------------
|
|
50565
|
+
// Per-item helpers (puros — usables desde template).
|
|
50566
|
+
// ---------------------------------------------------------------------------
|
|
50567
|
+
openButtonProps() {
|
|
50568
|
+
return {
|
|
50569
|
+
text: this.tt('open'),
|
|
50570
|
+
color: 'dark',
|
|
50571
|
+
fill: 'clear',
|
|
50572
|
+
size: 'small',
|
|
50573
|
+
shape: 'round',
|
|
50574
|
+
type: 'button',
|
|
50575
|
+
icon: { name: 'open-outline', slot: 'end' },
|
|
50576
|
+
};
|
|
50577
|
+
}
|
|
50578
|
+
isExpanded(id) {
|
|
50579
|
+
return this._expanded().has(id);
|
|
50580
|
+
}
|
|
50581
|
+
toggleExpand(id) {
|
|
50582
|
+
const next = new Set(this._expanded());
|
|
50583
|
+
if (next.has(id))
|
|
50584
|
+
next.delete(id);
|
|
50585
|
+
else
|
|
50586
|
+
next.add(id);
|
|
50587
|
+
this._expanded.set(next);
|
|
50588
|
+
}
|
|
50589
|
+
isLong(body) {
|
|
50590
|
+
if (!body)
|
|
50591
|
+
return false;
|
|
50592
|
+
return body.length > 110 || body.includes('\n');
|
|
50593
|
+
}
|
|
50594
|
+
/**
|
|
50595
|
+
* Icon resolver con orden de fallback:
|
|
50596
|
+
* 1. notif.payload.icon (forward-compat, sin breaking change en schema)
|
|
50597
|
+
* 2. mapa por notif.type
|
|
50598
|
+
* 3. default notifications-outline
|
|
50599
|
+
*/
|
|
50600
|
+
resolveIcon(n) {
|
|
50601
|
+
const hint = n.payload?.['icon'];
|
|
50602
|
+
if (hint?.value) {
|
|
50603
|
+
if (hint.type === 'image' || hint.type === 'avatar') {
|
|
50604
|
+
return { kind: hint.type, src: hint.value };
|
|
50605
|
+
}
|
|
50606
|
+
// Ignoramos hint.color — todos los iconos van en dark por convención UX.
|
|
50607
|
+
return { kind: 'ionicon', name: hint.value, color: 'dark' };
|
|
50608
|
+
}
|
|
50609
|
+
const t = n.type;
|
|
50610
|
+
const name = (() => {
|
|
50611
|
+
switch (t) {
|
|
50612
|
+
case 'approval':
|
|
50613
|
+
return 'checkmark-circle-outline';
|
|
50614
|
+
case 'chat':
|
|
50615
|
+
case 'chatbot':
|
|
50616
|
+
return 'chatbubble-outline';
|
|
50617
|
+
case 'comment':
|
|
50618
|
+
return 'chatbox-ellipses-outline';
|
|
50619
|
+
case 'reminder':
|
|
50620
|
+
return 'alarm-outline';
|
|
50621
|
+
case 'system':
|
|
50622
|
+
return 'cog-outline';
|
|
50623
|
+
case 'user':
|
|
50624
|
+
case 'mention':
|
|
50625
|
+
return 'person-outline';
|
|
50626
|
+
case 'document':
|
|
50627
|
+
return 'document-text-outline';
|
|
50628
|
+
default:
|
|
50629
|
+
return 'notifications-outline';
|
|
50630
|
+
}
|
|
50631
|
+
})();
|
|
50632
|
+
return { kind: 'ionicon', name, color: 'dark' };
|
|
50633
|
+
}
|
|
50634
|
+
ioniconName(n) {
|
|
50635
|
+
const r = this.resolveIcon(n);
|
|
50636
|
+
return r.kind === 'ionicon' ? r.name : '';
|
|
50637
|
+
}
|
|
50638
|
+
ioniconColor(n) {
|
|
50639
|
+
const r = this.resolveIcon(n);
|
|
50640
|
+
return r.kind === 'ionicon' ? r.color : undefined;
|
|
50641
|
+
}
|
|
50642
|
+
imageSrc(n) {
|
|
50643
|
+
const r = this.resolveIcon(n);
|
|
50644
|
+
return r.kind === 'image' || r.kind === 'avatar' ? r.src : '';
|
|
50645
|
+
}
|
|
50646
|
+
ionColor(token) {
|
|
50647
|
+
if (!token)
|
|
50648
|
+
return 'var(--ion-color-primary)';
|
|
50649
|
+
return `var(--ion-color-${token})`;
|
|
50650
|
+
}
|
|
50651
|
+
relativeTime(n) {
|
|
50652
|
+
const d = n.createdAt instanceof Date ? n.createdAt : null;
|
|
50653
|
+
if (!d)
|
|
50654
|
+
return '';
|
|
50655
|
+
const diffMs = Date.now() - d.getTime();
|
|
50656
|
+
const min = Math.floor(diffMs / 60000);
|
|
50657
|
+
if (min < 1)
|
|
50658
|
+
return this.tt('timeNow');
|
|
50659
|
+
if (min < 60)
|
|
50660
|
+
return this.tt('timeMinutes').replace('{n}', String(min));
|
|
50661
|
+
const h = Math.floor(min / 60);
|
|
50662
|
+
if (h < 24)
|
|
50663
|
+
return this.tt('timeHours').replace('{n}', String(h));
|
|
50664
|
+
const days = Math.floor(h / 24);
|
|
50665
|
+
return this.tt('timeDays').replace('{n}', String(days));
|
|
50666
|
+
}
|
|
50667
|
+
/** Devuelve label "Desde {appName}" si la notif viene de otra app, sino null. */
|
|
50668
|
+
showCrossAppBadge(n) {
|
|
50669
|
+
const currentApp = 'showcase';
|
|
50670
|
+
if (!n.appId || n.appId === currentApp)
|
|
50671
|
+
return null;
|
|
50672
|
+
return this.tt('crossApp').replace('{app}', n.appId);
|
|
50673
|
+
}
|
|
50674
|
+
// ---------------------------------------------------------------------------
|
|
50675
|
+
// Actions
|
|
50676
|
+
// ---------------------------------------------------------------------------
|
|
50677
|
+
async onOpen(n) {
|
|
50678
|
+
try {
|
|
50679
|
+
const result = await this.action.open(n);
|
|
50680
|
+
this.resolvedConfig().onNotificationOpen?.(n, result);
|
|
50681
|
+
}
|
|
50682
|
+
catch (err) {
|
|
50683
|
+
this.errors.handle(err, {
|
|
50684
|
+
context: 'notifications.open',
|
|
50685
|
+
fallbackKey: 'openError',
|
|
50686
|
+
i18nNamespace: this.ns,
|
|
50687
|
+
});
|
|
50688
|
+
}
|
|
50689
|
+
}
|
|
50690
|
+
async onMarkAll() {
|
|
50691
|
+
if (this._bulkBusy())
|
|
50692
|
+
return;
|
|
50693
|
+
this._bulkBusy.set(true);
|
|
50694
|
+
try {
|
|
50695
|
+
await this.notifs.markAllAsRead();
|
|
50696
|
+
this.toast.show({
|
|
50697
|
+
message: this.tt('markedAllSuccess'),
|
|
50698
|
+
color: 'dark',
|
|
50699
|
+
duration: 2500,
|
|
50700
|
+
position: 'top',
|
|
50701
|
+
});
|
|
50702
|
+
}
|
|
50703
|
+
catch (err) {
|
|
50704
|
+
this.errors.handle(err, {
|
|
50705
|
+
context: 'notifications.markAll',
|
|
50706
|
+
fallbackKey: 'markedAllError',
|
|
50707
|
+
i18nNamespace: this.ns,
|
|
50708
|
+
});
|
|
50709
|
+
}
|
|
50710
|
+
finally {
|
|
50711
|
+
this._bulkBusy.set(false);
|
|
50712
|
+
}
|
|
50713
|
+
}
|
|
50714
|
+
onLoadMore() {
|
|
50715
|
+
if (this._loadingMore())
|
|
50716
|
+
return;
|
|
50717
|
+
this._loadingMore.set(true);
|
|
50718
|
+
// Slice viene del cache local — no hay fetch real, pero un breve spinner
|
|
50719
|
+
// confirma al user que el click registró y suaviza la aparición de items.
|
|
50720
|
+
setTimeout(() => {
|
|
50721
|
+
this._pageSize.update(n => n + this.pageStep);
|
|
50722
|
+
this._loadingMore.set(false);
|
|
50723
|
+
}, 250);
|
|
50724
|
+
}
|
|
50725
|
+
onGoToSettings() {
|
|
50726
|
+
this.nav.navigateByUrl(this.resolvedConfig().settingsNotificationsPath);
|
|
50727
|
+
}
|
|
50728
|
+
// ---------------------------------------------------------------------------
|
|
50729
|
+
// Grouping helpers
|
|
50730
|
+
// ---------------------------------------------------------------------------
|
|
50731
|
+
bucketOf(n, now) {
|
|
50732
|
+
const d = n.createdAt instanceof Date ? n.createdAt : null;
|
|
50733
|
+
if (!d)
|
|
50734
|
+
return 'earlier';
|
|
50735
|
+
if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) {
|
|
50736
|
+
return 'today';
|
|
50737
|
+
}
|
|
50738
|
+
if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()) {
|
|
50739
|
+
return 'thisMonth';
|
|
50740
|
+
}
|
|
50741
|
+
return 'earlier';
|
|
50742
|
+
}
|
|
50743
|
+
tt(key) {
|
|
50744
|
+
return this.i18n.t(key, this.ns);
|
|
50745
|
+
}
|
|
50746
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationsViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
50747
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: NotificationsViewComponent, isStandalone: true, selector: "val-notifications-view", inputs: { config: "config" }, ngImport: i0, template: `
|
|
50748
|
+
<div class="page">
|
|
50749
|
+
<header class="page-header">
|
|
50750
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
50751
|
+
<val-title
|
|
50752
|
+
[props]="{
|
|
50753
|
+
size: 'large',
|
|
50754
|
+
color: 'dark',
|
|
50755
|
+
bold: false,
|
|
50756
|
+
content: pageDescription(),
|
|
50757
|
+
}"
|
|
50758
|
+
/>
|
|
50759
|
+
@if (showMarkAll()) {
|
|
50760
|
+
<div class="row-actions">
|
|
50761
|
+
<val-button [props]="markAllButtonProps()" (click)="onMarkAll()" />
|
|
50762
|
+
</div>
|
|
50763
|
+
}
|
|
50764
|
+
</header>
|
|
50765
|
+
|
|
50766
|
+
@if (resolvedConfig().showPushOffCta && !pushEnabled()) {
|
|
50767
|
+
<section class="notif-cta">
|
|
50768
|
+
<ion-icon class="notif-cta__icon" name="notifications-off-outline" aria-hidden="true" />
|
|
50769
|
+
<div class="notif-cta__body">
|
|
50770
|
+
<val-text
|
|
50771
|
+
[props]="{
|
|
50772
|
+
content: ctaTitle(),
|
|
50773
|
+
size: 'medium',
|
|
50774
|
+
color: 'dark',
|
|
50775
|
+
bold: true,
|
|
50776
|
+
}"
|
|
50777
|
+
/>
|
|
50778
|
+
<val-text
|
|
50779
|
+
[props]="{
|
|
50780
|
+
content: ctaHint(),
|
|
50781
|
+
size: 'small',
|
|
50782
|
+
color: 'medium',
|
|
50783
|
+
bold: false,
|
|
50784
|
+
}"
|
|
50785
|
+
/>
|
|
50786
|
+
<div class="row-actions">
|
|
50787
|
+
<val-button [props]="ctaButtonProps()" (click)="onGoToSettings()" />
|
|
50788
|
+
</div>
|
|
50789
|
+
</div>
|
|
50790
|
+
</section>
|
|
50791
|
+
}
|
|
50792
|
+
|
|
50793
|
+
@if (loading()) {
|
|
50794
|
+
<val-skeleton-layout [props]="{ preset: 'list', rows: 6 }" />
|
|
50795
|
+
} @else if (errorState()) {
|
|
50796
|
+
<val-empty-state [props]="errorState()!" />
|
|
50797
|
+
} @else if (allNotifs().length === 0) {
|
|
50798
|
+
<val-empty-state [props]="emptyStateProps()" />
|
|
50799
|
+
} @else {
|
|
50800
|
+
@for (group of groups(); track group.key) {
|
|
50801
|
+
<section class="group">
|
|
50802
|
+
<val-title
|
|
50803
|
+
[props]="{
|
|
50804
|
+
size: 'medium',
|
|
50805
|
+
color: 'dark',
|
|
50806
|
+
bold: true,
|
|
50807
|
+
content: group.label,
|
|
50808
|
+
}"
|
|
50809
|
+
/>
|
|
50810
|
+
<ul class="notif-list">
|
|
50811
|
+
@for (n of group.items; track n.id) {
|
|
50812
|
+
<li class="notif" [class.notif--unread]="!n.isRead">
|
|
50813
|
+
<div class="notif__icon">
|
|
50814
|
+
@switch (resolveIcon(n).kind) {
|
|
50815
|
+
@case ('ionicon') {
|
|
50816
|
+
<ion-icon
|
|
50817
|
+
[name]="ioniconName(n)"
|
|
50818
|
+
[style.color]="ionColor(ioniconColor(n))"
|
|
50819
|
+
aria-hidden="true"
|
|
50820
|
+
/>
|
|
50821
|
+
}
|
|
50822
|
+
@case ('image') {
|
|
50823
|
+
<img [src]="imageSrc(n)" alt="" loading="lazy" />
|
|
50824
|
+
}
|
|
50825
|
+
@case ('avatar') {
|
|
50826
|
+
<img class="notif__avatar" [src]="imageSrc(n)" alt="" loading="lazy" />
|
|
50827
|
+
}
|
|
50828
|
+
}
|
|
50829
|
+
</div>
|
|
50830
|
+
|
|
50831
|
+
<div class="notif__body">
|
|
50832
|
+
<div class="notif__head">
|
|
50833
|
+
<val-text
|
|
50834
|
+
[props]="{
|
|
50835
|
+
content: n.title || '—',
|
|
50836
|
+
size: 'medium',
|
|
50837
|
+
color: 'dark',
|
|
50838
|
+
bold: true,
|
|
50839
|
+
}"
|
|
50840
|
+
/>
|
|
50841
|
+
<span class="notif__time">{{ relativeTime(n) }}</span>
|
|
50842
|
+
</div>
|
|
50843
|
+
@if (n.body) {
|
|
50844
|
+
<p class="notif__text" [class.notif__text--clamped]="!isExpanded(n.id) && isLong(n.body)">
|
|
50845
|
+
{{ n.body }}
|
|
50846
|
+
</p>
|
|
50847
|
+
@if (isLong(n.body)) {
|
|
50848
|
+
<button class="notif__expand" type="button" (click)="toggleExpand(n.id)">
|
|
50849
|
+
{{ isExpanded(n.id) ? seeLess() : seeMore() }}
|
|
50850
|
+
</button>
|
|
50851
|
+
}
|
|
50852
|
+
}
|
|
50853
|
+
@if (showCrossAppBadge(n); as appName) {
|
|
50854
|
+
<span class="notif__app">{{ appName }}</span>
|
|
50855
|
+
}
|
|
50856
|
+
@if (n.actionRoute) {
|
|
50857
|
+
<div class="row-actions row-actions--end">
|
|
50858
|
+
<val-button [props]="openButtonProps()" (click)="onOpen(n)" />
|
|
50859
|
+
</div>
|
|
50860
|
+
}
|
|
50861
|
+
</div>
|
|
50862
|
+
</li>
|
|
50863
|
+
}
|
|
50864
|
+
</ul>
|
|
50865
|
+
</section>
|
|
50866
|
+
}
|
|
50867
|
+
|
|
50868
|
+
@if (hasMore()) {
|
|
50869
|
+
<div class="row-actions row-actions--center">
|
|
50870
|
+
<val-button [props]="loadMoreButtonProps()" (click)="onLoadMore()" />
|
|
50871
|
+
</div>
|
|
50872
|
+
}
|
|
50873
|
+
}
|
|
50874
|
+
</div>
|
|
50875
|
+
`, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.row-actions{margin-top:12px}.row-actions--center{display:flex;justify-content:center;margin-top:20px}.row-actions--end{display:flex;justify-content:flex-end;margin-top:8px}.notif-cta{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;margin-bottom:16px;border:1px solid var(--ion-color-light-shade, rgba(0, 0, 0, .08));border-radius:14px;background:var(--ion-color-light)}.notif-cta__icon{font-size:24px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0;margin-top:2px}.notif-cta__body{display:flex;flex-direction:column;gap:2px}.group{padding:12px 0}.group+.group{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.notif-list{list-style:none;padding:0;margin:12px 0 0;display:flex;flex-direction:column;gap:10px}.notif{display:grid;grid-template-columns:44px 1fr;gap:12px;padding:12px;border-radius:12px;background:transparent;transition:background .12s ease}.notif--unread{background:var(--ion-color-light, rgba(0, 0, 0, .04))}.notif__icon{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--ion-color-light, rgba(0, 0, 0, .06));overflow:hidden}.notif__icon ion-icon{font-size:22px}.notif__icon img{width:100%;height:100%;object-fit:cover}.notif__avatar{border-radius:50%}.notif__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif__head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}.notif__time{font-size:12px;color:var(--ion-color-medium, #92949c);flex-shrink:0}.notif__text{margin:0;font-size:14px;line-height:1.4;color:var(--ion-color-dark, #1a1a1a);white-space:pre-wrap}.notif__text--clamped{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notif__expand{background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-dark, #1a1a1a);cursor:pointer;align-self:flex-start;text-decoration:underline}.notif__app{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--ion-color-medium, #92949c);margin-top:2px}\n"], dependencies: [{ 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: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }] }); }
|
|
50876
|
+
}
|
|
50877
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NotificationsViewComponent, decorators: [{
|
|
50878
|
+
type: Component,
|
|
50879
|
+
args: [{ selector: 'val-notifications-view', standalone: true, imports: [
|
|
50880
|
+
IonIcon,
|
|
50881
|
+
DisplayComponent,
|
|
50882
|
+
TitleComponent,
|
|
50883
|
+
TextComponent,
|
|
50884
|
+
ButtonComponent,
|
|
50885
|
+
SkeletonLayoutComponent,
|
|
50886
|
+
EmptyStateComponent,
|
|
50887
|
+
], template: `
|
|
50888
|
+
<div class="page">
|
|
50889
|
+
<header class="page-header">
|
|
50890
|
+
<val-display [props]="{ size: 'small', color: 'dark', content: pageTitle() }" />
|
|
50891
|
+
<val-title
|
|
50892
|
+
[props]="{
|
|
50893
|
+
size: 'large',
|
|
50894
|
+
color: 'dark',
|
|
50895
|
+
bold: false,
|
|
50896
|
+
content: pageDescription(),
|
|
50897
|
+
}"
|
|
50898
|
+
/>
|
|
50899
|
+
@if (showMarkAll()) {
|
|
50900
|
+
<div class="row-actions">
|
|
50901
|
+
<val-button [props]="markAllButtonProps()" (click)="onMarkAll()" />
|
|
50902
|
+
</div>
|
|
50903
|
+
}
|
|
50904
|
+
</header>
|
|
50905
|
+
|
|
50906
|
+
@if (resolvedConfig().showPushOffCta && !pushEnabled()) {
|
|
50907
|
+
<section class="notif-cta">
|
|
50908
|
+
<ion-icon class="notif-cta__icon" name="notifications-off-outline" aria-hidden="true" />
|
|
50909
|
+
<div class="notif-cta__body">
|
|
50910
|
+
<val-text
|
|
50911
|
+
[props]="{
|
|
50912
|
+
content: ctaTitle(),
|
|
50913
|
+
size: 'medium',
|
|
50914
|
+
color: 'dark',
|
|
50915
|
+
bold: true,
|
|
50916
|
+
}"
|
|
50917
|
+
/>
|
|
50918
|
+
<val-text
|
|
50919
|
+
[props]="{
|
|
50920
|
+
content: ctaHint(),
|
|
50921
|
+
size: 'small',
|
|
50922
|
+
color: 'medium',
|
|
50923
|
+
bold: false,
|
|
50924
|
+
}"
|
|
50925
|
+
/>
|
|
50926
|
+
<div class="row-actions">
|
|
50927
|
+
<val-button [props]="ctaButtonProps()" (click)="onGoToSettings()" />
|
|
50928
|
+
</div>
|
|
50929
|
+
</div>
|
|
50930
|
+
</section>
|
|
50931
|
+
}
|
|
50932
|
+
|
|
50933
|
+
@if (loading()) {
|
|
50934
|
+
<val-skeleton-layout [props]="{ preset: 'list', rows: 6 }" />
|
|
50935
|
+
} @else if (errorState()) {
|
|
50936
|
+
<val-empty-state [props]="errorState()!" />
|
|
50937
|
+
} @else if (allNotifs().length === 0) {
|
|
50938
|
+
<val-empty-state [props]="emptyStateProps()" />
|
|
50939
|
+
} @else {
|
|
50940
|
+
@for (group of groups(); track group.key) {
|
|
50941
|
+
<section class="group">
|
|
50942
|
+
<val-title
|
|
50943
|
+
[props]="{
|
|
50944
|
+
size: 'medium',
|
|
50945
|
+
color: 'dark',
|
|
50946
|
+
bold: true,
|
|
50947
|
+
content: group.label,
|
|
50948
|
+
}"
|
|
50949
|
+
/>
|
|
50950
|
+
<ul class="notif-list">
|
|
50951
|
+
@for (n of group.items; track n.id) {
|
|
50952
|
+
<li class="notif" [class.notif--unread]="!n.isRead">
|
|
50953
|
+
<div class="notif__icon">
|
|
50954
|
+
@switch (resolveIcon(n).kind) {
|
|
50955
|
+
@case ('ionicon') {
|
|
50956
|
+
<ion-icon
|
|
50957
|
+
[name]="ioniconName(n)"
|
|
50958
|
+
[style.color]="ionColor(ioniconColor(n))"
|
|
50959
|
+
aria-hidden="true"
|
|
50960
|
+
/>
|
|
50961
|
+
}
|
|
50962
|
+
@case ('image') {
|
|
50963
|
+
<img [src]="imageSrc(n)" alt="" loading="lazy" />
|
|
50964
|
+
}
|
|
50965
|
+
@case ('avatar') {
|
|
50966
|
+
<img class="notif__avatar" [src]="imageSrc(n)" alt="" loading="lazy" />
|
|
50967
|
+
}
|
|
50968
|
+
}
|
|
50969
|
+
</div>
|
|
50970
|
+
|
|
50971
|
+
<div class="notif__body">
|
|
50972
|
+
<div class="notif__head">
|
|
50973
|
+
<val-text
|
|
50974
|
+
[props]="{
|
|
50975
|
+
content: n.title || '—',
|
|
50976
|
+
size: 'medium',
|
|
50977
|
+
color: 'dark',
|
|
50978
|
+
bold: true,
|
|
50979
|
+
}"
|
|
50980
|
+
/>
|
|
50981
|
+
<span class="notif__time">{{ relativeTime(n) }}</span>
|
|
50982
|
+
</div>
|
|
50983
|
+
@if (n.body) {
|
|
50984
|
+
<p class="notif__text" [class.notif__text--clamped]="!isExpanded(n.id) && isLong(n.body)">
|
|
50985
|
+
{{ n.body }}
|
|
50986
|
+
</p>
|
|
50987
|
+
@if (isLong(n.body)) {
|
|
50988
|
+
<button class="notif__expand" type="button" (click)="toggleExpand(n.id)">
|
|
50989
|
+
{{ isExpanded(n.id) ? seeLess() : seeMore() }}
|
|
50990
|
+
</button>
|
|
50991
|
+
}
|
|
50992
|
+
}
|
|
50993
|
+
@if (showCrossAppBadge(n); as appName) {
|
|
50994
|
+
<span class="notif__app">{{ appName }}</span>
|
|
50995
|
+
}
|
|
50996
|
+
@if (n.actionRoute) {
|
|
50997
|
+
<div class="row-actions row-actions--end">
|
|
50998
|
+
<val-button [props]="openButtonProps()" (click)="onOpen(n)" />
|
|
50999
|
+
</div>
|
|
51000
|
+
}
|
|
51001
|
+
</div>
|
|
51002
|
+
</li>
|
|
51003
|
+
}
|
|
51004
|
+
</ul>
|
|
51005
|
+
</section>
|
|
51006
|
+
}
|
|
51007
|
+
|
|
51008
|
+
@if (hasMore()) {
|
|
51009
|
+
<div class="row-actions row-actions--center">
|
|
51010
|
+
<val-button [props]="loadMoreButtonProps()" (click)="onLoadMore()" />
|
|
51011
|
+
</div>
|
|
51012
|
+
}
|
|
51013
|
+
}
|
|
51014
|
+
</div>
|
|
51015
|
+
`, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.row-actions{margin-top:12px}.row-actions--center{display:flex;justify-content:center;margin-top:20px}.row-actions--end{display:flex;justify-content:flex-end;margin-top:8px}.notif-cta{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;margin-bottom:16px;border:1px solid var(--ion-color-light-shade, rgba(0, 0, 0, .08));border-radius:14px;background:var(--ion-color-light)}.notif-cta__icon{font-size:24px;color:var(--ion-color-dark, #1a1a1a);flex-shrink:0;margin-top:2px}.notif-cta__body{display:flex;flex-direction:column;gap:2px}.group{padding:12px 0}.group+.group{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.notif-list{list-style:none;padding:0;margin:12px 0 0;display:flex;flex-direction:column;gap:10px}.notif{display:grid;grid-template-columns:44px 1fr;gap:12px;padding:12px;border-radius:12px;background:transparent;transition:background .12s ease}.notif--unread{background:var(--ion-color-light, rgba(0, 0, 0, .04))}.notif__icon{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--ion-color-light, rgba(0, 0, 0, .06));overflow:hidden}.notif__icon ion-icon{font-size:22px}.notif__icon img{width:100%;height:100%;object-fit:cover}.notif__avatar{border-radius:50%}.notif__body{display:flex;flex-direction:column;gap:4px;min-width:0}.notif__head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}.notif__time{font-size:12px;color:var(--ion-color-medium, #92949c);flex-shrink:0}.notif__text{margin:0;font-size:14px;line-height:1.4;color:var(--ion-color-dark, #1a1a1a);white-space:pre-wrap}.notif__text--clamped{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notif__expand{background:none;border:0;padding:0;font-size:13px;color:var(--ion-color-dark, #1a1a1a);cursor:pointer;align-self:flex-start;text-decoration:underline}.notif__app{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--ion-color-medium, #92949c);margin-top:2px}\n"] }]
|
|
51016
|
+
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
51017
|
+
type: Input
|
|
51018
|
+
}] } });
|
|
51019
|
+
|
|
51020
|
+
/**
|
|
51021
|
+
* Helper para montar el inbox de notificaciones (`val-notifications-view`) como
|
|
51022
|
+
* ruta en una app del factory. El `config` se pasa por route `data`
|
|
51023
|
+
* (`notificationsConfig`) y el componente lo lee como fallback de su
|
|
51024
|
+
* `@Input() config`.
|
|
51025
|
+
*
|
|
51026
|
+
* @example
|
|
51027
|
+
* ```ts
|
|
51028
|
+
* // app.routes.ts
|
|
51029
|
+
* export const appRoutes: Routes = [
|
|
51030
|
+
* ...provideValtechNotificationsRoutes(), // → /app/notifications
|
|
51031
|
+
* { path: 'settings', loadChildren: () => ... },
|
|
51032
|
+
* ];
|
|
51033
|
+
*
|
|
51034
|
+
* // con config acotada:
|
|
51035
|
+
* ...provideValtechNotificationsRoutes({ config: { enableMarkAll: false } }),
|
|
51036
|
+
* ...provideValtechNotificationsRoutes({
|
|
51037
|
+
* config: { settingsNotificationsPath: '/app/preferences/push' },
|
|
51038
|
+
* }),
|
|
51039
|
+
* ```
|
|
51040
|
+
*/
|
|
51041
|
+
function provideValtechNotificationsRoutes(opts) {
|
|
51042
|
+
return [
|
|
51043
|
+
{
|
|
51044
|
+
path: opts?.path ?? 'notifications',
|
|
51045
|
+
component: NotificationsViewComponent,
|
|
51046
|
+
data: { notificationsConfig: opts?.config },
|
|
51047
|
+
},
|
|
51048
|
+
];
|
|
51049
|
+
}
|
|
51050
|
+
|
|
50101
51051
|
/** Built-in label sets for the three platform locales. */
|
|
50102
51052
|
const CALLOUT_LABELS = {
|
|
50103
51053
|
es: {
|
|
@@ -52140,124 +53090,6 @@ function beautifyLegalArticle(article) {
|
|
|
52140
53090
|
* Cliente NUNCA escribe directo — todas las mutaciones via PUT /v2/auth/preferences.
|
|
52141
53091
|
*/
|
|
52142
53092
|
|
|
52143
|
-
/**
|
|
52144
|
-
* Refreshable Stream
|
|
52145
|
-
*
|
|
52146
|
-
* Helper estándar para vistas que consumen datos de Firestore. Soporta DOS
|
|
52147
|
-
* modos — el caller decide cuál vía la `factory`:
|
|
52148
|
-
*
|
|
52149
|
-
* - **one-shot** (recomendado por defecto): `() => from(svc.getAllOnce())`.
|
|
52150
|
-
* Carga una vez y completa. Más barato — sin listener vivo.
|
|
52151
|
-
* - **real-time**: `() => svc.getAll()`. Listener Firestore que auto-actualiza.
|
|
52152
|
-
* Solo donde importa (inbox de notificaciones, badges).
|
|
52153
|
-
*
|
|
52154
|
-
* En ambos modos la primera suscripción se gatea por `firebaseAuthReady`
|
|
52155
|
-
* (sesión de Firebase Auth lista) — así el listener/lectura nunca se adjunta
|
|
52156
|
-
* antes de que `request.auth` esté disponible en las reglas de Firestore, lo
|
|
52157
|
-
* que cierra la ventana de `permission-denied` en cold start de PWA.
|
|
52158
|
-
*
|
|
52159
|
-
* El `retry` con backoff corto es solo red de seguridad para reconexiones
|
|
52160
|
-
* transitorias (no para cubrir cold start — eso lo cubre el gate de auth).
|
|
52161
|
-
*/
|
|
52162
|
-
/**
|
|
52163
|
-
* Crea un stream refrescable a partir de una factory de Observable.
|
|
52164
|
-
*
|
|
52165
|
-
* DEBE llamarse en un injection context (field initializer o constructor) —
|
|
52166
|
-
* usa `toSignal`/`toObservable`/`inject` internamente.
|
|
52167
|
-
*
|
|
52168
|
-
* @param factory - Función que produce el Observable a consumir. El caller
|
|
52169
|
-
* decide el modo: `() => from(svc.getAllOnce())` (one-shot) o
|
|
52170
|
-
* `() => svc.getAll()` (real-time).
|
|
52171
|
-
* @param options - Configuración opcional (fallback, retry, gate).
|
|
52172
|
-
*
|
|
52173
|
-
* @example
|
|
52174
|
-
* ```typescript
|
|
52175
|
-
* // one-shot (recomendado por defecto)
|
|
52176
|
-
* private readonly stream = createRefreshableStream(
|
|
52177
|
-
* () => from(this.svc.getAllOnce()),
|
|
52178
|
-
* );
|
|
52179
|
-
*
|
|
52180
|
-
* // real-time (inbox)
|
|
52181
|
-
* private readonly stream = createRefreshableStream(
|
|
52182
|
-
* () => this.notifs.getAll(50),
|
|
52183
|
-
* );
|
|
52184
|
-
*
|
|
52185
|
-
* readonly items = this.stream.data;
|
|
52186
|
-
* readonly loading = this.stream.loading;
|
|
52187
|
-
*
|
|
52188
|
-
* ionViewWillEnter() {
|
|
52189
|
-
* this.pageRefresh.register(() => this.stream.reload());
|
|
52190
|
-
* }
|
|
52191
|
-
* ```
|
|
52192
|
-
*/
|
|
52193
|
-
function createRefreshableStream(factory, options) {
|
|
52194
|
-
const injector = options?.injector ?? inject(Injector);
|
|
52195
|
-
const fallback = (options?.fallback ?? null);
|
|
52196
|
-
const retryCount = options?.retryCount ?? 4;
|
|
52197
|
-
const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
|
|
52198
|
-
const gateOnFirebaseAuth = options?.gateOnFirebaseAuth ?? true;
|
|
52199
|
-
const authGateTimeoutMs = options?.authGateTimeoutMs ?? 20000;
|
|
52200
|
-
const firstEmitTimeoutMs = options?.firstEmitTimeoutMs ?? 20000;
|
|
52201
|
-
// FirebaseService es opcional — sin él (apps sin Firebase) no se gatea.
|
|
52202
|
-
const firebase = gateOnFirebaseAuth ? injector.get(FirebaseService, null) : null;
|
|
52203
|
-
const _reload = signal(0);
|
|
52204
|
-
const _loading = signal(true);
|
|
52205
|
-
const _error = signal(false);
|
|
52206
|
-
// Gate de Firebase Auth: el stream no abre la primera suscripción hasta que
|
|
52207
|
-
// la sesión esté confirmada. `race` con un `timer` de seguridad — si la
|
|
52208
|
-
// sesión NUNCA se establece (token inválido, signInWithFirebase falló), el
|
|
52209
|
-
// gate igual se libera tras `authGateTimeoutMs` y la factory procede: si no
|
|
52210
|
-
// hay permiso terminará en estado `error`, jamás en skeleton infinito.
|
|
52211
|
-
// Tag los dos inputs del `race` para distinguir en logs cuál ganó: la sesión
|
|
52212
|
-
// de Firebase Auth (`auth-ready`) o el timeout de seguridad (`timeout`).
|
|
52213
|
-
const authGate$ = firebase
|
|
52214
|
-
? race(firebase.firebaseAuthReady$.pipe(map$1(() => 'auth-ready')), timer(authGateTimeoutMs).pipe(map$1(() => 'timeout'))).pipe(take(1), tap(winner => {
|
|
52215
|
-
if (winner === 'timeout') {
|
|
52216
|
-
console.warn(`[RefreshStream] auth gate resolved by TIMEOUT after ${authGateTimeoutMs}ms ` +
|
|
52217
|
-
'(Firebase Auth session never confirmed)');
|
|
52218
|
-
}
|
|
52219
|
-
else {
|
|
52220
|
-
console.log('[RefreshStream] auth gate resolved by firebaseAuthReady$ (session ready)');
|
|
52221
|
-
}
|
|
52222
|
-
}))
|
|
52223
|
-
: of(true);
|
|
52224
|
-
const data = toSignal(toObservable(_reload, { injector }).pipe(tap(() => {
|
|
52225
|
-
_loading.set(true);
|
|
52226
|
-
_error.set(false);
|
|
52227
|
-
}),
|
|
52228
|
-
// Esperar a Firebase Auth listo antes de cada (re)suscripción. Como
|
|
52229
|
-
// `firebaseAuthReady$` completa tras la primera emisión, las reloads
|
|
52230
|
-
// posteriores resuelven el gate de inmediato (auth ya está listo).
|
|
52231
|
-
switchMap(() => authGate$.pipe(switchMap(() => factory().pipe(tap(() => {
|
|
52232
|
-
_loading.set(false);
|
|
52233
|
-
_error.set(false);
|
|
52234
|
-
console.log('[RefreshStream] factory emitted first value — loading=false');
|
|
52235
|
-
}),
|
|
52236
|
-
// Backoff corto exponencial acotado — solo red de seguridad para
|
|
52237
|
-
// reconexiones transitorias, NO para cubrir cold start.
|
|
52238
|
-
retry({
|
|
52239
|
-
count: retryCount,
|
|
52240
|
-
delay: (_err, attempt) => timer(Math.min(retryBaseDelayMs * 2 ** (attempt - 1), 4000)),
|
|
52241
|
-
}),
|
|
52242
|
-
// Techo absoluto para la PRIMERA emisión. Si la factory no emite
|
|
52243
|
-
// ni valor ni error (servicio nunca inicializado → `EMPTY` →
|
|
52244
|
-
// silencio), `timeout` dispara un error → `catchError` → estado
|
|
52245
|
-
// `error`. Nunca skeleton perpetuo. Va DESPUÉS de `retry` para
|
|
52246
|
-
// que cuente el tiempo total, no por intento.
|
|
52247
|
-
timeout({ first: firstEmitTimeoutMs }), catchError(err => {
|
|
52248
|
-
_loading.set(false);
|
|
52249
|
-
_error.set(true);
|
|
52250
|
-
console.error(`[RefreshStream] factory failed/timed out → error state: ${err instanceof Error ? err.message : String(err)}`);
|
|
52251
|
-
return of(fallback);
|
|
52252
|
-
})))))), { initialValue: null, injector });
|
|
52253
|
-
return {
|
|
52254
|
-
data,
|
|
52255
|
-
loading: _loading.asReadonly(),
|
|
52256
|
-
error: _error.asReadonly(),
|
|
52257
|
-
reload: () => _reload.update(n => n + 1),
|
|
52258
|
-
};
|
|
52259
|
-
}
|
|
52260
|
-
|
|
52261
53093
|
/**
|
|
52262
53094
|
* Refreshable Stream
|
|
52263
53095
|
*
|
|
@@ -60422,5 +61254,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
60422
61254
|
* Generated bundle index. Do not edit.
|
|
60423
61255
|
*/
|
|
60424
61256
|
|
|
60425
|
-
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, AccountViewComponent, 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, CreateOrgModalComponent, 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, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, 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, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, 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, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, 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, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_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, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechOrganizationRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
61257
|
+
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, AccountViewComponent, 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, CreateOrgModalComponent, 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, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, 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, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, 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, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_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, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
60426
61258
|
//# sourceMappingURL=valtech-components.mjs.map
|