valtech-components 2.0.1007 → 2.0.1009

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/esm2022/lib/components/molecules/comment/comment.component.mjs +3 -3
  2. package/esm2022/lib/components/organisms/account-view/account-view.component.mjs +2 -2
  3. package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +3 -141
  4. package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +1 -21
  5. package/esm2022/lib/components/organisms/profile-view/profile-view.component.mjs +6 -16
  6. package/esm2022/lib/components/templates/auth-background/auth-background.component.mjs +55 -23
  7. package/esm2022/lib/services/auth/auth.service.mjs +1 -14
  8. package/esm2022/lib/services/auth/types.mjs +1 -1
  9. package/esm2022/lib/services/org/org.service.mjs +1 -8
  10. package/esm2022/lib/services/org/types.mjs +1 -1
  11. package/esm2022/lib/services/preferences/preferences.service.mjs +36 -11
  12. package/esm2022/lib/version.mjs +2 -2
  13. package/esm2022/public-api.mjs +1 -7
  14. package/fesm2022/valtech-components.mjs +407 -1912
  15. package/fesm2022/valtech-components.mjs.map +1 -1
  16. package/lib/components/organisms/organization-view/organization-view.component.d.ts +0 -6
  17. package/lib/components/templates/auth-background/auth-background.component.d.ts +6 -2
  18. package/lib/services/auth/auth.service.d.ts +1 -7
  19. package/lib/services/auth/types.d.ts +0 -23
  20. package/lib/services/org/org.service.d.ts +1 -6
  21. package/lib/services/org/types.d.ts +0 -30
  22. package/lib/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/public-api.d.ts +1 -8
  25. package/esm2022/lib/components/organisms/about-view/about-view.component.mjs +0 -436
  26. package/esm2022/lib/components/organisms/about-view/about-view.i18n.mjs +0 -61
  27. package/esm2022/lib/components/organisms/about-view/about.routes.mjs +0 -36
  28. package/esm2022/lib/components/organisms/about-view/types.mjs +0 -19
  29. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.component.mjs +0 -385
  30. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.mjs +0 -63
  31. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.component.mjs +0 -313
  32. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.i18n.mjs +0 -63
  33. package/esm2022/lib/services/apikeys/api-keys.service.mjs +0 -46
  34. package/esm2022/lib/services/apikeys/types.mjs +0 -7
  35. package/lib/components/organisms/about-view/about-view.component.d.ts +0 -88
  36. package/lib/components/organisms/about-view/about-view.i18n.d.ts +0 -14
  37. package/lib/components/organisms/about-view/about.routes.d.ts +0 -31
  38. package/lib/components/organisms/about-view/types.d.ts +0 -76
  39. package/lib/components/organisms/api-keys-modal/api-keys-modal.component.d.ts +0 -44
  40. package/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.d.ts +0 -6
  41. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +0 -47
  42. package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +0 -6
  43. package/lib/services/apikeys/api-keys.service.d.ts +0 -25
  44. package/lib/services/apikeys/types.d.ts +0 -46
@@ -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, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, 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, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, 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.1007';
57
+ const VERSION = '2.0.1009';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -8658,19 +8658,6 @@ 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
- }
8674
8661
  /**
8675
8662
  * Cambia la contraseña del usuario autenticado.
8676
8663
  * Requiere la contraseña actual para verificación.
@@ -21670,7 +21657,7 @@ class CommentComponent {
21670
21657
  </div>
21671
21658
  }
21672
21659
  </div>
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"] }] }); }
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"] }] }); }
21674
21661
  }
21675
21662
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CommentComponent, decorators: [{
21676
21663
  type: Component,
@@ -21860,7 +21847,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
21860
21847
  </div>
21861
21848
  }
21862
21849
  </div>
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"] }]
21850
+ `, 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"] }]
21864
21851
  }], propDecorators: { props: [{
21865
21852
  type: Input
21866
21853
  }], authorClick: [{
@@ -41750,7 +41737,7 @@ const PROFILE_VIEW_I18N = {
41750
41737
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41751
41738
  const HANDLE_MIN = 3;
41752
41739
  const HANDLE_MAX = 20;
41753
- const DEFAULT_NAMESPACE$i = 'Settings.Profile';
41740
+ const DEFAULT_NAMESPACE$f = 'Settings.Profile';
41754
41741
  /**
41755
41742
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41756
41743
  *
@@ -41800,7 +41787,7 @@ class ProfileViewComponent {
41800
41787
  showPhone: merged.showPhone ?? true,
41801
41788
  showShareCta: merged.showShareCta ?? true,
41802
41789
  profileBaseUrl: merged.profileBaseUrl ?? '',
41803
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$i,
41790
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
41804
41791
  onSaved: merged.onSaved,
41805
41792
  onAvatarUploaded: merged.onAvatarUploaded,
41806
41793
  };
@@ -42276,17 +42263,12 @@ class ProfileViewComponent {
42276
42263
 
42277
42264
  <val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
42278
42265
 
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
- }
42266
+ <!-- Card de compartir + modal QR removidos temporalmente (se retoman luego).
42267
+ Wiring intacto: shareCta(), shareModal(), shareOpen, showShareCta. -->
42284
42268
  }
42285
42269
  }
42286
42270
  </div>
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"] }] }); }
42271
+ `, 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"] }] }); }
42290
42272
  }
42291
42273
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProfileViewComponent, decorators: [{
42292
42274
  type: Component,
@@ -42362,16 +42344,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
42362
42344
 
42363
42345
  <val-form #form [props]="formMeta" (onSubmit)="onSubmit($event)" />
42364
42346
 
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
- }
42347
+ <!-- Card de compartir + modal QR removidos temporalmente (se retoman luego).
42348
+ Wiring intacto: shareCta(), shareModal(), shareOpen, showShareCta. -->
42370
42349
  }
42371
42350
  }
42372
42351
  </div>
42373
-
42374
- <val-share-profile-modal [isOpen]="shareOpen()" [props]="shareModal()" (dismissed)="shareOpen.set(false)" />
42375
42352
  `, 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"] }]
42376
42353
  }], ctorParameters: () => [], propDecorators: { config: [{
42377
42354
  type: Input
@@ -42502,6 +42479,21 @@ class PreferencesService {
42502
42479
  this._updateInFlight = false;
42503
42480
  const destroyRef = inject(DestroyRef);
42504
42481
  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
+ // Mismo seed para theme — ThemeService aplica su valor de localStorage en
42492
+ // bootstrap (`currentOption`). Necesario para desgatear el effect de theme
42493
+ // (abajo) sin que el default 'auto' pise el tema restaurado.
42494
+ if (this.themeService) {
42495
+ this._theme.set(this.themeService.currentOption);
42496
+ }
42505
42497
  // Auto-bind al user actual. effect() corre en injection context (constructor OK).
42506
42498
  // allowSignalWrites: bindToUser/unbind escriben `_synced` (señal interna sin
42507
42499
  // dependencias del propio effect) → seguro y necesario.
@@ -42514,14 +42506,18 @@ class PreferencesService {
42514
42506
  this.unbind();
42515
42507
  }
42516
42508
  }, { allowSignalWrites: true });
42517
- // Side-effect: aplicar theme local cuando llega snapshot real.
42518
- // OJO: usar `toggleUserPreference()` y NO el setter `Theme =`. El setter
42519
- // solo persiste localStorage + emite el BehaviorSubject; no aplica las
42520
- // clases `body.dark`/`body.light` que el palette `dark.class.css` de
42521
- // Ionic necesita para conmutar el tema visible.
42509
+ // Side-effect: aplicar theme. SIN gate `_synced` (local-first, igual que
42510
+ // fontSize): ThemeService/localStorage es la verdad del dispositivo y
42511
+ // `_theme` viene seedeado arriba. Aplicar siempre que cambie da apply
42512
+ // optimista al instante; el sync cross-device también aplica vía snapshot.
42513
+ // Gatearlo en `_synced` hacía que el primer cambio de un user nuevo (sin doc
42514
+ // Firestore aún) no se aplicara: el selector mostraba "Dark" pero el tema no
42515
+ // cambiaba. (El selector de idioma no sufre esto porque val-language-selector
42516
+ // llama i18n.setLanguage directo; el de theme depende solo de este effect.)
42517
+ // OJO: usar `toggleUserPreference()` y NO el setter `Theme =`. El setter solo
42518
+ // persiste localStorage + emite el BehaviorSubject; no aplica las clases
42519
+ // `body.dark`/`html.ion-palette-dark` que el palette de Ionic necesita.
42522
42520
  effect(() => {
42523
- if (!this._synced())
42524
- return;
42525
42521
  const t = this._theme();
42526
42522
  if (this.themeService) {
42527
42523
  this.themeService.toggleUserPreference(t);
@@ -42536,11 +42532,17 @@ class PreferencesService {
42536
42532
  this.i18n.setLanguage(l);
42537
42533
  }
42538
42534
  });
42539
- // Side-effect: aplicar fontSize local cuando llega snapshot real.
42535
+ // Side-effect: aplicar fontSize.
42536
+ // SIN gate `_synced` (igual que theme): el tamaño es una
42537
+ // preferencia local-first — FontSizeService/localStorage es la verdad del
42538
+ // dispositivo y `_fontSize` ya viene seedeado de ahí arriba. Aplicar siempre
42539
+ // que cambie permite que el cambio del usuario surta efecto al instante
42540
+ // (optimista, sin esperar el round-trip a Firestore) y que el sync
42541
+ // cross-device también aplique al llegar el snapshot. Gatearlo en `_synced`
42542
+ // hacía que el primer cambio de un user nuevo (sin doc Firestore aún) no se
42543
+ // aplicara: el selector mostraba "Grande" pero el font-size no cambiaba.
42540
42544
  // allowSignalWrites: fontSizeService.setSize() escribe this._size (signal).
42541
42545
  effect(() => {
42542
- if (!this._synced())
42543
- return;
42544
42546
  const fs = this._fontSize();
42545
42547
  if (this.fontSizeService) {
42546
42548
  this.fontSizeService.setSize(fs);
@@ -42716,7 +42718,7 @@ const PREFERENCES_VIEW_I18N = {
42716
42718
  },
42717
42719
  };
42718
42720
 
42719
- const DEFAULT_NAMESPACE$h = 'Settings.General';
42721
+ const DEFAULT_NAMESPACE$e = 'Settings.General';
42720
42722
  const DEFAULT_LANGUAGES = ['es', 'en'];
42721
42723
  /**
42722
42724
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42753,7 +42755,7 @@ class PreferencesViewComponent {
42753
42755
  showLanguage: merged.showLanguage ?? true,
42754
42756
  showFontSize: merged.showFontSize ?? true,
42755
42757
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42756
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$h,
42758
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
42757
42759
  onThemeChanged: merged.onThemeChanged,
42758
42760
  onLanguageChanged: merged.onLanguageChanged,
42759
42761
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -44509,7 +44511,7 @@ const SECURITY_VIEW_I18N = {
44509
44511
  };
44510
44512
 
44511
44513
  addIcons({ laptopOutline, phonePortraitOutline });
44512
- const DEFAULT_NAMESPACE$g = 'Settings.Security';
44514
+ const DEFAULT_NAMESPACE$d = 'Settings.Security';
44513
44515
  /**
44514
44516
  * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44515
44517
  *
@@ -44558,7 +44560,7 @@ class SecurityViewComponent {
44558
44560
  showChangeEmail: merged.showChangeEmail ?? true,
44559
44561
  showMfa: merged.showMfa ?? true,
44560
44562
  showPolicies: merged.showPolicies ?? true,
44561
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$g,
44563
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
44562
44564
  handleDeepLinks: merged.handleDeepLinks ?? true,
44563
44565
  homeRoute: merged.homeRoute ?? '/app/home',
44564
44566
  onMfaCompleted: merged.onMfaCompleted,
@@ -45360,13 +45362,6 @@ class OrgService {
45360
45362
  inviteUser(orgId, req) {
45361
45363
  return this.http.post(`${this.baseUrl}/${orgId}/invite`, req);
45362
45364
  }
45363
- /**
45364
- * Crea/asigna miembros en masa (ADR-023). Resultado por-fila (partial success):
45365
- * `created` (pre-aprovisionado), `assigned` (ya existía), `skipped`, `error`.
45366
- */
45367
- importMembers(orgId, req) {
45368
- return this.http.post(`${this.baseUrl}/${orgId}/members/import`, req);
45369
- }
45370
45365
  leaveOrg(orgId) {
45371
45366
  return this.http.post(`${this.baseUrl}/${orgId}/leave`, {});
45372
45367
  }
@@ -45471,7 +45466,7 @@ const CREATE_ORG_MODAL_I18N = {
45471
45466
  },
45472
45467
  };
45473
45468
 
45474
- const DEFAULT_NAMESPACE$f = 'CreateOrgModal';
45469
+ const DEFAULT_NAMESPACE$c = 'CreateOrgModal';
45475
45470
  /**
45476
45471
  * `val-create-org-modal` — modal de creación de organización (organism
45477
45472
  * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
@@ -45497,10 +45492,10 @@ class CreateOrgModalComponent {
45497
45492
  this.toast = inject(ToastService);
45498
45493
  this.errors = inject(ValtechErrorService);
45499
45494
  /** Namespace i18n con que la vista resuelve sus textos. */
45500
- this.i18nNamespace = DEFAULT_NAMESPACE$f;
45495
+ this.i18nNamespace = DEFAULT_NAMESPACE$c;
45501
45496
  this._busy = signal(false);
45502
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$f)) {
45503
- this.i18n.registerContent(DEFAULT_NAMESPACE$f, CREATE_ORG_MODAL_I18N);
45497
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
45498
+ this.i18n.registerContent(DEFAULT_NAMESPACE$c, CREATE_ORG_MODAL_I18N);
45504
45499
  }
45505
45500
  this.formMeta = signal(this.buildFormMeta());
45506
45501
  effect(() => {
@@ -45736,7 +45731,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
45736
45731
  },
45737
45732
  };
45738
45733
 
45739
- const DEFAULT_NAMESPACE$e = 'Settings.DeleteAccount';
45734
+ const DEFAULT_NAMESPACE$b = 'Settings.DeleteAccount';
45740
45735
  /**
45741
45736
  * `val-delete-account-modal` — modal de eliminación de cuenta (organism,
45742
45737
  * cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -45762,7 +45757,7 @@ class DeleteAccountModalComponent {
45762
45757
  this.i18n = inject(I18nService);
45763
45758
  this.toast = inject(ToastService);
45764
45759
  /** Namespace i18n con que la vista resuelve sus textos. */
45765
- this.i18nNamespace = DEFAULT_NAMESPACE$e;
45760
+ this.i18nNamespace = DEFAULT_NAMESPACE$b;
45766
45761
  this.hasPassword = signal(null);
45767
45762
  this.codeSent = signal(false);
45768
45763
  this.formState = signal(ComponentStates.ENABLED);
@@ -45829,8 +45824,8 @@ class DeleteAccountModalComponent {
45829
45824
  ],
45830
45825
  actions: this.submitBtn('delete-code-submit', 'confirmCta'),
45831
45826
  }));
45832
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$e)) {
45833
- this.i18n.registerContent(DEFAULT_NAMESPACE$e, DELETE_ACCOUNT_MODAL_I18N);
45827
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45828
+ this.i18n.registerContent(DEFAULT_NAMESPACE$b, DELETE_ACCOUNT_MODAL_I18N);
45834
45829
  }
45835
45830
  }
45836
45831
  ngOnInit() {
@@ -46031,7 +46026,7 @@ const ORG_INFO_SHEET_I18N = {
46031
46026
  },
46032
46027
  };
46033
46028
 
46034
- const DEFAULT_NAMESPACE$d = 'OrgInfoSheet';
46029
+ const DEFAULT_NAMESPACE$a = 'OrgInfoSheet';
46035
46030
  /**
46036
46031
  * `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
46037
46032
  * Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
@@ -46076,15 +46071,15 @@ class OrgInfoSheetComponent {
46076
46071
  text: this.closeProps.text || this.resolvedCloseLabel(),
46077
46072
  }));
46078
46073
  addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
46079
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$d)) {
46080
- this.i18n.registerContent(DEFAULT_NAMESPACE$d, ORG_INFO_SHEET_I18N);
46074
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
46075
+ this.i18n.registerContent(DEFAULT_NAMESPACE$a, ORG_INFO_SHEET_I18N);
46081
46076
  }
46082
46077
  }
46083
46078
  dismiss() {
46084
46079
  this._modalRef?.dismiss(null, 'cancel');
46085
46080
  }
46086
46081
  tt(key) {
46087
- return this.i18n.t(key, DEFAULT_NAMESPACE$d);
46082
+ return this.i18n.t(key, DEFAULT_NAMESPACE$a);
46088
46083
  }
46089
46084
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46090
46085
  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: `
@@ -46251,7 +46246,7 @@ const SWITCH_ORG_MODAL_I18N = {
46251
46246
  },
46252
46247
  };
46253
46248
 
46254
- const DEFAULT_NAMESPACE$c = 'Settings.SwitchOrg';
46249
+ const DEFAULT_NAMESPACE$9 = 'Settings.SwitchOrg';
46255
46250
  addIcons({ businessOutline, checkmarkCircleOutline });
46256
46251
  /**
46257
46252
  * `val-switch-org-modal` — modal de cambio de organización activa (organism
@@ -46277,7 +46272,7 @@ class SwitchOrgModalComponent {
46277
46272
  this.orgSwitch = inject(OrgSwitchService);
46278
46273
  this.errors = inject(ValtechErrorService);
46279
46274
  /** Namespace i18n con que la vista resuelve sus textos. */
46280
- this.i18nNamespace = DEFAULT_NAMESPACE$c;
46275
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
46281
46276
  this.orgs = signal([]);
46282
46277
  this.loading = signal(false);
46283
46278
  this.query = signal('');
@@ -46293,8 +46288,8 @@ class SwitchOrgModalComponent {
46293
46288
  return list;
46294
46289
  return list.filter(o => o.name.toLowerCase().includes(q));
46295
46290
  });
46296
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
46297
- this.i18n.registerContent(DEFAULT_NAMESPACE$c, SWITCH_ORG_MODAL_I18N);
46291
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46292
+ this.i18n.registerContent(DEFAULT_NAMESPACE$9, SWITCH_ORG_MODAL_I18N);
46298
46293
  }
46299
46294
  this.loadOrgs();
46300
46295
  }
@@ -46612,7 +46607,7 @@ const ACCOUNT_VIEW_I18N = {
46612
46607
  };
46613
46608
 
46614
46609
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
46615
- const DEFAULT_NAMESPACE$b = 'Settings.Account';
46610
+ const DEFAULT_NAMESPACE$8 = 'Settings.Account';
46616
46611
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
46617
46612
  /**
46618
46613
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -46653,7 +46648,7 @@ class AccountViewComponent {
46653
46648
  showNewOrgCta: merged.showNewOrgCta ?? true,
46654
46649
  showLogout: merged.showLogout ?? true,
46655
46650
  showDeleteAccount: merged.showDeleteAccount ?? true,
46656
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$b,
46651
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$8,
46657
46652
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
46658
46653
  onOrgSwitched: merged.onOrgSwitched,
46659
46654
  onOrgCreated: merged.onOrgCreated,
@@ -46705,7 +46700,7 @@ class AccountViewComponent {
46705
46700
  {
46706
46701
  text: this.tt('orgsNewCta'),
46707
46702
  color: 'primary',
46708
- fill: 'outline',
46703
+ fill: 'solid',
46709
46704
  shape: 'round',
46710
46705
  size: 'default',
46711
46706
  type: 'button',
@@ -47378,7 +47373,7 @@ const EDIT_ORG_MODAL_I18N = {
47378
47373
  },
47379
47374
  };
47380
47375
 
47381
- const DEFAULT_NAMESPACE$a = 'EditOrgModal';
47376
+ const DEFAULT_NAMESPACE$7 = 'EditOrgModal';
47382
47377
  /**
47383
47378
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
47384
47379
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -47405,10 +47400,10 @@ class EditOrgModalComponent {
47405
47400
  /** Organización a editar (pasada por `componentProps`). */
47406
47401
  this.org = null;
47407
47402
  /** Namespace i18n con que la vista resuelve sus textos. */
47408
- this.i18nNamespace = DEFAULT_NAMESPACE$a;
47403
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
47409
47404
  this.saving = signal(false);
47410
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
47411
- this.i18n.registerContent(DEFAULT_NAMESPACE$a, EDIT_ORG_MODAL_I18N);
47405
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
47406
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, EDIT_ORG_MODAL_I18N);
47412
47407
  }
47413
47408
  this.formMeta = signal(this.buildFormMeta());
47414
47409
  effect(() => {
@@ -47646,7 +47641,7 @@ const INVITE_MEMBER_MODAL_I18N = {
47646
47641
  },
47647
47642
  };
47648
47643
 
47649
- const DEFAULT_NAMESPACE$9 = 'InviteModal';
47644
+ const DEFAULT_NAMESPACE$6 = 'InviteModal';
47650
47645
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
47651
47646
  /**
47652
47647
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -47677,7 +47672,7 @@ class InviteMemberModalComponent {
47677
47672
  /** Ids de miembros ya existentes (excluidos de los resultados). */
47678
47673
  this.existingMemberIds = [];
47679
47674
  /** Namespace i18n con que la vista resuelve sus textos. */
47680
- this.i18nNamespace = DEFAULT_NAMESPACE$9;
47675
+ this.i18nNamespace = DEFAULT_NAMESPACE$6;
47681
47676
  this.query = signal('');
47682
47677
  this.searching = signal(false);
47683
47678
  this.searchResults = signal([]);
@@ -47724,8 +47719,8 @@ class InviteMemberModalComponent {
47724
47719
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
47725
47720
  }));
47726
47721
  this.searchSubject = new Subject();
47727
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
47728
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, INVITE_MEMBER_MODAL_I18N);
47722
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47723
+ this.i18n.registerContent(DEFAULT_NAMESPACE$6, INVITE_MEMBER_MODAL_I18N);
47729
47724
  }
47730
47725
  this.loadRoles();
47731
47726
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
@@ -48214,7 +48209,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
48214
48209
  },
48215
48210
  };
48216
48211
 
48217
- const DEFAULT_NAMESPACE$8 = 'Settings.MemberDetail';
48212
+ const DEFAULT_NAMESPACE$5 = 'Settings.MemberDetail';
48218
48213
  /**
48219
48214
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
48220
48215
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -48251,7 +48246,7 @@ class MemberDetailModalComponent {
48251
48246
  { id: 'admin', name: 'admin' },
48252
48247
  ];
48253
48248
  /** Namespace i18n con que la vista resuelve sus textos. */
48254
- this.i18nNamespace = DEFAULT_NAMESPACE$8;
48249
+ this.i18nNamespace = DEFAULT_NAMESPACE$5;
48255
48250
  this.working = signal(false);
48256
48251
  this.currentRole = signal('');
48257
48252
  this.roleControl = new FormControl('');
@@ -48291,8 +48286,8 @@ class MemberDetailModalComponent {
48291
48286
  type: 'button',
48292
48287
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
48293
48288
  }));
48294
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
48295
- this.i18n.registerContent(DEFAULT_NAMESPACE$8, MEMBER_DETAIL_MODAL_I18N);
48289
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48290
+ this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_DETAIL_MODAL_I18N);
48296
48291
  }
48297
48292
  }
48298
48293
  ngOnInit() {
@@ -48629,7 +48624,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
48629
48624
  },
48630
48625
  };
48631
48626
 
48632
- const DEFAULT_NAMESPACE$7 = 'Settings.TransferModal';
48627
+ const DEFAULT_NAMESPACE$4 = 'Settings.TransferModal';
48633
48628
  /**
48634
48629
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
48635
48630
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -48653,7 +48648,7 @@ class TransferOwnershipModalComponent {
48653
48648
  /** Lista de miembros transferibles (todos excepto el owner actual). */
48654
48649
  this.members = [];
48655
48650
  /** Namespace i18n con que la vista resuelve sus textos. */
48656
- this.i18nNamespace = DEFAULT_NAMESPACE$7;
48651
+ this.i18nNamespace = DEFAULT_NAMESPACE$4;
48657
48652
  this.query = signal('');
48658
48653
  this.selectedId = signal('');
48659
48654
  this.filtered = computed(() => {
@@ -48665,8 +48660,8 @@ class TransferOwnershipModalComponent {
48665
48660
  m.userId.toLowerCase().includes(q));
48666
48661
  });
48667
48662
  this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
48668
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
48669
- this.i18n.registerContent(DEFAULT_NAMESPACE$7, TRANSFER_OWNERSHIP_MODAL_I18N);
48663
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
48664
+ this.i18n.registerContent(DEFAULT_NAMESPACE$4, TRANSFER_OWNERSHIP_MODAL_I18N);
48670
48665
  }
48671
48666
  }
48672
48667
  ngOnInit() {
@@ -48851,849 +48846,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48851
48846
  type: Input
48852
48847
  }] } });
48853
48848
 
48854
- /**
48855
- * Defaults i18n (es/en) embebidos en `val-member-import-modal` (ADR-023).
48856
- * Auto-registrados si el consumer no proveyó el namespace `Settings.ImportModal`.
48857
- */
48858
- const MEMBER_IMPORT_MODAL_I18N = {
48859
- es: {
48860
- title: 'Importar miembros',
48861
- hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
48862
- csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48863
- rolesAvailable: 'Roles disponibles:',
48864
- onConflictLabel: 'Si el email ya existe',
48865
- conflictAssign: 'Asignar el rol',
48866
- conflictSkip: 'Omitir',
48867
- conflictError: 'Marcar error',
48868
- sendActivation: 'Enviar email de activación a las cuentas nuevas',
48869
- submit: 'Importar',
48870
- submitting: 'Importando...',
48871
- parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
48872
- noRows: 'Pega al menos una fila válida (email y rol).',
48873
- summary: 'Resumen',
48874
- created: 'creados',
48875
- assigned: 'asignados',
48876
- skipped: 'omitidos',
48877
- failed: 'con error',
48878
- colEmail: 'Email',
48879
- colStatus: 'Estado',
48880
- colDetail: 'Detalle',
48881
- activationSent: 'activación enviada',
48882
- activationPending: 'activación pendiente',
48883
- again: 'Importar otro lote',
48884
- close: 'Cerrar',
48885
- genericError: 'No se pudo completar la importación. Intenta de nuevo.',
48886
- },
48887
- en: {
48888
- title: 'Import members',
48889
- hint: 'Paste a CSV with columns: email, name, role (one row per person).',
48890
- csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48891
- rolesAvailable: 'Available roles:',
48892
- onConflictLabel: 'If the email already exists',
48893
- conflictAssign: 'Assign the role',
48894
- conflictSkip: 'Skip',
48895
- conflictError: 'Mark as error',
48896
- sendActivation: 'Send activation email to new accounts',
48897
- submit: 'Import',
48898
- submitting: 'Importing...',
48899
- parseError: "Couldn't parse the CSV. Check the format.",
48900
- noRows: 'Paste at least one valid row (email and role).',
48901
- summary: 'Summary',
48902
- created: 'created',
48903
- assigned: 'assigned',
48904
- skipped: 'skipped',
48905
- failed: 'failed',
48906
- colEmail: 'Email',
48907
- colStatus: 'Status',
48908
- colDetail: 'Detail',
48909
- activationSent: 'activation sent',
48910
- activationPending: 'activation pending',
48911
- again: 'Import another batch',
48912
- close: 'Close',
48913
- genericError: "Couldn't complete the import. Try again.",
48914
- },
48915
- };
48916
-
48917
- const DEFAULT_NAMESPACE$6 = 'Settings.ImportModal';
48918
- /**
48919
- * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
48920
- *
48921
- * El admin pega un CSV (`email,nombre,rol`); el modal lo parsea a filas y llama
48922
- * `OrgService.importMembers`. Muestra el resultado por-fila (partial success):
48923
- * `created` / `assigned` / `skipped` / `error`. Header canónico (Regla #5).
48924
- *
48925
- * Auto-registra defaults i18n (es/en) en `Settings.ImportModal`.
48926
- */
48927
- class MemberImportModalComponent {
48928
- constructor() {
48929
- this.i18n = inject(I18nService);
48930
- this.orgService = inject(OrgService);
48931
- /** Org destino del import. */
48932
- this.orgId = '';
48933
- /** Nombres de roles disponibles (para el hint). Opcional. */
48934
- this.availableRoles = [];
48935
- /** Namespace i18n. */
48936
- this.i18nNamespace = DEFAULT_NAMESPACE$6;
48937
- this.csv = '';
48938
- this.onConflict = 'assignRole';
48939
- this.sendActivation = true;
48940
- this.submitting = signal(false);
48941
- this.errorMsg = signal('');
48942
- this.results = signal(null);
48943
- this.rolesHint = computed(() => this.availableRoles.length ? `${this.t('rolesAvailable')} ${this.availableRoles.join(', ')}` : '');
48944
- this.summaryLabel = computed(() => {
48945
- const s = this.results()?.summary;
48946
- if (!s)
48947
- return this.t('summary');
48948
- return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
48949
- });
48950
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
48951
- this.i18n.registerContent(DEFAULT_NAMESPACE$6, MEMBER_IMPORT_MODAL_I18N);
48952
- }
48953
- }
48954
- ngOnInit() {
48955
- if (!this.i18n.hasNamespace(this.i18nNamespace)) {
48956
- this.i18n.registerContent(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
48957
- }
48958
- }
48959
- /** Parsea el CSV pegado a filas {email,name,roleName}. Tolera header. */
48960
- parseCsv() {
48961
- const rows = [];
48962
- for (const raw of this.csv.split(/\r?\n/)) {
48963
- const line = raw.trim();
48964
- if (!line)
48965
- continue;
48966
- const cols = line.split(',').map(c => c.trim());
48967
- const email = (cols[0] ?? '').toLowerCase();
48968
- // Saltar header (primera col "email") o líneas sin @.
48969
- if (!email || !email.includes('@'))
48970
- continue;
48971
- rows.push({ email, name: cols[1] ?? '', roleName: cols[2] ?? '' });
48972
- }
48973
- return rows;
48974
- }
48975
- submit() {
48976
- this.errorMsg.set('');
48977
- const users = this.parseCsv();
48978
- if (users.length === 0) {
48979
- this.errorMsg.set(this.t('noRows'));
48980
- return;
48981
- }
48982
- if (users.some(u => !u.roleName)) {
48983
- this.errorMsg.set(this.t('parseError'));
48984
- return;
48985
- }
48986
- this.submitting.set(true);
48987
- this.orgService
48988
- .importMembers(this.orgId, {
48989
- users,
48990
- onConflict: this.onConflict,
48991
- sendActivation: this.sendActivation,
48992
- })
48993
- .subscribe({
48994
- next: res => {
48995
- this.submitting.set(false);
48996
- this.results.set(res);
48997
- // Refrescar lista del consumer si hubo altas/asignaciones efectivas.
48998
- if (res.summary.created + res.summary.assigned > 0) {
48999
- this.onSuccess?.();
49000
- }
49001
- },
49002
- error: err => {
49003
- this.submitting.set(false);
49004
- this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49005
- },
49006
- });
49007
- }
49008
- statusLabel(r) {
49009
- const base = this.t(r.status === 'error' ? 'failed' : r.status);
49010
- if (r.status === 'created' && r.activation) {
49011
- return `${base} · ${this.t(r.activation === 'sent' ? 'activationSent' : 'activationPending')}`;
49012
- }
49013
- return base;
49014
- }
49015
- reset() {
49016
- this.csv = '';
49017
- this.errorMsg.set('');
49018
- this.results.set(null);
49019
- }
49020
- dismiss() {
49021
- this._modalRef?.dismiss(null, 'cancel');
49022
- }
49023
- t(key) {
49024
- return this.i18n.t(key, this.i18nNamespace);
49025
- }
49026
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49027
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MemberImportModalComponent, isStandalone: true, selector: "val-member-import-modal", inputs: { orgId: "orgId", availableRoles: "availableRoles", onSuccess: "onSuccess", _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
49028
- <ion-header>
49029
- <ion-toolbar>
49030
- <ion-buttons slot="end">
49031
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49032
- <strong>{{ t('close') }}</strong>
49033
- </ion-button>
49034
- </ion-buttons>
49035
- </ion-toolbar>
49036
- </ion-header>
49037
-
49038
- <ion-content class="ion-padding">
49039
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49040
-
49041
- @if (!results()) {
49042
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49043
-
49044
- @if (rolesHint()) {
49045
- <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49046
- }
49047
-
49048
- <ion-textarea
49049
- class="import-csv"
49050
- [autoGrow]="true"
49051
- [rows]="6"
49052
- [placeholder]="t('csvPlaceholder')"
49053
- [(ngModel)]="csv"
49054
- />
49055
-
49056
- <ion-select
49057
- class="import-conflict"
49058
- label="{{ t('onConflictLabel') }}"
49059
- labelPlacement="stacked"
49060
- interface="popover"
49061
- [(ngModel)]="onConflict"
49062
- >
49063
- <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49064
- <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49065
- <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49066
- </ion-select>
49067
-
49068
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49069
- {{ t('sendActivation') }}
49070
- </ion-checkbox>
49071
-
49072
- @if (errorMsg()) {
49073
- <p class="import-error">{{ errorMsg() }}</p>
49074
- }
49075
-
49076
- <div class="import-actions">
49077
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49078
- @if (submitting()) {
49079
- <ion-spinner name="dots" />
49080
- &nbsp;{{ t('submitting') }}
49081
- } @else {
49082
- {{ t('submit') }}
49083
- }
49084
- </ion-button>
49085
- </div>
49086
- } @else {
49087
- <!-- Resultados -->
49088
- <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49089
- <div class="import-results">
49090
- @for (r of results()!.results; track r.email) {
49091
- <div class="import-row import-row--{{ r.status }}">
49092
- <span class="import-row__email">{{ r.email }}</span>
49093
- <span class="import-row__status">{{ statusLabel(r) }}</span>
49094
- @if (r.reason) {
49095
- <span class="import-row__reason">{{ r.reason }}</span>
49096
- }
49097
- </div>
49098
- }
49099
- </div>
49100
- <div class="import-actions">
49101
- <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49102
- {{ t('again') }}
49103
- </ion-button>
49104
- </div>
49105
- }
49106
- </ion-content>
49107
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { 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"] }] }); }
49108
- }
49109
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
49110
- type: Component,
49111
- args: [{ selector: 'val-member-import-modal', standalone: true, imports: [
49112
- FormsModule,
49113
- IonHeader,
49114
- IonToolbar,
49115
- IonButtons,
49116
- IonButton,
49117
- IonContent,
49118
- IonTextarea,
49119
- IonSelect,
49120
- IonSelectOption,
49121
- IonCheckbox,
49122
- IonSpinner,
49123
- DisplayComponent,
49124
- TextComponent,
49125
- TitleComponent,
49126
- ], template: `
49127
- <ion-header>
49128
- <ion-toolbar>
49129
- <ion-buttons slot="end">
49130
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49131
- <strong>{{ t('close') }}</strong>
49132
- </ion-button>
49133
- </ion-buttons>
49134
- </ion-toolbar>
49135
- </ion-header>
49136
-
49137
- <ion-content class="ion-padding">
49138
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49139
-
49140
- @if (!results()) {
49141
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49142
-
49143
- @if (rolesHint()) {
49144
- <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49145
- }
49146
-
49147
- <ion-textarea
49148
- class="import-csv"
49149
- [autoGrow]="true"
49150
- [rows]="6"
49151
- [placeholder]="t('csvPlaceholder')"
49152
- [(ngModel)]="csv"
49153
- />
49154
-
49155
- <ion-select
49156
- class="import-conflict"
49157
- label="{{ t('onConflictLabel') }}"
49158
- labelPlacement="stacked"
49159
- interface="popover"
49160
- [(ngModel)]="onConflict"
49161
- >
49162
- <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49163
- <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49164
- <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49165
- </ion-select>
49166
-
49167
- <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49168
- {{ t('sendActivation') }}
49169
- </ion-checkbox>
49170
-
49171
- @if (errorMsg()) {
49172
- <p class="import-error">{{ errorMsg() }}</p>
49173
- }
49174
-
49175
- <div class="import-actions">
49176
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49177
- @if (submitting()) {
49178
- <ion-spinner name="dots" />
49179
- &nbsp;{{ t('submitting') }}
49180
- } @else {
49181
- {{ t('submit') }}
49182
- }
49183
- </ion-button>
49184
- </div>
49185
- } @else {
49186
- <!-- Resultados -->
49187
- <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49188
- <div class="import-results">
49189
- @for (r of results()!.results; track r.email) {
49190
- <div class="import-row import-row--{{ r.status }}">
49191
- <span class="import-row__email">{{ r.email }}</span>
49192
- <span class="import-row__status">{{ statusLabel(r) }}</span>
49193
- @if (r.reason) {
49194
- <span class="import-row__reason">{{ r.reason }}</span>
49195
- }
49196
- </div>
49197
- }
49198
- </div>
49199
- <div class="import-actions">
49200
- <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49201
- {{ t('again') }}
49202
- </ion-button>
49203
- </div>
49204
- }
49205
- </ion-content>
49206
- `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.import-csv{--background: var(--ion-color-light);border-radius:10px;margin-bottom:16px;font-family:monospace;font-size:13px}.import-conflict{margin-bottom:12px}.import-activation{display:block;margin-bottom:16px;font-size:14px}.import-error{margin:0 0 12px;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.import-actions{margin-top:8px}.import-results{display:flex;flex-direction:column;gap:6px;margin:12px 0}.import-row{display:grid;grid-template-columns:1fr auto;grid-row-gap:2px;align-items:center;padding:10px 12px;border-radius:8px;border-left:3px solid var(--ion-color-medium);background:var(--ion-color-light);font-size:14px}.import-row--created{border-left-color:var(--ion-color-success, #2dd36f)}.import-row--assigned{border-left-color:var(--ion-color-primary, #7026df)}.import-row--skipped{border-left-color:var(--ion-color-medium, #92949c)}.import-row--error{border-left-color:var(--ion-color-danger, #c0392b)}.import-row__email{font-weight:600;color:var(--ion-color-dark)}.import-row__status{text-align:right;color:var(--ion-color-medium-shade, #6b6e76)}.import-row__reason{grid-column:1 / -1;font-size:12px;color:var(--ion-color-danger, #c0392b)}\n"] }]
49207
- }], ctorParameters: () => [], propDecorators: { orgId: [{
49208
- type: Input
49209
- }], availableRoles: [{
49210
- type: Input
49211
- }], onSuccess: [{
49212
- type: Input
49213
- }], _modalRef: [{
49214
- type: Input
49215
- }], i18nNamespace: [{
49216
- type: Input
49217
- }] } });
49218
-
49219
- /**
49220
- * Cliente HTTP de Client API Keys org-scopeadas (ADR-023). La org activa se
49221
- * resuelve en backend desde el JWT, así que el path no la incluye.
49222
- */
49223
- class ApiKeyService {
49224
- constructor(config, http) {
49225
- this.config = config;
49226
- this.http = http;
49227
- }
49228
- get baseUrl() {
49229
- return `${this.config.apiUrl}/org/api-keys`;
49230
- }
49231
- /** Lista las API keys de la org activa. */
49232
- list() {
49233
- return this.http.get(`${this.baseUrl}/`).pipe(map$1(r => r.keys ?? []));
49234
- }
49235
- /** Crea una API key. La respuesta incluye el secreto (se muestra UNA vez). */
49236
- create(req) {
49237
- return this.http.post(`${this.baseUrl}/`, req);
49238
- }
49239
- /** Revoca (elimina) una API key por id. */
49240
- revoke(keyId) {
49241
- return this.http.delete(`${this.baseUrl}/${keyId}`);
49242
- }
49243
- /** Catálogo de permisos asignables a una key. */
49244
- availablePermissions() {
49245
- return this.http
49246
- .get(`${this.baseUrl}/permissions`)
49247
- .pipe(map$1(r => r.permissions ?? []));
49248
- }
49249
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: i1$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
49250
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, providedIn: 'root' }); }
49251
- }
49252
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, decorators: [{
49253
- type: Injectable,
49254
- args: [{ providedIn: 'root' }]
49255
- }], ctorParameters: () => [{ type: undefined, decorators: [{
49256
- type: Inject,
49257
- args: [VALTECH_AUTH_CONFIG]
49258
- }] }, { type: i1$3.HttpClient }] });
49259
-
49260
- /**
49261
- * Defaults i18n (es/en) embebidos en `val-api-keys-modal` (ADR-023).
49262
- * Auto-registrados si el consumer no proveyó el namespace `Settings.ApiKeysModal`.
49263
- */
49264
- const API_KEYS_MODAL_I18N = {
49265
- es: {
49266
- title: 'Client API Keys',
49267
- hint: 'Llaves M2M org-scopeadas para integraciones (p. ej. importar miembros).',
49268
- empty: 'Aún no hay API keys en esta organización.',
49269
- newKey: 'Nueva API key',
49270
- nameLabel: 'Nombre',
49271
- namePlaceholder: 'Ej: Integración RRHH',
49272
- permissionsLabel: 'Permisos',
49273
- expiresLabel: 'Expira en (días, opcional)',
49274
- create: 'Crear key',
49275
- creating: 'Creando...',
49276
- cancel: 'Cancelar',
49277
- revoke: 'Revocar',
49278
- revokeConfirm: '¿Revocar esta API key? Las integraciones que la usen dejarán de funcionar.',
49279
- secretTitle: 'Copia tu API key ahora',
49280
- secretWarning: 'Este secreto se muestra una sola vez. Guárdalo en un lugar seguro.',
49281
- copy: 'Copiar',
49282
- copied: 'Copiada',
49283
- done: 'Listo',
49284
- statusActive: 'activa',
49285
- lastUsed: 'Último uso',
49286
- never: 'nunca',
49287
- permsCount: 'permisos',
49288
- noName: 'El nombre es obligatorio.',
49289
- noPerms: 'Selecciona al menos un permiso.',
49290
- close: 'Cerrar',
49291
- genericError: 'No se pudo completar la operación. Intenta de nuevo.',
49292
- },
49293
- en: {
49294
- title: 'Client API Keys',
49295
- hint: 'Org-scoped M2M keys for integrations (e.g. member import).',
49296
- empty: 'No API keys in this organization yet.',
49297
- newKey: 'New API key',
49298
- nameLabel: 'Name',
49299
- namePlaceholder: 'E.g: HR integration',
49300
- permissionsLabel: 'Permissions',
49301
- expiresLabel: 'Expires in (days, optional)',
49302
- create: 'Create key',
49303
- creating: 'Creating...',
49304
- cancel: 'Cancel',
49305
- revoke: 'Revoke',
49306
- revokeConfirm: 'Revoke this API key? Integrations using it will stop working.',
49307
- secretTitle: 'Copy your API key now',
49308
- secretWarning: 'This secret is shown only once. Store it somewhere safe.',
49309
- copy: 'Copy',
49310
- copied: 'Copied',
49311
- done: 'Done',
49312
- statusActive: 'active',
49313
- lastUsed: 'Last used',
49314
- never: 'never',
49315
- permsCount: 'permissions',
49316
- noName: 'Name is required.',
49317
- noPerms: 'Select at least one permission.',
49318
- close: 'Close',
49319
- genericError: "Couldn't complete the operation. Try again.",
49320
- },
49321
- };
49322
-
49323
- const DEFAULT_NAMESPACE$5 = 'Settings.ApiKeysModal';
49324
- /**
49325
- * `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
49326
- *
49327
- * Lista las keys de la org, permite crear (mostrando el secreto UNA vez con copia)
49328
- * y revocar. Pensado para habilitar integraciones M2M del import de miembros, pero
49329
- * sirve a cualquier permiso del catálogo. Header canónico (Regla #5).
49330
- */
49331
- class ApiKeysModalComponent {
49332
- constructor() {
49333
- this.i18n = inject(I18nService);
49334
- this.apiKeys = inject(ApiKeyService);
49335
- /** Namespace i18n. */
49336
- this.i18nNamespace = DEFAULT_NAMESPACE$5;
49337
- this.keys = signal([]);
49338
- this.availablePermissions = signal([]);
49339
- this.loading = signal(true);
49340
- this.showCreate = signal(false);
49341
- this.submitting = signal(false);
49342
- this.errorMsg = signal('');
49343
- this.secret = signal(null);
49344
- this.copied = signal(false);
49345
- this.name = '';
49346
- this.expiresInDays = null;
49347
- this.selectedPerms = signal(new Set());
49348
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
49349
- this.i18n.registerContent(DEFAULT_NAMESPACE$5, API_KEYS_MODAL_I18N);
49350
- }
49351
- }
49352
- ngOnInit() {
49353
- if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49354
- this.i18n.registerContent(this.i18nNamespace, API_KEYS_MODAL_I18N);
49355
- }
49356
- this.refresh();
49357
- this.apiKeys.availablePermissions().subscribe({
49358
- next: perms => this.availablePermissions.set(perms),
49359
- error: () => { },
49360
- });
49361
- }
49362
- refresh() {
49363
- this.loading.set(true);
49364
- this.apiKeys.list().subscribe({
49365
- next: keys => {
49366
- this.keys.set(keys);
49367
- this.loading.set(false);
49368
- },
49369
- error: () => this.loading.set(false),
49370
- });
49371
- }
49372
- openCreate() {
49373
- this.errorMsg.set('');
49374
- this.name = '';
49375
- this.expiresInDays = null;
49376
- this.selectedPerms.set(new Set());
49377
- this.showCreate.set(true);
49378
- }
49379
- togglePerm(perm, ev) {
49380
- const checked = ev.detail?.checked ?? false;
49381
- const next = new Set(this.selectedPerms());
49382
- if (checked) {
49383
- next.add(perm);
49384
- }
49385
- else {
49386
- next.delete(perm);
49387
- }
49388
- this.selectedPerms.set(next);
49389
- }
49390
- submit() {
49391
- this.errorMsg.set('');
49392
- if (!this.name.trim()) {
49393
- this.errorMsg.set(this.t('noName'));
49394
- return;
49395
- }
49396
- const permissions = Array.from(this.selectedPerms());
49397
- if (permissions.length === 0) {
49398
- this.errorMsg.set(this.t('noPerms'));
49399
- return;
49400
- }
49401
- this.submitting.set(true);
49402
- this.apiKeys
49403
- .create({
49404
- name: this.name.trim(),
49405
- permissions,
49406
- expiresInDays: this.expiresInDays && this.expiresInDays > 0 ? this.expiresInDays : undefined,
49407
- })
49408
- .subscribe({
49409
- next: res => {
49410
- this.submitting.set(false);
49411
- this.showCreate.set(false);
49412
- this.secret.set(res.key);
49413
- },
49414
- error: err => {
49415
- this.submitting.set(false);
49416
- this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49417
- },
49418
- });
49419
- }
49420
- revoke(key) {
49421
- if (!confirm(this.t('revokeConfirm')))
49422
- return;
49423
- this.apiKeys.revoke(key.keyId).subscribe({
49424
- next: () => this.refresh(),
49425
- error: err => this.errorMsg.set(err?.error?.message ?? this.t('genericError')),
49426
- });
49427
- }
49428
- async copy(secret) {
49429
- try {
49430
- await navigator.clipboard.writeText(secret);
49431
- this.copied.set(true);
49432
- }
49433
- catch {
49434
- // clipboard no disponible — el user puede seleccionar el texto manualmente
49435
- }
49436
- }
49437
- finishSecret() {
49438
- this.secret.set(null);
49439
- this.copied.set(false);
49440
- this.refresh();
49441
- }
49442
- dismiss() {
49443
- this._modalRef?.dismiss(null, 'cancel');
49444
- }
49445
- t(key) {
49446
- return this.i18n.t(key, this.i18nNamespace);
49447
- }
49448
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49449
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ApiKeysModalComponent, isStandalone: true, selector: "val-api-keys-modal", inputs: { _modalRef: "_modalRef", i18nNamespace: "i18nNamespace" }, ngImport: i0, template: `
49450
- <ion-header>
49451
- <ion-toolbar>
49452
- <ion-buttons slot="end">
49453
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49454
- <strong>{{ t('close') }}</strong>
49455
- </ion-button>
49456
- </ion-buttons>
49457
- </ion-toolbar>
49458
- </ion-header>
49459
-
49460
- <ion-content class="ion-padding">
49461
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49462
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49463
-
49464
- @if (secret(); as s) {
49465
- <!-- Secreto recién creado: mostrar UNA vez -->
49466
- <div class="apikey-secret">
49467
- <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49468
- <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49469
- <div class="apikey-secret__box">
49470
- <code>{{ s.secret }}</code>
49471
- </div>
49472
- <div class="apikey-actions">
49473
- <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49474
- {{ copied() ? t('copied') : t('copy') }}
49475
- </ion-button>
49476
- <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49477
- {{ t('done') }}
49478
- </ion-button>
49479
- </div>
49480
- </div>
49481
- } @else {
49482
- @if (loading()) {
49483
- <ion-spinner name="dots" />
49484
- } @else {
49485
- <!-- Lista -->
49486
- @if (keys().length === 0) {
49487
- <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49488
- } @else {
49489
- <div class="apikey-list">
49490
- @for (k of keys(); track k.keyId) {
49491
- <div class="apikey-row">
49492
- <div class="apikey-row__main">
49493
- <span class="apikey-row__name">{{ k.name }}</span>
49494
- <span class="apikey-row__meta">
49495
- {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49496
- {{ k.lastUsedAt || t('never') }}
49497
- </span>
49498
- </div>
49499
- <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49500
- {{ t('revoke') }}
49501
- </ion-button>
49502
- </div>
49503
- }
49504
- </div>
49505
- }
49506
-
49507
- @if (errorMsg()) {
49508
- <p class="apikey-error">{{ errorMsg() }}</p>
49509
- }
49510
-
49511
- <!-- Crear -->
49512
- @if (showCreate()) {
49513
- <div class="apikey-create">
49514
- <ion-input
49515
- label="{{ t('nameLabel') }}"
49516
- labelPlacement="stacked"
49517
- [placeholder]="t('namePlaceholder')"
49518
- [(ngModel)]="name"
49519
- />
49520
- <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49521
- <div class="apikey-perms">
49522
- @for (p of availablePermissions(); track p) {
49523
- <ion-checkbox
49524
- labelPlacement="end"
49525
- [checked]="selectedPerms().has(p)"
49526
- (ionChange)="togglePerm(p, $event)"
49527
- >
49528
- {{ p }}
49529
- </ion-checkbox>
49530
- }
49531
- </div>
49532
- <ion-input
49533
- label="{{ t('expiresLabel') }}"
49534
- labelPlacement="stacked"
49535
- type="number"
49536
- [(ngModel)]="expiresInDays"
49537
- />
49538
- <div class="apikey-actions">
49539
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49540
- @if (submitting()) {
49541
- <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49542
- } @else {
49543
- {{ t('create') }}
49544
- }
49545
- </ion-button>
49546
- <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49547
- {{ t('cancel') }}
49548
- </ion-button>
49549
- </div>
49550
- </div>
49551
- } @else {
49552
- <div class="apikey-actions">
49553
- <ion-button expand="block" color="primary" (click)="openCreate()">
49554
- {{ t('newKey') }}
49555
- </ion-button>
49556
- </div>
49557
- }
49558
- }
49559
- }
49560
- </ion-content>
49561
- `, isInline: true, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { 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"] }] }); }
49562
- }
49563
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, decorators: [{
49564
- type: Component,
49565
- args: [{ selector: 'val-api-keys-modal', standalone: true, imports: [
49566
- FormsModule,
49567
- IonHeader,
49568
- IonToolbar,
49569
- IonButtons,
49570
- IonButton,
49571
- IonContent,
49572
- IonInput,
49573
- IonCheckbox,
49574
- IonSpinner,
49575
- DisplayComponent,
49576
- TextComponent,
49577
- TitleComponent,
49578
- ], template: `
49579
- <ion-header>
49580
- <ion-toolbar>
49581
- <ion-buttons slot="end">
49582
- <ion-button fill="clear" color="dark" (click)="dismiss()">
49583
- <strong>{{ t('close') }}</strong>
49584
- </ion-button>
49585
- </ion-buttons>
49586
- </ion-toolbar>
49587
- </ion-header>
49588
-
49589
- <ion-content class="ion-padding">
49590
- <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49591
- <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49592
-
49593
- @if (secret(); as s) {
49594
- <!-- Secreto recién creado: mostrar UNA vez -->
49595
- <div class="apikey-secret">
49596
- <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49597
- <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49598
- <div class="apikey-secret__box">
49599
- <code>{{ s.secret }}</code>
49600
- </div>
49601
- <div class="apikey-actions">
49602
- <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49603
- {{ copied() ? t('copied') : t('copy') }}
49604
- </ion-button>
49605
- <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49606
- {{ t('done') }}
49607
- </ion-button>
49608
- </div>
49609
- </div>
49610
- } @else {
49611
- @if (loading()) {
49612
- <ion-spinner name="dots" />
49613
- } @else {
49614
- <!-- Lista -->
49615
- @if (keys().length === 0) {
49616
- <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49617
- } @else {
49618
- <div class="apikey-list">
49619
- @for (k of keys(); track k.keyId) {
49620
- <div class="apikey-row">
49621
- <div class="apikey-row__main">
49622
- <span class="apikey-row__name">{{ k.name }}</span>
49623
- <span class="apikey-row__meta">
49624
- {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49625
- {{ k.lastUsedAt || t('never') }}
49626
- </span>
49627
- </div>
49628
- <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49629
- {{ t('revoke') }}
49630
- </ion-button>
49631
- </div>
49632
- }
49633
- </div>
49634
- }
49635
-
49636
- @if (errorMsg()) {
49637
- <p class="apikey-error">{{ errorMsg() }}</p>
49638
- }
49639
-
49640
- <!-- Crear -->
49641
- @if (showCreate()) {
49642
- <div class="apikey-create">
49643
- <ion-input
49644
- label="{{ t('nameLabel') }}"
49645
- labelPlacement="stacked"
49646
- [placeholder]="t('namePlaceholder')"
49647
- [(ngModel)]="name"
49648
- />
49649
- <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49650
- <div class="apikey-perms">
49651
- @for (p of availablePermissions(); track p) {
49652
- <ion-checkbox
49653
- labelPlacement="end"
49654
- [checked]="selectedPerms().has(p)"
49655
- (ionChange)="togglePerm(p, $event)"
49656
- >
49657
- {{ p }}
49658
- </ion-checkbox>
49659
- }
49660
- </div>
49661
- <ion-input
49662
- label="{{ t('expiresLabel') }}"
49663
- labelPlacement="stacked"
49664
- type="number"
49665
- [(ngModel)]="expiresInDays"
49666
- />
49667
- <div class="apikey-actions">
49668
- <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49669
- @if (submitting()) {
49670
- <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49671
- } @else {
49672
- {{ t('create') }}
49673
- }
49674
- </ion-button>
49675
- <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49676
- {{ t('cancel') }}
49677
- </ion-button>
49678
- </div>
49679
- </div>
49680
- } @else {
49681
- <div class="apikey-actions">
49682
- <ion-button expand="block" color="primary" (click)="openCreate()">
49683
- {{ t('newKey') }}
49684
- </ion-button>
49685
- </div>
49686
- }
49687
- }
49688
- }
49689
- </ion-content>
49690
- `, styles: ["val-display{display:block;margin-bottom:4px}val-title{display:block;margin-bottom:12px}.apikey-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}.apikey-row{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;border:1.5px solid var(--ion-color-light)}.apikey-row__main{display:flex;flex-direction:column}.apikey-row__name{font-weight:600;color:var(--ion-color-dark)}.apikey-row__meta{font-size:12px;color:var(--ion-color-medium-shade, #6b6e76)}.apikey-create{margin-top:8px;padding:16px;border-radius:12px;background:var(--ion-color-light)}.apikey-create__label{margin:12px 0 4px;font-size:13px;font-weight:600;color:var(--ion-color-dark)}.apikey-perms{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}.apikey-actions{margin-top:16px;display:flex;flex-direction:column;gap:8px}.apikey-error{margin:8px 0;font-size:.875rem;color:var(--ion-color-danger, #c0392b)}.apikey-secret__warn{font-size:.875rem;color:var(--ion-color-warning-shade, #b88600);margin:0 0 12px}.apikey-secret__box{padding:12px;border-radius:8px;background:var(--ion-color-dark);color:#fff;word-break:break-all;font-family:monospace;font-size:13px}\n"] }]
49691
- }], ctorParameters: () => [], propDecorators: { _modalRef: [{
49692
- type: Input
49693
- }], i18nNamespace: [{
49694
- type: Input
49695
- }] } });
49696
-
49697
48849
  /**
49698
48850
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
49699
48851
  * el constructor del componente si el consumer no proveyó el namespace
@@ -49749,16 +48901,6 @@ const ORGANIZATION_VIEW_I18N = {
49749
48901
  inviteCtaTitle: '¿Deseas agregar a alguien?',
49750
48902
  inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
49751
48903
  inviteOpen: 'Invitar',
49752
- importOpen: 'Importar',
49753
- apiKeysOpen: 'Gestionar',
49754
- apiKeysSectionTitle: 'Integraciones (API Keys)',
49755
- apiKeysSectionHint: 'También puedes operar tu organización vía API: crea una Client API Key org-scopeada para integraciones máquina-a-máquina (por ejemplo, dar de alta miembros en masa desde tu propio sistema).',
49756
- apiKeysMoreInfo: 'Más información',
49757
- orgsMoreInfo: 'Más información',
49758
- orgsInfoTitle: '¿Qué son las organizaciones?',
49759
- orgsInfoSubtitle: 'Todo lo que necesitas saber',
49760
- orgsInfoClose: 'Cerrar',
49761
- orgsInfoContent: 'Las organizaciones te permiten separar tus proyectos y datos por empresa, equipo o contexto. Puedes pertenecer a varias organizaciones y cambiar entre ellas en cualquier momento.\n\nCada organización tiene sus propios miembros, roles y permisos. La organización activa determina qué datos ves y qué acciones puedes realizar.\n\nPuedes crear una organización nueva o unirte a una existente mediante invitación.',
49762
48904
  viewMemberCta: 'Ver miembro',
49763
48905
  roleViewer: 'Visor',
49764
48906
  roleEditor: 'Editor',
@@ -49828,16 +48970,6 @@ const ORGANIZATION_VIEW_I18N = {
49828
48970
  inviteCtaTitle: 'Want to add someone?',
49829
48971
  inviteCtaHint: 'Invite users to join your organization.',
49830
48972
  inviteOpen: 'Invite',
49831
- importOpen: 'Import',
49832
- apiKeysOpen: 'Manage',
49833
- apiKeysSectionTitle: 'Integrations (API Keys)',
49834
- apiKeysSectionHint: 'You can also operate your organization via API: create an org-scoped Client API Key for machine-to-machine integrations (for example, bulk-provisioning members from your own system).',
49835
- apiKeysMoreInfo: 'More information',
49836
- orgsMoreInfo: 'More information',
49837
- orgsInfoTitle: 'What are organizations?',
49838
- orgsInfoSubtitle: 'Everything you need to know',
49839
- orgsInfoClose: 'Close',
49840
- orgsInfoContent: 'Organizations let you separate your projects and data by company, team, or context. You can belong to multiple organizations and switch between them at any time.\n\nEach organization has its own members, roles, and permissions. The active organization determines what data you see and what actions you can take.\n\nYou can create a new organization or join an existing one via invitation.',
49841
48973
  viewMemberCta: 'View member',
49842
48974
  roleViewer: 'Viewer',
49843
48975
  roleEditor: 'Editor',
@@ -49862,7 +48994,7 @@ const ORGANIZATION_VIEW_I18N = {
49862
48994
  },
49863
48995
  };
49864
48996
 
49865
- const DEFAULT_NAMESPACE$4 = 'Settings.Organization';
48997
+ const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49866
48998
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
49867
48999
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49868
49000
  /**
@@ -49919,7 +49051,7 @@ class OrganizationViewComponent {
49919
49051
  showInvite: merged.showInvite ?? true,
49920
49052
  showTransferOwnership: merged.showTransferOwnership ?? true,
49921
49053
  showLeave: merged.showLeave ?? true,
49922
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$4,
49054
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49923
49055
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49924
49056
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49925
49057
  onOrgEdited: merged.onOrgEdited,
@@ -49951,22 +49083,6 @@ class OrganizationViewComponent {
49951
49083
  this.members = signal([]);
49952
49084
  this.membersLoading = signal(false);
49953
49085
  this.membersLoadError = signal(null);
49954
- this.membersErrorState = computed(() => {
49955
- this.i18n.lang();
49956
- const err = this.membersLoadError();
49957
- if (!err)
49958
- return null;
49959
- return createErrorStateProps(err, {
49960
- title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
49961
- description: {
49962
- offline: this.tt('offlineHint'),
49963
- error: interpretError(err).message,
49964
- },
49965
- retryLabel: this.tt('retry'),
49966
- onRetry: () => this.loadMembers(),
49967
- retrying: this.membersLoading(),
49968
- });
49969
- });
49970
49086
  this.membersNextToken = signal(null);
49971
49087
  this.loadingMoreMembers = signal(false);
49972
49088
  this.showAllMembers = signal(false);
@@ -50053,22 +49169,6 @@ class OrganizationViewComponent {
50053
49169
  shape: 'round',
50054
49170
  type: 'button',
50055
49171
  }));
50056
- this.openImportButtonProps = computed(() => ({
50057
- text: this.tt('importOpen'),
50058
- color: 'dark',
50059
- fill: 'outline',
50060
- size: 'default',
50061
- shape: 'round',
50062
- type: 'button',
50063
- }));
50064
- this.openApiKeysButtonProps = computed(() => ({
50065
- text: this.tt('apiKeysOpen'),
50066
- color: 'dark',
50067
- fill: 'clear',
50068
- size: 'default',
50069
- shape: 'round',
50070
- type: 'button',
50071
- }));
50072
49172
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
50073
49173
  this.lastLoadedOrgId = null;
50074
49174
  const ns = this.ns;
@@ -50169,59 +49269,6 @@ class OrganizationViewComponent {
50169
49269
  backdropDismiss: true,
50170
49270
  });
50171
49271
  }
50172
- onOpenImport() {
50173
- void this.modalService.open({
50174
- component: MemberImportModalComponent,
50175
- componentProps: {
50176
- orgId: this.activeOrgId(),
50177
- availableRoles: this.availableRoles().map(r => r.name),
50178
- onSuccess: () => {
50179
- this.loadMembers();
50180
- this.resolvedConfig().onMemberInvited?.();
50181
- },
50182
- },
50183
- breakpoints: {
50184
- breakpoints: [0, 0.92],
50185
- initialBreakpoint: 0.92,
50186
- showHandle: true,
50187
- },
50188
- backdropDismiss: true,
50189
- });
50190
- }
50191
- onOpenApiKeys() {
50192
- void this.modalService.open({
50193
- component: ApiKeysModalComponent,
50194
- breakpoints: {
50195
- breakpoints: [0, 0.92],
50196
- initialBreakpoint: 0.92,
50197
- showHandle: true,
50198
- },
50199
- backdropDismiss: true,
50200
- });
50201
- }
50202
- // "Más información" sobre organizaciones — mismo sheet explicativo que muestra
50203
- // account-view sin org, pero disponible siempre (todo user tiene org por defecto).
50204
- onOrgInfo() {
50205
- void this.modalService.open({
50206
- component: OrgInfoSheetComponent,
50207
- componentProps: {
50208
- title: this.tt('orgsInfoTitle'),
50209
- subtitle: this.tt('orgsInfoSubtitle'),
50210
- body: this.tt('orgsInfoContent'),
50211
- closeLabel: this.tt('orgsInfoClose'),
50212
- closeProps: {
50213
- text: this.tt('orgsInfoClose'),
50214
- fill: 'outline',
50215
- color: 'dark',
50216
- size: 'default',
50217
- expand: 'block',
50218
- type: 'button',
50219
- },
50220
- },
50221
- breakpoints: { breakpoints: [0, 0.85], initialBreakpoint: 0.85, showHandle: true },
50222
- backdropDismiss: true,
50223
- });
50224
- }
50225
49272
  onViewPermissions() {
50226
49273
  this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
50227
49274
  }
@@ -50303,7 +49350,6 @@ class OrganizationViewComponent {
50303
49350
  if (!orgId)
50304
49351
  return Promise.resolve();
50305
49352
  this.membersLoading.set(true);
50306
- this.membersLoadError.set(null);
50307
49353
  this.membersNextToken.set(null);
50308
49354
  this.showAllMembers.set(false);
50309
49355
  return new Promise(resolve => {
@@ -50467,12 +49513,6 @@ class OrganizationViewComponent {
50467
49513
  }
50468
49514
  </div>
50469
49515
 
50470
- <!-- "Más información" sobre organizaciones — disponible siempre (todo
50471
- user tiene org por defecto y no ve el empty-state de account). -->
50472
- <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50473
- {{ tt('orgsMoreInfo') }}
50474
- </button>
50475
-
50476
49516
  <div class="section-body">
50477
49517
  @if (loading()) {
50478
49518
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -50520,8 +49560,6 @@ class OrganizationViewComponent {
50520
49560
  <div class="section-body">
50521
49561
  @if (membersLoading()) {
50522
49562
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50523
- } @else if (membersErrorState()) {
50524
- <val-empty-state [props]="membersErrorState()!" />
50525
49563
  } @else if (members().length === 0) {
50526
49564
  <val-text
50527
49565
  [props]="{
@@ -50614,25 +49652,10 @@ class OrganizationViewComponent {
50614
49652
  />
50615
49653
  </div>
50616
49654
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50617
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50618
49655
  </div>
50619
49656
  </section>
50620
49657
  }
50621
49658
 
50622
- <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50623
- <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50624
- <div class="invite-cta">
50625
- <div class="invite-cta__text">
50626
- <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50627
- <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50628
- <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50629
- {{ tt('apiKeysMoreInfo') }}
50630
- </button>
50631
- </div>
50632
- <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50633
- </div>
50634
- </section>
50635
-
50636
49659
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
50637
49660
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
50638
49661
  <section class="settings-section" data-testid="org-transfer-section">
@@ -50723,7 +49746,7 @@ class OrganizationViewComponent {
50723
49746
  </section>
50724
49747
  }
50725
49748
  </div>
50726
- `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.invite-cta__text{display:flex;flex-direction:column;gap:4px;flex:1}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field--full{width:100%}.org-info-field:last-child{border-bottom:none}.org-info-row{display:flex}.org-info-row .org-info-field{flex:1}.org-info-row .org-info-field:first-child{border-right:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }] }); }
49749
+ `, isInline: true, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.invite-cta__text{display:flex;flex-direction:column;gap:4px;flex:1}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field--full{width:100%}.org-info-field:last-child{border-bottom:none}.org-info-row{display:flex}.org-info-row .org-info-field{flex:1}.org-info-row .org-info-field:first-child{border-right:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "component", type: DisplayComponent, selector: "val-display", inputs: ["props"] }, { kind: "component", type: EmptyStateComponent, selector: "val-empty-state", inputs: ["props"] }, { kind: "directive", type: HasPermissionDirective, selector: "[valHasPermission]", inputs: ["valHasPermission"] }, { kind: "component", type: SkeletonLayoutComponent, selector: "val-skeleton-layout", inputs: ["props"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }, { kind: "component", type: MemberCardComponent, selector: "val-member-card", inputs: ["props"], outputs: ["onAction"] }] }); }
50727
49750
  }
50728
49751
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
50729
49752
  type: Component,
@@ -50737,9 +49760,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50737
49760
  ButtonComponent,
50738
49761
  MemberCardComponent,
50739
49762
  TransferOwnershipModalComponent,
50740
- MemberImportModalComponent,
50741
- ApiKeysModalComponent,
50742
- OrgInfoSheetComponent,
50743
49763
  ], template: `
50744
49764
  <div class="page">
50745
49765
  <header class="page-header">
@@ -50771,12 +49791,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50771
49791
  }
50772
49792
  </div>
50773
49793
 
50774
- <!-- "Más información" sobre organizaciones — disponible siempre (todo
50775
- user tiene org por defecto y no ve el empty-state de account). -->
50776
- <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50777
- {{ tt('orgsMoreInfo') }}
50778
- </button>
50779
-
50780
49794
  <div class="section-body">
50781
49795
  @if (loading()) {
50782
49796
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -50824,8 +49838,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50824
49838
  <div class="section-body">
50825
49839
  @if (membersLoading()) {
50826
49840
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50827
- } @else if (membersErrorState()) {
50828
- <val-empty-state [props]="membersErrorState()!" />
50829
49841
  } @else if (members().length === 0) {
50830
49842
  <val-text
50831
49843
  [props]="{
@@ -50918,25 +49930,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50918
49930
  />
50919
49931
  </div>
50920
49932
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50921
- <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
50922
49933
  </div>
50923
49934
  </section>
50924
49935
  }
50925
49936
 
50926
- <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50927
- <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50928
- <div class="invite-cta">
50929
- <div class="invite-cta__text">
50930
- <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50931
- <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50932
- <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50933
- {{ tt('apiKeysMoreInfo') }}
50934
- </button>
50935
- </div>
50936
- <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50937
- </div>
50938
- </section>
50939
-
50940
49937
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
50941
49938
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
50942
49939
  <section class="settings-section" data-testid="org-transfer-section">
@@ -51027,7 +50024,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
51027
50024
  </section>
51028
50025
  }
51029
50026
  </div>
51030
- `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.org-more-info-link{background:none;border:none;padding:4px 0;margin-top:4px;font-size:13px;font-weight:600;color:var(--ion-color-primary, #7026df);cursor:pointer;text-align:left}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.invite-cta__text{display:flex;flex-direction:column;gap:4px;flex:1}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field--full{width:100%}.org-info-field:last-child{border-bottom:none}.org-info-row{display:flex}.org-info-row .org-info-field{flex:1}.org-info-row .org-info-field:first-child{border-right:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}\n"] }]
50027
+ `, styles: [".page{padding:16px 0;max-width:720px;margin:0 auto}.page-header{margin-bottom:16px}.settings-section{padding:16px 0}.settings-section+.settings-section{border-top:1px solid var(--val-border-color, rgba(0, 0, 0, .08))}.section-header-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.section-body{display:flex;flex-direction:column;gap:10px}.row-actions{margin-top:12px}.row-actions--gap{display:flex;gap:8px}.invite-cta{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}.invite-cta__text{display:flex;flex-direction:column;gap:4px;flex:1}.org-info-card{border-radius:14px;background:var(--ion-color-light, #f4f5f8);overflow:hidden}:host-context(body.dark) .org-info-card,:host-context(html.ion-palette-dark) .org-info-card,:host-context([data-theme=\"dark\"]) .org-info-card{background:#ffffff0d}.org-info-field{padding:12px 16px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-field--full{width:100%}.org-info-field:last-child{border-bottom:none}.org-info-row{display:flex}.org-info-row .org-info-field{flex:1}.org-info-row .org-info-field:first-child{border-right:1px solid var(--val-border-color, rgba(0, 0, 0, .06))}.org-info-label{font-size:.74rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ion-color-medium)}.org-info-value{font-size:.95rem;font-weight:500;color:var(--ion-color-dark)}.org-info-value--muted{color:var(--ion-color-medium);font-weight:400}.plan-badge{display:inline-block;font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:20px;width:fit-content}.plan-badge--free{background:var(--ion-color-light-shade, #d7d8da);color:var(--ion-color-dark)}.plan-badge--pro{background:var(--ion-color-primary);color:#fff}.plan-badge--enterprise{background:#f5c542;color:#222}.members-list{display:flex;flex-direction:column;gap:8px}.members-show-more{background:none;border:none;color:var(--ion-color-primary);font-size:14px;font-weight:500;cursor:pointer;padding:8px 0}.rbac-debug{opacity:.7}.rbac-debug__body{display:flex;flex-direction:column;gap:6px;font-family:monospace;font-size:.78rem}.rbac-debug__row{display:flex;gap:8px;align-items:flex-start}.rbac-debug__label{color:var(--ion-color-medium);min-width:72px;flex-shrink:0}.rbac-debug__value{color:var(--ion-color-dark);word-break:break-all}.rbac-debug__value--perms{color:var(--ion-color-medium)}\n"] }]
51031
50028
  }], ctorParameters: () => [], propDecorators: { config: [{
51032
50029
  type: Input
51033
50030
  }] } });
@@ -51141,7 +50138,7 @@ addIcons({
51141
50138
  notificationsOutline,
51142
50139
  refreshOutline,
51143
50140
  });
51144
- const DEFAULT_NAMESPACE$3 = 'Settings.Notifications';
50141
+ const DEFAULT_NAMESPACE$2 = 'Settings.Notifications';
51145
50142
  /**
51146
50143
  * `val-notification-preferences-view` — **preferencias de notificaciones push**
51147
50144
  * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
@@ -51184,7 +50181,7 @@ class NotificationPreferencesViewComponent {
51184
50181
  const merged = { ...fromRoute, ...(this.config ?? {}) };
51185
50182
  return {
51186
50183
  showDetails: merged.showDetails ?? true,
51187
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
50184
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
51188
50185
  onPushEnabled: merged.onPushEnabled,
51189
50186
  onPushDisabled: merged.onPushDisabled,
51190
50187
  };
@@ -51955,7 +50952,7 @@ const PERMISSIONS_VIEW_I18N = {
51955
50952
  },
51956
50953
  };
51957
50954
 
51958
- const DEFAULT_NAMESPACE$2 = 'Settings.Permissions';
50955
+ const DEFAULT_NAMESPACE$1 = 'Settings.Permissions';
51959
50956
  /**
51960
50957
  * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
51961
50958
  * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
@@ -51995,7 +50992,7 @@ class PermissionsViewComponent {
51995
50992
  return {
51996
50993
  showSystemRoles: merged.showSystemRoles ?? true,
51997
50994
  showCustomRoles: merged.showCustomRoles ?? true,
51998
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
50995
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
51999
50996
  };
52000
50997
  });
52001
50998
  this.loading = signal(false);
@@ -52772,7 +51769,7 @@ addIcons({
52772
51769
  openOutline,
52773
51770
  personOutline,
52774
51771
  });
52775
- const DEFAULT_NAMESPACE$1 = 'Notifications';
51772
+ const DEFAULT_NAMESPACE = 'Notifications';
52776
51773
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
52777
51774
  const DEFAULT_PAGE_SIZE = 10;
52778
51775
  const DEFAULT_MAX_LOAD = 50;
@@ -52832,7 +51829,7 @@ class NotificationsViewComponent {
52832
51829
  showPushOffCta: merged.showPushOffCta ?? true,
52833
51830
  enableMarkAll: merged.enableMarkAll ?? true,
52834
51831
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
52835
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
51832
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
52836
51833
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
52837
51834
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
52838
51835
  onNotificationOpen: merged.onNotificationOpen,
@@ -53500,797 +52497,6 @@ function provideValtechNotificationsRoutes(opts) {
53500
52497
  ];
53501
52498
  }
53502
52499
 
53503
- /**
53504
- * Defaults i18n del componente (es/en). El componente los auto-registra en su
53505
- * constructor si el consumer no proveyó el namespace `ContentReaction`. Esto
53506
- * garantiza que NUNCA haya una key faltante: una key faltante se evalúa en el
53507
- * template por cada change-detection → `console.warn` por CD → con el
53508
- * `val-debug-console` activo (espeja console.* a un signal) se dispara un loop
53509
- * infinito que congela el navegador en dev. Auto-registrar lo elimina de raíz.
53510
- */
53511
- const CONTENT_REACTION_I18N = {
53512
- es: {
53513
- question: '¿Te resultó útil este contenido?',
53514
- commentPlaceholder: 'Cuéntanos más (opcional)...',
53515
- submit: 'Enviar opinión',
53516
- update: 'Actualizar opinión',
53517
- thankYou: '¡Gracias por tu opinión!',
53518
- submitted: 'Tu opinión ha sido registrada',
53519
- errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53520
- positiveLabel: 'Sí, mucho',
53521
- negativeLabel: 'No tanto',
53522
- negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53523
- },
53524
- en: {
53525
- question: 'Was this content helpful?',
53526
- commentPlaceholder: 'Tell us more (optional)...',
53527
- submit: 'Send feedback',
53528
- update: 'Update feedback',
53529
- thankYou: 'Thanks for your feedback!',
53530
- submitted: 'Your feedback has been recorded',
53531
- errorSubmitting: 'Failed to send. Please try again.',
53532
- positiveLabel: 'Yes, a lot',
53533
- negativeLabel: 'Not really',
53534
- negativeFeedbackTitle: "Tell us what you didn't like?",
53535
- },
53536
- };
53537
- /**
53538
- * Componente para reacciones de contenido con emojis.
53539
- *
53540
- * @example
53541
- * ```html
53542
- * <val-content-reaction
53543
- * [props]="{
53544
- * entityRef: { entityType: 'article', entityId: 'art-123' },
53545
- * question: '¿Te fue útil este artículo?'
53546
- * }"
53547
- * (reactionSubmit)="onReactionSubmit($event)"
53548
- * />
53549
- * ```
53550
- */
53551
- class ContentReactionComponent {
53552
- constructor() {
53553
- this.feedbackService = inject(FeedbackService);
53554
- this.toast = inject(ToastService);
53555
- this.i18n = inject(I18nService);
53556
- this.auth = inject(AuthService, { optional: true });
53557
- this.isConfigured = computed(() => this.feedbackService.isConfigured());
53558
- this.props = {};
53559
- this.reactionSubmit = new EventEmitter();
53560
- this.reactionChange = new EventEmitter();
53561
- // Estado reactivo
53562
- this.state = signal({
53563
- selectedValue: null,
53564
- comment: '',
53565
- isLoading: false,
53566
- isSubmitted: false,
53567
- hadPreviousReaction: false,
53568
- error: null,
53569
- });
53570
- // Valores por defecto
53571
- this.defaultEmojis = ['😞', '😐', '😊'];
53572
- this.defaultLabels = ['No me ayudó', 'Regular', 'Muy útil'];
53573
- // Computed properties
53574
- this.resolvedProps = computed(() => ({
53575
- entityRef: this.props.entityRef,
53576
- question: this.props.question || this.t('question'),
53577
- showComment: this.props.showComment ?? true,
53578
- commentOnValues: this.props.commentOnValues,
53579
- commentPlaceholder: this.props.commentPlaceholder || this.t('commentPlaceholder'),
53580
- maxCommentLength: this.props.maxCommentLength ?? 500,
53581
- emojis: this.props.emojis || this.defaultEmojis,
53582
- emojiLabels: this.props.emojiLabels || this.defaultLabels,
53583
- showThankYou: this.props.showThankYou ?? true,
53584
- thankYouMessage: this.props.thankYouMessage || this.t('thankYou'),
53585
- disabled: this.props.disabled ?? false,
53586
- readonly: this.props.readonly ?? false,
53587
- // Variante: 'emoji' cuando se declara explícitamente o cuando se pasan emojis custom.
53588
- variant: this.props.variant ?? (this.props.emojis ? 'emoji' : 'buttons'),
53589
- }));
53590
- this.activeReactionValues = computed(() => this.resolvedProps().emojis.length === 2 ? ['negative', 'positive'] : ['negative', 'neutral', 'positive']);
53591
- this.showCommentField = computed(() => {
53592
- const sel = this.state().selectedValue;
53593
- if (!sel)
53594
- return false;
53595
- const commentOn = this.resolvedProps().commentOnValues;
53596
- if (commentOn !== undefined)
53597
- return commentOn.includes(sel);
53598
- return this.resolvedProps().showComment;
53599
- });
53600
- this.canSubmit = computed(() => this.state().selectedValue !== null && !this.state().isLoading);
53601
- // Auto-registro de defaults i18n (es/en) si el consumer no proveyó el
53602
- // namespace. Garantiza que `t()` nunca caiga en key faltante. Ver
53603
- // CONTENT_REACTION_I18N. Idempotente: solo la 1ª instancia registra.
53604
- if (!this.i18n.hasNamespace('ContentReaction')) {
53605
- this.i18n.registerContent('ContentReaction', CONTENT_REACTION_I18N);
53606
- }
53607
- }
53608
- ngOnInit() {
53609
- this.loadPreviousReaction();
53610
- }
53611
- ngOnChanges(changes) {
53612
- if (changes['props'] && !changes['props'].firstChange) {
53613
- this.loadPreviousReaction();
53614
- }
53615
- }
53616
- async loadPreviousReaction() {
53617
- if (!this.props.entityRef)
53618
- return;
53619
- // Si hay initialValue, usarlo directamente sin consultar
53620
- if (this.props.initialValue) {
53621
- this.state.update(s => ({
53622
- ...s,
53623
- selectedValue: this.props.initialValue,
53624
- hadPreviousReaction: true,
53625
- isLoading: false,
53626
- isSubmitted: true,
53627
- }));
53628
- return;
53629
- }
53630
- // Si skipInitialCheck está activo, no consultar
53631
- if (this.props.skipInitialCheck) {
53632
- return;
53633
- }
53634
- this.state.update(s => ({ ...s, isLoading: true, error: null }));
53635
- try {
53636
- const check = await this.feedbackService.checkFeedback(this.props.entityRef.entityType, this.props.entityRef.entityId);
53637
- if (check.hasFeedback && check.reactionValue) {
53638
- this.state.update(s => ({
53639
- ...s,
53640
- selectedValue: check.reactionValue,
53641
- hadPreviousReaction: true,
53642
- isLoading: false,
53643
- isSubmitted: true,
53644
- }));
53645
- }
53646
- else {
53647
- this.state.update(s => ({ ...s, isLoading: false }));
53648
- }
53649
- }
53650
- catch (error) {
53651
- console.error('Error loading previous reaction:', error);
53652
- this.state.update(s => ({ ...s, isLoading: false }));
53653
- }
53654
- }
53655
- selectReaction(value) {
53656
- if (this.resolvedProps().disabled || this.resolvedProps().readonly)
53657
- return;
53658
- const previousValue = this.state().selectedValue;
53659
- this.state.update(s => ({
53660
- ...s,
53661
- selectedValue: value,
53662
- isSubmitted: false,
53663
- error: null,
53664
- }));
53665
- this.reactionChange.emit({ value, previousValue });
53666
- }
53667
- async submitReaction() {
53668
- const currentState = this.state();
53669
- const props = this.resolvedProps();
53670
- if (!currentState.selectedValue || props.disabled)
53671
- return;
53672
- this.state.update(s => ({ ...s, isLoading: true, error: null }));
53673
- try {
53674
- // Determinar si usar endpoint anónimo o autenticado
53675
- const isAuthenticated = this.auth?.isAuthenticated() ?? false;
53676
- const useAnonymous = this.props.allowAnonymous && !isAuthenticated;
53677
- if (useAnonymous) {
53678
- // Usar endpoint anónimo (público)
53679
- await this.feedbackService.createAnonymousReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53680
- }
53681
- else {
53682
- // Usar endpoint autenticado
53683
- await this.feedbackService.createReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53684
- }
53685
- this.state.update(s => ({
53686
- ...s,
53687
- isLoading: false,
53688
- isSubmitted: true,
53689
- comment: '',
53690
- hadPreviousReaction: !useAnonymous, // Solo para autenticados
53691
- }));
53692
- this.reactionSubmit.emit({
53693
- value: currentState.selectedValue,
53694
- comment: currentState.comment || undefined,
53695
- entityRef: props.entityRef,
53696
- isUpdate: currentState.hadPreviousReaction,
53697
- });
53698
- if (props.showThankYou) {
53699
- this.toast.show({
53700
- message: props.thankYouMessage,
53701
- duration: 2000,
53702
- });
53703
- }
53704
- }
53705
- catch (error) {
53706
- console.error('Error submitting reaction:', error);
53707
- this.state.update(s => ({
53708
- ...s,
53709
- isLoading: false,
53710
- error: this.t('errorSubmitting'),
53711
- }));
53712
- this.toast.show({
53713
- message: this.t('errorSubmitting'),
53714
- duration: 3000,
53715
- });
53716
- }
53717
- }
53718
- updateComment(event) {
53719
- const value = event.detail.value || '';
53720
- this.state.update(s => ({ ...s, comment: value }));
53721
- }
53722
- getEmoji(index) {
53723
- return this.resolvedProps().emojis[index];
53724
- }
53725
- getEmojiLabel(index) {
53726
- return this.resolvedProps().emojiLabels[index];
53727
- }
53728
- isSelected(value) {
53729
- return this.state().selectedValue === value;
53730
- }
53731
- t(key) {
53732
- const translations = {
53733
- question: '¿Te resultó útil este contenido?',
53734
- commentPlaceholder: 'Cuéntanos más (opcional)...',
53735
- submit: 'Enviar opinión',
53736
- update: 'Actualizar opinión',
53737
- thankYou: '¡Gracias por tu opinión!',
53738
- submitted: 'Tu opinión ha sido registrada',
53739
- errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53740
- positiveLabel: 'Sí, mucho',
53741
- negativeLabel: 'No tanto',
53742
- negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53743
- };
53744
- return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
53745
- }
53746
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53747
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">\n {{ state().selectedValue === 'negative' ? t('negativeFeedbackTitle') : resolvedProps().question }}\n </p>\n\n <!-- Botones de reacci\u00F3n \u2014 variante emoji (caras, estrellas, thumbs, \u2026) -->\n @if (resolvedProps().variant === 'emoji') {\n <div class=\"reaction-buttons reaction-buttons--emoji\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <ion-button\n fill=\"clear\"\n shape=\"round\"\n [class.selected]=\"isSelected(value)\"\n [attr.aria-label]=\"resolvedProps().emojiLabels[i]\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji-btn\">{{ resolvedProps().emojis[i] }}</span>\n </ion-button>\n }\n </div>\n } @else {\n <!-- Botones de reacci\u00F3n \u2014 variante texto (default) -->\n <div class=\"reaction-buttons\">\n <ion-button\n [fill]=\"isSelected('positive') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('positive')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('positive')\"\n >\n {{ t('positiveLabel') }}\n </ion-button>\n <ion-button\n [fill]=\"isSelected('negative') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('negative')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('negative')\"\n >\n {{ t('negativeLabel') }}\n </ion-button>\n </div>\n }\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" shape=\"round\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:1.0625rem;font-weight:700;color:var(--ion-color-dark);margin:0 0 16px}.reaction-buttons{display:flex;justify-content:center;gap:12px;margin-bottom:20px}.reaction-buttons--emoji{gap:8px}.reaction-buttons--emoji ion-button{--padding-start: 4px;--padding-end: 4px;transition:transform .15s ease}.reaction-buttons--emoji ion-button.selected{transform:scale(1.25)}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]){opacity:.55}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]):hover{opacity:1}.emoji-btn{font-size:2rem;line-height:1;display:block}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: transparent;--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:.75rem;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:.875rem}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }] }); }
53748
- }
53749
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
53750
- type: Component,
53751
- args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">\n {{ state().selectedValue === 'negative' ? t('negativeFeedbackTitle') : resolvedProps().question }}\n </p>\n\n <!-- Botones de reacci\u00F3n \u2014 variante emoji (caras, estrellas, thumbs, \u2026) -->\n @if (resolvedProps().variant === 'emoji') {\n <div class=\"reaction-buttons reaction-buttons--emoji\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <ion-button\n fill=\"clear\"\n shape=\"round\"\n [class.selected]=\"isSelected(value)\"\n [attr.aria-label]=\"resolvedProps().emojiLabels[i]\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji-btn\">{{ resolvedProps().emojis[i] }}</span>\n </ion-button>\n }\n </div>\n } @else {\n <!-- Botones de reacci\u00F3n \u2014 variante texto (default) -->\n <div class=\"reaction-buttons\">\n <ion-button\n [fill]=\"isSelected('positive') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('positive')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('positive')\"\n >\n {{ t('positiveLabel') }}\n </ion-button>\n <ion-button\n [fill]=\"isSelected('negative') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('negative')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('negative')\"\n >\n {{ t('negativeLabel') }}\n </ion-button>\n </div>\n }\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" shape=\"round\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:1.0625rem;font-weight:700;color:var(--ion-color-dark);margin:0 0 16px}.reaction-buttons{display:flex;justify-content:center;gap:12px;margin-bottom:20px}.reaction-buttons--emoji{gap:8px}.reaction-buttons--emoji ion-button{--padding-start: 4px;--padding-end: 4px;transition:transform .15s ease}.reaction-buttons--emoji ion-button.selected{transform:scale(1.25)}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]){opacity:.55}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]):hover{opacity:1}.emoji-btn{font-size:2rem;line-height:1;display:block}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: transparent;--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:.75rem;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:.875rem}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
53752
- }], ctorParameters: () => [], propDecorators: { props: [{
53753
- type: Input
53754
- }], reactionSubmit: [{
53755
- type: Output
53756
- }], reactionChange: [{
53757
- type: Output
53758
- }] } });
53759
-
53760
- /**
53761
- * Defaults i18n (es/en) embebidos en `val-about-view`. Auto-registrados en el
53762
- * constructor del componente si el consumer no proveyó el namespace
53763
- * (`Settings.About` por default). SOLO incluye las keys que la vista usa.
53764
- *
53765
- * Textos portados EXACTOS del override `Settings.About` de showcase
53766
- * (`frontend/showcase/src/i18n/index.ts`). Las keys de las acciones
53767
- * showcase-specific (`feedback*`, `share*`, `faq*`, `donate*`) NO se incluyen:
53768
- * esas acciones se modelan ahora como `actions[]` de la config (el consumer
53769
- * aporta sus propios textos). `copyrightText` se incluye aquí (antes salía del
53770
- * namespace `Layout`) para que la vista sea autocontenida.
53771
- */
53772
- const ABOUT_VIEW_I18N = {
53773
- es: {
53774
- pageTitle: 'Acerca de',
53775
- pageDescription: 'Demo del factory Valtech',
53776
- tagline: 'App de demo — explora las funcionalidades en vivo',
53777
- descTitle: 'Acerca de Showcase',
53778
- descBody: 'Showcase es la app de referencia del factory Valtech. Aquí puedes explorar el flujo completo en vivo: autenticación, perfil, organizaciones, notificaciones y ajustes de cuenta.',
53779
- actionsTitle: 'Acciones',
53780
- rateTitle: 'Calificar la app',
53781
- rateDesc: 'Cuéntanos qué te ha parecido la experiencia.',
53782
- rateModalTitle: 'Tu opinión',
53783
- rateQuestion: '¿Qué te ha parecido la experiencia hasta ahora?',
53784
- rateComment: 'Cuéntanos más (opcional)…',
53785
- rateThanks: '¡Gracias por tu opinión!',
53786
- rateClose: 'Cerrar',
53787
- rateBad: 'No tanto',
53788
- rateMid: 'Bien',
53789
- rateGood: '¡Me encanta!',
53790
- socialTitle: 'Síguenos',
53791
- socialDesc: 'Estamos en redes sociales.',
53792
- versionLabel: 'Versión',
53793
- versionCopied: 'v{v} copiado',
53794
- copyrightText: '© {year}, Soluciones Valtech Ltda. o sus afiliados. Todos los derechos reservados.',
53795
- },
53796
- en: {
53797
- pageTitle: 'About',
53798
- pageDescription: 'Valtech factory demo',
53799
- tagline: 'Demo app — explore features live',
53800
- descTitle: 'About Showcase',
53801
- descBody: 'Showcase is the reference app for the Valtech factory. Explore the full flow live: authentication, profile, organizations, notifications and account settings.',
53802
- actionsTitle: 'Actions',
53803
- rateTitle: 'Rate the app',
53804
- rateDesc: 'Tell us how your experience has been.',
53805
- rateModalTitle: 'Your feedback',
53806
- rateQuestion: 'How has your experience been so far?',
53807
- rateComment: 'Tell us more (optional)…',
53808
- rateThanks: 'Thanks for your feedback!',
53809
- rateClose: 'Close',
53810
- rateBad: 'Not great',
53811
- rateMid: 'Good',
53812
- rateGood: 'Love it!',
53813
- socialTitle: 'Follow us',
53814
- socialDesc: "We're on social media.",
53815
- versionLabel: 'Version',
53816
- versionCopied: 'v{v} copied',
53817
- copyrightText: '© {year}, Soluciones Valtech Ltda. or its affiliates. All rights reserved.',
53818
- },
53819
- };
53820
-
53821
- addIcons({
53822
- copyOutline,
53823
- starOutline,
53824
- chatbubbleEllipsesOutline,
53825
- shareSocialOutline,
53826
- helpCircleOutline,
53827
- logoFacebook,
53828
- logoInstagram,
53829
- logoLinkedin,
53830
- logoTwitter,
53831
- logoYoutube,
53832
- logoTiktok,
53833
- });
53834
- const DEFAULT_NAMESPACE = 'Settings.About';
53835
- const DEFAULT_BRAND_NAME = 'Valtech';
53836
- const DEFAULT_LOGO_SRC = 'assets/images/main-icon.png';
53837
- const DEFAULT_RATING_ENTITY_ID = 'experience';
53838
- /**
53839
- * `val-about-view` — vista About full-feature autocontenida (organism).
53840
- * Promovida desde `showcase` bajo el proceso de ADR-021. Reúne:
53841
- * - **HERO** — logo + nombre de marca + tagline + pill de versión (copiable).
53842
- * - **Descripción** — título + cuerpo.
53843
- * - **Acciones** — action-card de rating built-in (abre `val-content-reaction`
53844
- * DIRECTAMENTE vía `ModalService`, sin wrapper local — decisión CTO) + las
53845
- * acciones genéricas que el consumer aporte por `config.actions`.
53846
- * - **Redes** — links sociales (`config.socialLinks`); sección oculta si vacía.
53847
- * - **Footer** — copyright con el año actual.
53848
- *
53849
- * Lo app-specific (logo, nombre, versión, redes, acciones share/faq) viaja como
53850
- * DATA de config — NO como slots (ADR-021). La versión la pasa el consumer
53851
- * (`config.version` = su `APP_VERSION`); la lib NO importa el `version.ts` de la
53852
- * app. Si no se pasa versión, el pill se oculta.
53853
- *
53854
- * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute`
53855
- * se inyecta `{ optional: true }` solo para leer el route data de config.
53856
- *
53857
- * Auto-registra sus defaults i18n (es/en) en el constructor si el consumer no
53858
- * proveyó el namespace configurado (default `Settings.About`).
53859
- *
53860
- * GOTCHA i18n ContentReaction: `val-content-reaction` exige su namespace
53861
- * `ContentReaction` registrado donde se use (key faltante en binding + CD =
53862
- * navegador congelado en dev). `ContentReactionComponent` se auto-registra ese
53863
- * namespace en SU constructor, y como se abre vía `ModalService` ese constructor
53864
- * corre antes de que su template binde — queda cubierto. Igual lo importamos
53865
- * como dependencia explícita de esta vista para que la garantía sea local.
53866
- */
53867
- class AboutViewComponent {
53868
- /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
53869
- get ns() {
53870
- return this.resolvedConfig().i18nNamespace;
53871
- }
53872
- constructor() {
53873
- this.nav = inject(NavigationService);
53874
- this.i18n = inject(I18nService);
53875
- this.modalService = inject(ModalService);
53876
- this.toast = inject(ToastService);
53877
- this.route = inject(ActivatedRoute, { optional: true });
53878
- this.resolvedConfig = computed(() => {
53879
- const fromRoute = (this.route?.snapshot.data['aboutConfig'] ?? {});
53880
- const merged = { ...fromRoute, ...(this.config ?? {}) };
53881
- return {
53882
- brand: {
53883
- name: merged.brand?.name ?? DEFAULT_BRAND_NAME,
53884
- logoSrc: merged.brand?.logoSrc ?? DEFAULT_LOGO_SRC,
53885
- },
53886
- version: merged.version ?? '',
53887
- socialLinks: merged.socialLinks ?? [],
53888
- showRating: merged.showRating ?? true,
53889
- ratingEntityId: merged.ratingEntityId ?? DEFAULT_RATING_ENTITY_ID,
53890
- actions: merged.actions ?? [],
53891
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
53892
- onRated: merged.onRated,
53893
- };
53894
- });
53895
- this.brandName = computed(() => this.resolvedConfig().brand.name);
53896
- this.logoSrc = computed(() => this.resolvedConfig().brand.logoSrc);
53897
- this.version = computed(() => this.resolvedConfig().version);
53898
- this.socialLinks = computed(() => this.resolvedConfig().socialLinks);
53899
- this.showRating = computed(() => this.resolvedConfig().showRating);
53900
- this.actions = computed(() => this.resolvedConfig().actions);
53901
- /** Copyright del footer — año actual interpolado en `copyrightText`. */
53902
- this.copyright = computed(() => {
53903
- this.i18n.lang();
53904
- const year = String(new Date().getFullYear());
53905
- return this.tt('copyrightText').replace('{year}', year);
53906
- });
53907
- /** Title metadata para secciones — bold + size small. */
53908
- this.rateActionCard = computed(() => {
53909
- this.i18n.lang();
53910
- return {
53911
- icon: { ionicon: 'star-outline', color: 'primary' },
53912
- title: this.tt('rateTitle'),
53913
- description: this.tt('rateDesc'),
53914
- size: 'medium',
53915
- bordered: false,
53916
- shadowed: false,
53917
- };
53918
- });
53919
- const ns = this.ns;
53920
- if (!this.i18n.hasNamespace(ns)) {
53921
- this.i18n.registerContent(ns, ABOUT_VIEW_I18N);
53922
- }
53923
- this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
53924
- }
53925
- /** Helper i18n bound al namespace resuelto. Reactivo vía `lang()`. */
53926
- tt(key) {
53927
- this.i18n.lang();
53928
- return this.i18n.t(key, this.ns);
53929
- }
53930
- /** Title metadata para secciones — bold + size small. */
53931
- sectionTitle(key) {
53932
- this.i18n.lang();
53933
- return {
53934
- content: this.tt(key),
53935
- size: 'small',
53936
- color: 'dark',
53937
- bold: true,
53938
- };
53939
- }
53940
- /** ActionCardMetadata para una acción genérica del consumer. */
53941
- extraActionCard(action) {
53942
- return {
53943
- icon: { ionicon: action.icon ?? 'chatbubble-ellipses-outline', color: 'primary' },
53944
- title: action.label,
53945
- description: action.description,
53946
- size: 'medium',
53947
- bordered: false,
53948
- shadowed: false,
53949
- };
53950
- }
53951
- /** Copia la versión al portapapeles + toast (color dark). */
53952
- async copyVersion() {
53953
- const v = this.version();
53954
- if (!v)
53955
- return;
53956
- try {
53957
- await navigator.clipboard.writeText(v);
53958
- this.toast.show({
53959
- message: this.tt('versionCopied').replace('{v}', v),
53960
- color: 'dark',
53961
- duration: 1500,
53962
- });
53963
- }
53964
- catch {
53965
- // clipboard no disponible — silencioso
53966
- }
53967
- }
53968
- /** Abre una URL externa en nueva tab. `mailto:` queda en la misma tab. */
53969
- openUrl(url) {
53970
- if (url.startsWith('mailto:')) {
53971
- window.location.href = url;
53972
- return;
53973
- }
53974
- window.open(url, '_blank', 'noopener,noreferrer');
53975
- }
53976
- /**
53977
- * "Calificar la app" — abre `val-content-reaction` DIRECTAMENTE como sheet
53978
- * modal (sin wrapper local — decisión CTO). El componente persiste la reacción
53979
- * contra el backend de feedback y muestra su propio estado de "gracias"; el
53980
- * sheet se cierra arrastrando (breakpoint 0). El namespace i18n
53981
- * `ContentReaction` lo auto-registra `ContentReactionComponent` en su
53982
- * constructor, que corre al crear el modal — sin riesgo de key faltante.
53983
- *
53984
- * Sheet (bottom sheet): el contenido scrollea dentro y el botón "Enviar" queda
53985
- * siempre alcanzable, también en viewports chicos.
53986
- */
53987
- async openRating() {
53988
- const reactionProps = {
53989
- // entityType "app" distingue esta reacción de las de artículos/FAQs.
53990
- entityRef: { entityType: 'app', entityId: this.resolvedConfig().ratingEntityId },
53991
- question: this.tt('rateQuestion'),
53992
- commentPlaceholder: this.tt('rateComment'),
53993
- thankYouMessage: this.tt('rateThanks'),
53994
- showComment: true,
53995
- showThankYou: true,
53996
- // Modal one-shot: NO bloquear el render con el check de rating previo.
53997
- skipInitialCheck: true,
53998
- emojis: ['😕', '🙂', '😍'],
53999
- emojiLabels: [this.tt('rateBad'), this.tt('rateMid'), this.tt('rateGood')],
54000
- };
54001
- await this.modalService.open({
54002
- component: ContentReactionComponent,
54003
- componentProps: { props: reactionProps },
54004
- breakpoints: {
54005
- initialBreakpoint: 0.9,
54006
- breakpoints: [0, 0.9, 1],
54007
- showHandle: true,
54008
- handleBehavior: 'cycle',
54009
- },
54010
- });
54011
- this.resolvedConfig().onRated?.();
54012
- }
54013
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
54014
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AboutViewComponent, isStandalone: true, selector: "val-about-view", inputs: { config: "config" }, ngImport: i0, template: `
54015
- <ion-grid class="about-grid">
54016
- <!-- HERO: logo + name + version -->
54017
- <div class="about-hero">
54018
- <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54019
- <val-title
54020
- [props]="{
54021
- content: brandName(),
54022
- size: 'xlarge',
54023
- color: 'dark',
54024
- bold: true,
54025
- }"
54026
- />
54027
- <val-text
54028
- [props]="{
54029
- content: tt('tagline'),
54030
- size: 'medium',
54031
- color: 'dark',
54032
- bold: false,
54033
- }"
54034
- />
54035
- @if (version()) {
54036
- <div
54037
- class="version-row"
54038
- (click)="copyVersion()"
54039
- role="button"
54040
- [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54041
- >
54042
- <val-pill
54043
- [props]="{
54044
- label: tt('versionLabel') + ' ' + version(),
54045
- color: 'dark',
54046
- weight: 'bold',
54047
- size: 'small',
54048
- }"
54049
- />
54050
- <ion-icon name="copy-outline" class="version-copy-icon" />
54051
- </div>
54052
- }
54053
- </div>
54054
-
54055
- <!-- DESCRIPTION -->
54056
- <section class="about-section">
54057
- <val-title [props]="sectionTitle('descTitle')" />
54058
- <val-text
54059
- [props]="{
54060
- content: tt('descBody'),
54061
- size: 'medium',
54062
- color: 'dark',
54063
- bold: false,
54064
- }"
54065
- />
54066
- </section>
54067
-
54068
- <!-- ACTIONS — rating built-in + acciones del consumer. -->
54069
- @if (showRating() || actions().length > 0) {
54070
- <section class="about-section about-section--reviews">
54071
- <val-title [props]="sectionTitle('actionsTitle')" />
54072
- <div class="about-actions">
54073
- @if (showRating()) {
54074
- <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54075
- }
54076
- @for (action of actions(); track action.label) {
54077
- <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54078
- }
54079
- </div>
54080
- </section>
54081
- }
54082
-
54083
- <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54084
- @if (socialLinks().length > 0) {
54085
- <section class="about-section">
54086
- <val-title [props]="sectionTitle('socialTitle')" />
54087
- <val-text
54088
- [props]="{
54089
- content: tt('socialDesc'),
54090
- size: 'medium',
54091
- color: 'dark',
54092
- bold: false,
54093
- }"
54094
- />
54095
- <div class="about-social">
54096
- @for (s of socialLinks(); track s.url) {
54097
- <ion-button
54098
- fill="clear"
54099
- size="small"
54100
- class="social-btn"
54101
- [attr.aria-label]="s.label"
54102
- (click)="openUrl(s.url)"
54103
- >
54104
- <ion-icon [name]="s.icon" slot="icon-only" />
54105
- </ion-button>
54106
- }
54107
- </div>
54108
- </section>
54109
- }
54110
-
54111
- <!-- FOOTER -->
54112
- <footer class="about-footer">
54113
- <val-text
54114
- [props]="{
54115
- content: copyright(),
54116
- size: 'small',
54117
- color: 'dark',
54118
- bold: false,
54119
- }"
54120
- />
54121
- </footer>
54122
- </ion-grid>
54123
- `, isInline: true, styles: [":host{display:block}.about-grid{padding:16px 0;max-width:720px;margin:0 auto}.about-hero{display:flex;flex-direction:column;align-items:center;gap:8px;padding:24px 0 16px;text-align:center}.about-hero__logo{height:96px;width:auto;display:block}.version-row{display:flex;align-items:center;gap:6px;cursor:pointer;opacity:.85;transition:opacity .15s ease}.version-row:hover{opacity:1}.version-copy-icon{font-size:14px;color:var(--ion-color-medium)}.about-section{padding:16px 0 8px;display:flex;flex-direction:column;gap:8px}.about-actions{display:flex;flex-direction:column;gap:8px}.about-section--reviews val-action-card{display:block}.about-section--reviews val-action-card ::ng-deep .action-card{background:var(--ion-color-light, #f4f5f8);box-shadow:none}.about-section--reviews val-action-card ::ng-deep .action-card__description{color:var(--ion-color-dark);opacity:.7}.about-social{display:flex;flex-wrap:wrap;gap:10px;padding-top:8px}.about-social .social-btn{--border-radius: 50%;--background: var(--ion-color-light, #f4f5f8);--color: var(--ion-color-dark);--padding-start: 0;--padding-end: 0;width:48px;height:48px;margin:0;transition:transform .15s ease}.about-social .social-btn:hover{--background: var(--ion-color-primary-tint, #8a4ce5);--color: #fff;transform:translateY(-2px)}.about-social .social-btn ion-icon{font-size:22px}.about-footer{display:flex;flex-direction:column;align-items:center;gap:4px;padding:24px 0;text-align:center}:host-context(body.dark) val-action-card ::ng-deep .action-card,:host-context(html.ion-palette-dark) val-action-card ::ng-deep .action-card,:host-context([data-theme=\"dark\"]) val-action-card ::ng-deep .action-card{background:#ffffff0a}:host-context(body.dark) .about-section--reviews val-action-card ::ng-deep .action-card:hover,:host-context(html.ion-palette-dark) .about-section--reviews val-action-card ::ng-deep .action-card:hover,:host-context([data-theme=\"dark\"]) .about-section--reviews val-action-card ::ng-deep .action-card:hover{background:#ffffff14}:host-context(body.dark) .about-social .social-btn,:host-context(html.ion-palette-dark) .about-social .social-btn,:host-context([data-theme=\"dark\"]) .about-social .social-btn{--background: rgba(255, 255, 255, .04)}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: PillComponent, selector: "val-pill", inputs: ["preset", "props"], outputs: ["pillClick", "pillAction"] }, { kind: "component", type: TitleComponent, selector: "val-title", inputs: ["props"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ActionCardComponent, selector: "val-action-card", inputs: ["props"], outputs: ["onClick"] }] }); }
54124
- }
54125
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, decorators: [{
54126
- type: Component,
54127
- args: [{ selector: 'val-about-view', standalone: true, imports: [IonButton, IonGrid, IonIcon, PillComponent, TitleComponent, TextComponent, ActionCardComponent], template: `
54128
- <ion-grid class="about-grid">
54129
- <!-- HERO: logo + name + version -->
54130
- <div class="about-hero">
54131
- <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54132
- <val-title
54133
- [props]="{
54134
- content: brandName(),
54135
- size: 'xlarge',
54136
- color: 'dark',
54137
- bold: true,
54138
- }"
54139
- />
54140
- <val-text
54141
- [props]="{
54142
- content: tt('tagline'),
54143
- size: 'medium',
54144
- color: 'dark',
54145
- bold: false,
54146
- }"
54147
- />
54148
- @if (version()) {
54149
- <div
54150
- class="version-row"
54151
- (click)="copyVersion()"
54152
- role="button"
54153
- [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54154
- >
54155
- <val-pill
54156
- [props]="{
54157
- label: tt('versionLabel') + ' ' + version(),
54158
- color: 'dark',
54159
- weight: 'bold',
54160
- size: 'small',
54161
- }"
54162
- />
54163
- <ion-icon name="copy-outline" class="version-copy-icon" />
54164
- </div>
54165
- }
54166
- </div>
54167
-
54168
- <!-- DESCRIPTION -->
54169
- <section class="about-section">
54170
- <val-title [props]="sectionTitle('descTitle')" />
54171
- <val-text
54172
- [props]="{
54173
- content: tt('descBody'),
54174
- size: 'medium',
54175
- color: 'dark',
54176
- bold: false,
54177
- }"
54178
- />
54179
- </section>
54180
-
54181
- <!-- ACTIONS — rating built-in + acciones del consumer. -->
54182
- @if (showRating() || actions().length > 0) {
54183
- <section class="about-section about-section--reviews">
54184
- <val-title [props]="sectionTitle('actionsTitle')" />
54185
- <div class="about-actions">
54186
- @if (showRating()) {
54187
- <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54188
- }
54189
- @for (action of actions(); track action.label) {
54190
- <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54191
- }
54192
- </div>
54193
- </section>
54194
- }
54195
-
54196
- <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54197
- @if (socialLinks().length > 0) {
54198
- <section class="about-section">
54199
- <val-title [props]="sectionTitle('socialTitle')" />
54200
- <val-text
54201
- [props]="{
54202
- content: tt('socialDesc'),
54203
- size: 'medium',
54204
- color: 'dark',
54205
- bold: false,
54206
- }"
54207
- />
54208
- <div class="about-social">
54209
- @for (s of socialLinks(); track s.url) {
54210
- <ion-button
54211
- fill="clear"
54212
- size="small"
54213
- class="social-btn"
54214
- [attr.aria-label]="s.label"
54215
- (click)="openUrl(s.url)"
54216
- >
54217
- <ion-icon [name]="s.icon" slot="icon-only" />
54218
- </ion-button>
54219
- }
54220
- </div>
54221
- </section>
54222
- }
54223
-
54224
- <!-- FOOTER -->
54225
- <footer class="about-footer">
54226
- <val-text
54227
- [props]="{
54228
- content: copyright(),
54229
- size: 'small',
54230
- color: 'dark',
54231
- bold: false,
54232
- }"
54233
- />
54234
- </footer>
54235
- </ion-grid>
54236
- `, styles: [":host{display:block}.about-grid{padding:16px 0;max-width:720px;margin:0 auto}.about-hero{display:flex;flex-direction:column;align-items:center;gap:8px;padding:24px 0 16px;text-align:center}.about-hero__logo{height:96px;width:auto;display:block}.version-row{display:flex;align-items:center;gap:6px;cursor:pointer;opacity:.85;transition:opacity .15s ease}.version-row:hover{opacity:1}.version-copy-icon{font-size:14px;color:var(--ion-color-medium)}.about-section{padding:16px 0 8px;display:flex;flex-direction:column;gap:8px}.about-actions{display:flex;flex-direction:column;gap:8px}.about-section--reviews val-action-card{display:block}.about-section--reviews val-action-card ::ng-deep .action-card{background:var(--ion-color-light, #f4f5f8);box-shadow:none}.about-section--reviews val-action-card ::ng-deep .action-card__description{color:var(--ion-color-dark);opacity:.7}.about-social{display:flex;flex-wrap:wrap;gap:10px;padding-top:8px}.about-social .social-btn{--border-radius: 50%;--background: var(--ion-color-light, #f4f5f8);--color: var(--ion-color-dark);--padding-start: 0;--padding-end: 0;width:48px;height:48px;margin:0;transition:transform .15s ease}.about-social .social-btn:hover{--background: var(--ion-color-primary-tint, #8a4ce5);--color: #fff;transform:translateY(-2px)}.about-social .social-btn ion-icon{font-size:22px}.about-footer{display:flex;flex-direction:column;align-items:center;gap:4px;padding:24px 0;text-align:center}:host-context(body.dark) val-action-card ::ng-deep .action-card,:host-context(html.ion-palette-dark) val-action-card ::ng-deep .action-card,:host-context([data-theme=\"dark\"]) val-action-card ::ng-deep .action-card{background:#ffffff0a}:host-context(body.dark) .about-section--reviews val-action-card ::ng-deep .action-card:hover,:host-context(html.ion-palette-dark) .about-section--reviews val-action-card ::ng-deep .action-card:hover,:host-context([data-theme=\"dark\"]) .about-section--reviews val-action-card ::ng-deep .action-card:hover{background:#ffffff14}:host-context(body.dark) .about-social .social-btn,:host-context(html.ion-palette-dark) .about-social .social-btn,:host-context([data-theme=\"dark\"]) .about-social .social-btn{--background: rgba(255, 255, 255, .04)}\n"] }]
54237
- }], ctorParameters: () => [], propDecorators: { config: [{
54238
- type: Input
54239
- }] } });
54240
-
54241
- /**
54242
- * Configuración acotada de `val-about-view` (object-first). La vista About es
54243
- * app-specific por naturaleza (logo, nombre, versión, redes, acciones extra),
54244
- * así que lo propio de cada app se parametriza como **DATA de config** — NO como
54245
- * slots arbitrarios (ADR-021: una vista full-feature no abre slots; si falta
54246
- * algo, se promueve un nuevo punto de extensión).
54247
- *
54248
- * Ejes de variación permitidos:
54249
- * - **branding** — `brand.name` / `brand.logoSrc` (qué muestra el HERO).
54250
- * - **versión** — `version` la pasa el consumer (la lib NO puede importar el
54251
- * `version.ts` de la app). Si no se pasa, se oculta el pill de versión.
54252
- * - **redes** — `socialLinks`; si está vacío se oculta la sección social.
54253
- * - **rating** — `showRating` + `ratingEntityId` + `onRated`.
54254
- * - **acciones extra** — `actions[]` genéricas que el consumer agrega junto a
54255
- * la acción de rating built-in.
54256
- * - **branding i18n** — `i18nNamespace` (default `'Settings.About'`).
54257
- */
54258
-
54259
- /**
54260
- * Helper para montar la vista About (`val-about-view`) como ruta en una app del
54261
- * factory. El `config` se pasa por route `data` (`aboutConfig`) y el componente
54262
- * lo lee como fallback de su `@Input() config` (el `@Input` gana).
54263
- *
54264
- * @example
54265
- * ```ts
54266
- * // settings.routes.ts
54267
- * import { APP_VERSION } from '../version';
54268
- *
54269
- * export const settingsRoutes: Routes = [
54270
- * ...provideValtechAboutRoutes({ config: { version: APP_VERSION } }), // → /app/about
54271
- * { path: 'preferences', loadComponent: () => ... },
54272
- * ];
54273
- *
54274
- * // con redes y acciones extra:
54275
- * ...provideValtechAboutRoutes({
54276
- * config: {
54277
- * version: APP_VERSION,
54278
- * socialLinks: VALTECH_SOCIAL_LINKS.map(s => ({ icon: s.icon, url: s.url, label: s.name })),
54279
- * actions: [{ label: 'Compartir', icon: 'share-social-outline', onClick: () => share() }],
54280
- * },
54281
- * }),
54282
- * ```
54283
- */
54284
- function provideValtechAboutRoutes(opts) {
54285
- return [
54286
- {
54287
- path: opts?.path ?? 'about',
54288
- component: AboutViewComponent,
54289
- data: { aboutConfig: opts?.config },
54290
- },
54291
- ];
54292
- }
54293
-
54294
52500
  /** Built-in label sets for the three platform locales. */
54295
52501
  const CALLOUT_LABELS = {
54296
52502
  es: {
@@ -55827,7 +54033,7 @@ class AuthBackgroundComponent {
55827
54033
  this.config = computed(() => {
55828
54034
  const props = this._props();
55829
54035
  return {
55830
- waveCount: props.waveCount ?? 45,
54036
+ waveCount: props.waveCount ?? 16,
55831
54037
  waveColors: props.waveColors ?? this.defaultColors,
55832
54038
  showGrain: props.showGrain !== false,
55833
54039
  showGlass: props.showGlass !== false,
@@ -55844,14 +54050,22 @@ class AuthBackgroundComponent {
55844
54050
  const newWaves = Array.from({ length: count }, (_, i) => {
55845
54051
  // Distribuir uniforme a lo largo del viewBox (0..800) — sin huecos arriba
55846
54052
  const y = ((i + 0.5) / count) * 800;
55847
- const isThick = i % 5 === 0;
55848
- const isMedium = i % 3 === 0 && !isThick;
55849
- const amplitude = 10 + Math.random() * 20;
55850
- const frequency = 1 + Math.random() * 2;
54053
+ // Tiers (look "aurora"): pocas hero gruesas y luminosas, algunas mid, el
54054
+ // resto tenues de fondo. Acentos de marca en las hero + alguna suelta.
54055
+ const isHero = i % 5 === 0;
54056
+ const isMedium = !isHero && i % 2 === 0;
54057
+ const accent = isHero || i % 7 === 3;
54058
+ // Hero más amplias para cintas que "respiran"; el resto, contenidas.
54059
+ const amplitude = isHero ? 22 + Math.random() * 18 : 10 + Math.random() * 16;
54060
+ // Frecuencia ENTERA (ciclos completos sobre el ancho 1440). Imprescindible
54061
+ // para el loop seamless: con freq entero, path(x) === path(x ± 1440), así
54062
+ // el translateX de una vuelta (1440px) calza exacto y NO hay salto/corte.
54063
+ const frequency = 1 + Math.floor(Math.random() * 3); // 1 | 2 | 3
55851
54064
  const phase = Math.random() * Math.PI * 2;
55852
- // Path extendido off-canvas (-400 .. 1840) para que translateX no deje huecos
55853
- let path = `M-400,${y}`;
55854
- for (let x = -400; x <= 1840; x += 40) {
54065
+ // Path extendido a -1480 .. 2920 (≥ una vuelta de bleed a cada lado) para
54066
+ // que el desplazamiento de ±1440px nunca deje el viewBox sin onda.
54067
+ let path = `M-1480,${y}`;
54068
+ for (let x = -1480; x <= 2920; x += 40) {
55855
54069
  const waveY = y + amplitude * Math.sin((x / 1440) * Math.PI * frequency * 2 + phase);
55856
54070
  path += ` L${x},${waveY.toFixed(1)}`;
55857
54071
  }
@@ -55860,14 +54074,16 @@ class AuthBackgroundComponent {
55860
54074
  id: i,
55861
54075
  path,
55862
54076
  color,
55863
- thickness: isThick ? 1.2 : isMedium ? 0.7 : 0.3,
55864
- opacity: isThick ? 0.5 : isMedium ? 0.4 : 0.3,
55865
- // Pulse vertical (equalizer) — ~1/3 de las waves son "rápidas"
55866
- pulseDuration: Math.random() < 0.33 ? 0.8 + Math.random() * 0.8 : 1.6 + Math.random() * 2.4,
55867
- pulseDelay: -Math.random() * 4,
55868
- // Traveling horizontal ~1/3 más rápidas para sensación de ecualizador
55869
- travelDuration: Math.random() < 0.33 ? 5 + Math.random() * 4 : 10 + Math.random() * 12,
55870
- travelDelay: -Math.random() * 12,
54077
+ thickness: isHero ? 2.2 + Math.random() * 0.8 : isMedium ? 1 : 0.5,
54078
+ opacity: isHero ? 0.55 : isMedium ? 0.3 : 0.16,
54079
+ glow: isHero,
54080
+ accent,
54081
+ // Drift vertical sutil (no "equalizer"): da vida sin saltos.
54082
+ bob: 4 + Math.random() * 10,
54083
+ // Flujo horizontal continuo. Duraciones variadas = parallax/profundidad.
54084
+ // delay negativo desincroniza el arranque de cada capa.
54085
+ travelDuration: 14 + Math.random() * 22,
54086
+ travelDelay: -Math.random() * 30,
55871
54087
  travelDirection: i % 2 === 0 ? 'right' : 'left',
55872
54088
  };
55873
54089
  });
@@ -55884,6 +54100,16 @@ class AuthBackgroundComponent {
55884
54100
  }
55885
54101
  <div class="wave-bg">
55886
54102
  <svg class="waves" viewBox="0 0 1440 800" preserveAspectRatio="none">
54103
+ <defs>
54104
+ <!-- Halo: blur por debajo del trazo nítido = cinta luminosa. -->
54105
+ <filter id="wave-glow" x="-10%" y="-60%" width="120%" height="220%">
54106
+ <feGaussianBlur stdDeviation="2.6" result="b" />
54107
+ <feMerge>
54108
+ <feMergeNode in="b" />
54109
+ <feMergeNode in="SourceGraphic" />
54110
+ </feMerge>
54111
+ </filter>
54112
+ </defs>
55887
54113
  @for (wave of waves(); track wave.id) {
55888
54114
  <path
55889
54115
  [attr.d]="wave.path"
@@ -55891,9 +54117,10 @@ class AuthBackgroundComponent {
55891
54117
  [attr.stroke-width]="wave.thickness"
55892
54118
  [class.travel-left]="wave.travelDirection === 'left'"
55893
54119
  [class.travel-right]="wave.travelDirection === 'right'"
54120
+ [class.glow]="wave.glow"
54121
+ [class.accent]="wave.accent"
55894
54122
  [style.opacity]="wave.opacity"
55895
- [style.--pulse-duration]="wave.pulseDuration + 's'"
55896
- [style.--pulse-delay]="wave.pulseDelay + 's'"
54123
+ [style.--bob]="wave.bob + 'px'"
55897
54124
  [style.--travel-duration]="wave.travelDuration + 's'"
55898
54125
  [style.--travel-delay]="wave.travelDelay + 's'"
55899
54126
  />
@@ -55905,7 +54132,7 @@ class AuthBackgroundComponent {
55905
54132
  <ng-content></ng-content>
55906
54133
  </div>
55907
54134
  </section>
55908
- `, isInline: true, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
54135
+ `, isInline: true, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible;-webkit-mask-image:linear-gradient(to right,transparent 0%,#000 16%,#000 84%,transparent 100%);mask-image:linear-gradient(to right,transparent 0%,#000 16%,#000 84%,transparent 100%)}.waves path{fill:none;stroke-linecap:round;will-change:transform;animation:wave-flow-right var(--travel-duration, 18s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-flow-left var(--travel-duration, 18s) linear var(--travel-delay, 0s) infinite}.waves path.accent{stroke:var(--auth-wave-color)}.waves path.glow{filter:url(#wave-glow)}@keyframes wave-flow-right{0%{transform:translate(0)}25%{transform:translate(-360px,calc(var(--bob, 0px) * -1))}50%{transform:translate(-720px)}75%{transform:translate(-1080px,var(--bob, 0px))}to{transform:translate(-1440px)}}@keyframes wave-flow-left{0%{transform:translate(0)}25%{transform:translate(360px,var(--bob, 0px))}50%{transform:translate(720px)}75%{transform:translate(1080px,calc(var(--bob, 0px) * -1))}to{transform:translate(1440px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
55909
54136
  }
55910
54137
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AuthBackgroundComponent, decorators: [{
55911
54138
  type: Component,
@@ -55919,6 +54146,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55919
54146
  }
55920
54147
  <div class="wave-bg">
55921
54148
  <svg class="waves" viewBox="0 0 1440 800" preserveAspectRatio="none">
54149
+ <defs>
54150
+ <!-- Halo: blur por debajo del trazo nítido = cinta luminosa. -->
54151
+ <filter id="wave-glow" x="-10%" y="-60%" width="120%" height="220%">
54152
+ <feGaussianBlur stdDeviation="2.6" result="b" />
54153
+ <feMerge>
54154
+ <feMergeNode in="b" />
54155
+ <feMergeNode in="SourceGraphic" />
54156
+ </feMerge>
54157
+ </filter>
54158
+ </defs>
55922
54159
  @for (wave of waves(); track wave.id) {
55923
54160
  <path
55924
54161
  [attr.d]="wave.path"
@@ -55926,9 +54163,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55926
54163
  [attr.stroke-width]="wave.thickness"
55927
54164
  [class.travel-left]="wave.travelDirection === 'left'"
55928
54165
  [class.travel-right]="wave.travelDirection === 'right'"
54166
+ [class.glow]="wave.glow"
54167
+ [class.accent]="wave.accent"
55929
54168
  [style.opacity]="wave.opacity"
55930
- [style.--pulse-duration]="wave.pulseDuration + 's'"
55931
- [style.--pulse-delay]="wave.pulseDelay + 's'"
54169
+ [style.--bob]="wave.bob + 'px'"
55932
54170
  [style.--travel-duration]="wave.travelDuration + 's'"
55933
54171
  [style.--travel-delay]="wave.travelDelay + 's'"
55934
54172
  />
@@ -55940,7 +54178,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
55940
54178
  <ng-content></ng-content>
55941
54179
  </div>
55942
54180
  </section>
55943
- `, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible}.waves path{fill:none;transform-box:fill-box;transform-origin:center;will-change:transform;animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-right var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-pulse var(--pulse-duration, 3s) ease-in-out var(--pulse-delay, 0s) infinite alternate,wave-travel-left var(--travel-duration, 12s) linear var(--travel-delay, 0s) infinite}@keyframes wave-pulse{0%{transform:scaleY(.85)}50%{transform:scaleY(1.25)}to{transform:scaleY(.85)}}@keyframes wave-travel-right{0%{transform:translate(-300px)}to{transform:translate(300px)}}@keyframes wave-travel-left{0%{transform:translate(300px)}to{transform:translate(-300px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"] }]
54181
+ `, styles: [":host{display:block;--auth-bg-start: #ffffff;--auth-bg-mid: #f8f9fa;--auth-bg-end: #f0f0f0;--auth-glass-bg: rgba(255, 255, 255, .3);--auth-grain-opacity: .035;--auth-wave-color: var(--ion-color-primary, #6366f1)}:host-context(body.dark),:host-context(.dark),:host-context(.ion-palette-dark),:host-context(html.ion-palette-dark),:host-context([data-theme=\"dark\"]){--auth-bg-start: #0f0f1a;--auth-bg-mid: #1a1a2e;--auth-bg-end: #16213e;--auth-glass-bg: rgba(0, 0, 0, .3);--auth-grain-opacity: .05;--auth-wave-color: var(--ion-color-primary, #818cf8)}.auth-background{position:relative;display:flex;align-items:center;padding:0;background:linear-gradient(135deg,var(--auth-bg-start) 0%,var(--auth-bg-mid) 50%,var(--auth-bg-end) 100%);overflow:hidden}.grain{position:absolute;inset:0;pointer-events:none;z-index:0;opacity:var(--auth-grain-opacity);background-image:url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E\")}.glass-layer{position:absolute;inset:0;pointer-events:none;z-index:1;background:var(--auth-glass-bg);backdrop-filter:blur(3px) saturate(120%);-webkit-backdrop-filter:blur(3px) saturate(120%)}.wave-bg{position:absolute;inset:0;pointer-events:none;z-index:0;transition:filter .3s ease}:host-context(body.dark) .wave-bg,:host-context(.dark) .wave-bg,:host-context(.ion-palette-dark) .wave-bg,:host-context(html.ion-palette-dark) .wave-bg,:host-context([data-theme=\"dark\"]) .wave-bg{filter:invert(1) opacity(.6)}.waves{width:100%;height:100%;overflow:visible;-webkit-mask-image:linear-gradient(to right,transparent 0%,#000 16%,#000 84%,transparent 100%);mask-image:linear-gradient(to right,transparent 0%,#000 16%,#000 84%,transparent 100%)}.waves path{fill:none;stroke-linecap:round;will-change:transform;animation:wave-flow-right var(--travel-duration, 18s) linear var(--travel-delay, 0s) infinite}.waves path.travel-left{animation:wave-flow-left var(--travel-duration, 18s) linear var(--travel-delay, 0s) infinite}.waves path.accent{stroke:var(--auth-wave-color)}.waves path.glow{filter:url(#wave-glow)}@keyframes wave-flow-right{0%{transform:translate(0)}25%{transform:translate(-360px,calc(var(--bob, 0px) * -1))}50%{transform:translate(-720px)}75%{transform:translate(-1080px,var(--bob, 0px))}to{transform:translate(-1440px)}}@keyframes wave-flow-left{0%{transform:translate(0)}25%{transform:translate(360px,var(--bob, 0px))}50%{transform:translate(720px)}75%{transform:translate(1080px,calc(var(--bob, 0px) * -1))}to{transform:translate(1440px)}}@media (prefers-reduced-motion: reduce){.waves path{animation:none}}.content-layer{position:relative;z-index:2;width:100%;height:100%;display:flex;align-items:center}\n"] }]
55944
54182
  }], ctorParameters: () => [], propDecorators: { props: [{
55945
54183
  type: Input
55946
54184
  }] } });
@@ -60388,6 +58626,263 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
60388
58626
  type: Output
60389
58627
  }] } });
60390
58628
 
58629
+ /**
58630
+ * Defaults i18n del componente (es/en). El componente los auto-registra en su
58631
+ * constructor si el consumer no proveyó el namespace `ContentReaction`. Esto
58632
+ * garantiza que NUNCA haya una key faltante: una key faltante se evalúa en el
58633
+ * template por cada change-detection → `console.warn` por CD → con el
58634
+ * `val-debug-console` activo (espeja console.* a un signal) se dispara un loop
58635
+ * infinito que congela el navegador en dev. Auto-registrar lo elimina de raíz.
58636
+ */
58637
+ const CONTENT_REACTION_I18N = {
58638
+ es: {
58639
+ question: '¿Te resultó útil este contenido?',
58640
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
58641
+ submit: 'Enviar opinión',
58642
+ update: 'Actualizar opinión',
58643
+ thankYou: '¡Gracias por tu opinión!',
58644
+ submitted: 'Tu opinión ha sido registrada',
58645
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
58646
+ positiveLabel: 'Sí, mucho',
58647
+ negativeLabel: 'No tanto',
58648
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
58649
+ },
58650
+ en: {
58651
+ question: 'Was this content helpful?',
58652
+ commentPlaceholder: 'Tell us more (optional)...',
58653
+ submit: 'Send feedback',
58654
+ update: 'Update feedback',
58655
+ thankYou: 'Thanks for your feedback!',
58656
+ submitted: 'Your feedback has been recorded',
58657
+ errorSubmitting: 'Failed to send. Please try again.',
58658
+ positiveLabel: 'Yes, a lot',
58659
+ negativeLabel: 'Not really',
58660
+ negativeFeedbackTitle: "Tell us what you didn't like?",
58661
+ },
58662
+ };
58663
+ /**
58664
+ * Componente para reacciones de contenido con emojis.
58665
+ *
58666
+ * @example
58667
+ * ```html
58668
+ * <val-content-reaction
58669
+ * [props]="{
58670
+ * entityRef: { entityType: 'article', entityId: 'art-123' },
58671
+ * question: '¿Te fue útil este artículo?'
58672
+ * }"
58673
+ * (reactionSubmit)="onReactionSubmit($event)"
58674
+ * />
58675
+ * ```
58676
+ */
58677
+ class ContentReactionComponent {
58678
+ constructor() {
58679
+ this.feedbackService = inject(FeedbackService);
58680
+ this.toast = inject(ToastService);
58681
+ this.i18n = inject(I18nService);
58682
+ this.auth = inject(AuthService, { optional: true });
58683
+ this.isConfigured = computed(() => this.feedbackService.isConfigured());
58684
+ this.props = {};
58685
+ this.reactionSubmit = new EventEmitter();
58686
+ this.reactionChange = new EventEmitter();
58687
+ // Estado reactivo
58688
+ this.state = signal({
58689
+ selectedValue: null,
58690
+ comment: '',
58691
+ isLoading: false,
58692
+ isSubmitted: false,
58693
+ hadPreviousReaction: false,
58694
+ error: null,
58695
+ });
58696
+ // Valores por defecto
58697
+ this.defaultEmojis = ['😞', '😐', '😊'];
58698
+ this.defaultLabels = ['No me ayudó', 'Regular', 'Muy útil'];
58699
+ // Computed properties
58700
+ this.resolvedProps = computed(() => ({
58701
+ entityRef: this.props.entityRef,
58702
+ question: this.props.question || this.t('question'),
58703
+ showComment: this.props.showComment ?? true,
58704
+ commentOnValues: this.props.commentOnValues,
58705
+ commentPlaceholder: this.props.commentPlaceholder || this.t('commentPlaceholder'),
58706
+ maxCommentLength: this.props.maxCommentLength ?? 500,
58707
+ emojis: this.props.emojis || this.defaultEmojis,
58708
+ emojiLabels: this.props.emojiLabels || this.defaultLabels,
58709
+ showThankYou: this.props.showThankYou ?? true,
58710
+ thankYouMessage: this.props.thankYouMessage || this.t('thankYou'),
58711
+ disabled: this.props.disabled ?? false,
58712
+ readonly: this.props.readonly ?? false,
58713
+ // Variante: 'emoji' cuando se declara explícitamente o cuando se pasan emojis custom.
58714
+ variant: this.props.variant ?? (this.props.emojis ? 'emoji' : 'buttons'),
58715
+ }));
58716
+ this.activeReactionValues = computed(() => this.resolvedProps().emojis.length === 2 ? ['negative', 'positive'] : ['negative', 'neutral', 'positive']);
58717
+ this.showCommentField = computed(() => {
58718
+ const sel = this.state().selectedValue;
58719
+ if (!sel)
58720
+ return false;
58721
+ const commentOn = this.resolvedProps().commentOnValues;
58722
+ if (commentOn !== undefined)
58723
+ return commentOn.includes(sel);
58724
+ return this.resolvedProps().showComment;
58725
+ });
58726
+ this.canSubmit = computed(() => this.state().selectedValue !== null && !this.state().isLoading);
58727
+ // Auto-registro de defaults i18n (es/en) si el consumer no proveyó el
58728
+ // namespace. Garantiza que `t()` nunca caiga en key faltante. Ver
58729
+ // CONTENT_REACTION_I18N. Idempotente: solo la 1ª instancia registra.
58730
+ if (!this.i18n.hasNamespace('ContentReaction')) {
58731
+ this.i18n.registerContent('ContentReaction', CONTENT_REACTION_I18N);
58732
+ }
58733
+ }
58734
+ ngOnInit() {
58735
+ this.loadPreviousReaction();
58736
+ }
58737
+ ngOnChanges(changes) {
58738
+ if (changes['props'] && !changes['props'].firstChange) {
58739
+ this.loadPreviousReaction();
58740
+ }
58741
+ }
58742
+ async loadPreviousReaction() {
58743
+ if (!this.props.entityRef)
58744
+ return;
58745
+ // Si hay initialValue, usarlo directamente sin consultar
58746
+ if (this.props.initialValue) {
58747
+ this.state.update(s => ({
58748
+ ...s,
58749
+ selectedValue: this.props.initialValue,
58750
+ hadPreviousReaction: true,
58751
+ isLoading: false,
58752
+ isSubmitted: true,
58753
+ }));
58754
+ return;
58755
+ }
58756
+ // Si skipInitialCheck está activo, no consultar
58757
+ if (this.props.skipInitialCheck) {
58758
+ return;
58759
+ }
58760
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
58761
+ try {
58762
+ const check = await this.feedbackService.checkFeedback(this.props.entityRef.entityType, this.props.entityRef.entityId);
58763
+ if (check.hasFeedback && check.reactionValue) {
58764
+ this.state.update(s => ({
58765
+ ...s,
58766
+ selectedValue: check.reactionValue,
58767
+ hadPreviousReaction: true,
58768
+ isLoading: false,
58769
+ isSubmitted: true,
58770
+ }));
58771
+ }
58772
+ else {
58773
+ this.state.update(s => ({ ...s, isLoading: false }));
58774
+ }
58775
+ }
58776
+ catch (error) {
58777
+ console.error('Error loading previous reaction:', error);
58778
+ this.state.update(s => ({ ...s, isLoading: false }));
58779
+ }
58780
+ }
58781
+ selectReaction(value) {
58782
+ if (this.resolvedProps().disabled || this.resolvedProps().readonly)
58783
+ return;
58784
+ const previousValue = this.state().selectedValue;
58785
+ this.state.update(s => ({
58786
+ ...s,
58787
+ selectedValue: value,
58788
+ isSubmitted: false,
58789
+ error: null,
58790
+ }));
58791
+ this.reactionChange.emit({ value, previousValue });
58792
+ }
58793
+ async submitReaction() {
58794
+ const currentState = this.state();
58795
+ const props = this.resolvedProps();
58796
+ if (!currentState.selectedValue || props.disabled)
58797
+ return;
58798
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
58799
+ try {
58800
+ // Determinar si usar endpoint anónimo o autenticado
58801
+ const isAuthenticated = this.auth?.isAuthenticated() ?? false;
58802
+ const useAnonymous = this.props.allowAnonymous && !isAuthenticated;
58803
+ if (useAnonymous) {
58804
+ // Usar endpoint anónimo (público)
58805
+ await this.feedbackService.createAnonymousReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
58806
+ }
58807
+ else {
58808
+ // Usar endpoint autenticado
58809
+ await this.feedbackService.createReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
58810
+ }
58811
+ this.state.update(s => ({
58812
+ ...s,
58813
+ isLoading: false,
58814
+ isSubmitted: true,
58815
+ comment: '',
58816
+ hadPreviousReaction: !useAnonymous, // Solo para autenticados
58817
+ }));
58818
+ this.reactionSubmit.emit({
58819
+ value: currentState.selectedValue,
58820
+ comment: currentState.comment || undefined,
58821
+ entityRef: props.entityRef,
58822
+ isUpdate: currentState.hadPreviousReaction,
58823
+ });
58824
+ if (props.showThankYou) {
58825
+ this.toast.show({
58826
+ message: props.thankYouMessage,
58827
+ duration: 2000,
58828
+ });
58829
+ }
58830
+ }
58831
+ catch (error) {
58832
+ console.error('Error submitting reaction:', error);
58833
+ this.state.update(s => ({
58834
+ ...s,
58835
+ isLoading: false,
58836
+ error: this.t('errorSubmitting'),
58837
+ }));
58838
+ this.toast.show({
58839
+ message: this.t('errorSubmitting'),
58840
+ duration: 3000,
58841
+ });
58842
+ }
58843
+ }
58844
+ updateComment(event) {
58845
+ const value = event.detail.value || '';
58846
+ this.state.update(s => ({ ...s, comment: value }));
58847
+ }
58848
+ getEmoji(index) {
58849
+ return this.resolvedProps().emojis[index];
58850
+ }
58851
+ getEmojiLabel(index) {
58852
+ return this.resolvedProps().emojiLabels[index];
58853
+ }
58854
+ isSelected(value) {
58855
+ return this.state().selectedValue === value;
58856
+ }
58857
+ t(key) {
58858
+ const translations = {
58859
+ question: '¿Te resultó útil este contenido?',
58860
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
58861
+ submit: 'Enviar opinión',
58862
+ update: 'Actualizar opinión',
58863
+ thankYou: '¡Gracias por tu opinión!',
58864
+ submitted: 'Tu opinión ha sido registrada',
58865
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
58866
+ positiveLabel: 'Sí, mucho',
58867
+ negativeLabel: 'No tanto',
58868
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
58869
+ };
58870
+ return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
58871
+ }
58872
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
58873
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ContentReactionComponent, isStandalone: true, selector: "val-content-reaction", inputs: { props: "props" }, outputs: { reactionSubmit: "reactionSubmit", reactionChange: "reactionChange" }, usesOnChanges: true, ngImport: i0, template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">\n {{ state().selectedValue === 'negative' ? t('negativeFeedbackTitle') : resolvedProps().question }}\n </p>\n\n <!-- Botones de reacci\u00F3n \u2014 variante emoji (caras, estrellas, thumbs, \u2026) -->\n @if (resolvedProps().variant === 'emoji') {\n <div class=\"reaction-buttons reaction-buttons--emoji\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <ion-button\n fill=\"clear\"\n shape=\"round\"\n [class.selected]=\"isSelected(value)\"\n [attr.aria-label]=\"resolvedProps().emojiLabels[i]\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji-btn\">{{ resolvedProps().emojis[i] }}</span>\n </ion-button>\n }\n </div>\n } @else {\n <!-- Botones de reacci\u00F3n \u2014 variante texto (default) -->\n <div class=\"reaction-buttons\">\n <ion-button\n [fill]=\"isSelected('positive') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('positive')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('positive')\"\n >\n {{ t('positiveLabel') }}\n </ion-button>\n <ion-button\n [fill]=\"isSelected('negative') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('negative')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('negative')\"\n >\n {{ t('negativeLabel') }}\n </ion-button>\n </div>\n }\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" shape=\"round\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:1.0625rem;font-weight:700;color:var(--ion-color-dark);margin:0 0 16px}.reaction-buttons{display:flex;justify-content:center;gap:12px;margin-bottom:20px}.reaction-buttons--emoji{gap:8px}.reaction-buttons--emoji ion-button{--padding-start: 4px;--padding-end: 4px;transition:transform .15s ease}.reaction-buttons--emoji ion-button.selected{transform:scale(1.25)}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]){opacity:.55}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]):hover{opacity:1}.emoji-btn{font-size:2rem;line-height:1;display:block}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: transparent;--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:.75rem;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:.875rem}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }] }); }
58874
+ }
58875
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
58876
+ type: Component,
58877
+ args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "@if (isConfigured()) {\n<div class=\"content-reaction\" [class.disabled]=\"resolvedProps().disabled\" [class.readonly]=\"resolvedProps().readonly\">\n <!-- Loading inicial -->\n @if (state().isLoading && !state().selectedValue) {\n <div class=\"loading-container\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <!-- Pregunta -->\n <p class=\"question\">\n {{ state().selectedValue === 'negative' ? t('negativeFeedbackTitle') : resolvedProps().question }}\n </p>\n\n <!-- Botones de reacci\u00F3n \u2014 variante emoji (caras, estrellas, thumbs, \u2026) -->\n @if (resolvedProps().variant === 'emoji') {\n <div class=\"reaction-buttons reaction-buttons--emoji\">\n @for (value of activeReactionValues(); track value; let i = $index) {\n <ion-button\n fill=\"clear\"\n shape=\"round\"\n [class.selected]=\"isSelected(value)\"\n [attr.aria-label]=\"resolvedProps().emojiLabels[i]\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji-btn\">{{ resolvedProps().emojis[i] }}</span>\n </ion-button>\n }\n </div>\n } @else {\n <!-- Botones de reacci\u00F3n \u2014 variante texto (default) -->\n <div class=\"reaction-buttons\">\n <ion-button\n [fill]=\"isSelected('positive') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('positive')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('positive')\"\n >\n {{ t('positiveLabel') }}\n </ion-button>\n <ion-button\n [fill]=\"isSelected('negative') ? 'solid' : 'outline'\"\n shape=\"round\"\n color=\"dark\"\n [attr.aria-pressed]=\"isSelected('negative')\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction('negative')\"\n >\n {{ t('negativeLabel') }}\n </ion-button>\n </div>\n }\n\n <!-- Campo de comentario (solo si hay selecci\u00F3n) -->\n @if (showCommentField()) {\n <div class=\"comment-section\">\n <ion-textarea\n [value]=\"state().comment\"\n [placeholder]=\"resolvedProps().commentPlaceholder\"\n [maxlength]=\"resolvedProps().maxCommentLength\"\n [disabled]=\"resolvedProps().disabled\"\n [rows]=\"3\"\n class=\"comment-textarea\"\n (ionInput)=\"updateComment($event)\"\n ></ion-textarea>\n <span class=\"char-count\"> {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }} </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button expand=\"block\" shape=\"round\" [disabled]=\"!canSubmit()\" (click)=\"submitReaction()\" class=\"submit-button\">\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ state().hadPreviousReaction ? t('update') : t('submit') }} }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">{{ t('thankYou') }}</p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n } }\n</div>\n}\n", styles: [":host{display:block}.content-reaction{padding:16px;text-align:center}.content-reaction.disabled{opacity:.6;pointer-events:none}.content-reaction.readonly{pointer-events:none}.question{font-size:1.0625rem;font-weight:700;color:var(--ion-color-dark);margin:0 0 16px}.reaction-buttons{display:flex;justify-content:center;gap:12px;margin-bottom:20px}.reaction-buttons--emoji{gap:8px}.reaction-buttons--emoji ion-button{--padding-start: 4px;--padding-end: 4px;transition:transform .15s ease}.reaction-buttons--emoji ion-button.selected{transform:scale(1.25)}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]){opacity:.55}.reaction-buttons--emoji ion-button:not(.selected):not([disabled]):hover{opacity:1}.emoji-btn{font-size:2rem;line-height:1;display:block}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: transparent;--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:.75rem;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px}.submitted-message{margin-top:16px;color:var(--ion-color-success);font-weight:500;animation:fadeIn .3s ease-out}.error-message{margin-top:8px;color:var(--ion-color-danger);font-size:.875rem}.loading-container{display:flex;justify-content:center;padding:20px}@keyframes slideIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
58878
+ }], ctorParameters: () => [], propDecorators: { props: [{
58879
+ type: Input
58880
+ }], reactionSubmit: [{
58881
+ type: Output
58882
+ }], reactionChange: [{
58883
+ type: Output
58884
+ }] } });
58885
+
60391
58886
  const DEFAULT_SPLASH_SCREEN_CONFIG = {
60392
58887
  fadeOutDuration: 300,
60393
58888
  };
@@ -64240,5 +62735,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
64240
62735
  * Generated bundle index. Do not edit.
64241
62736
  */
64242
62737
 
64243
- 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, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyService, ApiKeysModalComponent, 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, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, 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, PermissionCatalogService, PermissionsViewComponent, 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, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
62738
+ 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, NotificationPreferencesViewComponent, 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, PermissionCatalogService, PermissionsViewComponent, 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, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor$1 as resolveColor, resolveInputDefaultValue, resolveWebBaseUrl, roleGuard, storagePaths, superAdminGuard, toArticle };
64244
62739
  //# sourceMappingURL=valtech-components.mjs.map