valtech-components 2.0.1009 → 2.0.1011

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 (42) hide show
  1. package/esm2022/lib/components/molecules/date-input/date-input.component.mjs +38 -18
  2. package/esm2022/lib/components/organisms/about-view/about-view.component.mjs +436 -0
  3. package/esm2022/lib/components/organisms/about-view/about-view.i18n.mjs +61 -0
  4. package/esm2022/lib/components/organisms/about-view/about.routes.mjs +36 -0
  5. package/esm2022/lib/components/organisms/about-view/types.mjs +19 -0
  6. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.component.mjs +385 -0
  7. package/esm2022/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.mjs +63 -0
  8. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.component.mjs +313 -0
  9. package/esm2022/lib/components/organisms/member-import-modal/member-import-modal.i18n.mjs +63 -0
  10. package/esm2022/lib/components/organisms/organization-view/organization-view.component.mjs +141 -3
  11. package/esm2022/lib/components/organisms/organization-view/organization-view.i18n.mjs +21 -1
  12. package/esm2022/lib/services/apikeys/api-keys.service.mjs +46 -0
  13. package/esm2022/lib/services/apikeys/types.mjs +7 -0
  14. package/esm2022/lib/services/auth/auth.service.mjs +14 -1
  15. package/esm2022/lib/services/auth/types.mjs +1 -1
  16. package/esm2022/lib/services/org/org.service.mjs +8 -1
  17. package/esm2022/lib/services/org/types.mjs +1 -1
  18. package/esm2022/lib/services/page-refresh/page-refresh.service.mjs +15 -4
  19. package/esm2022/lib/version.mjs +2 -2
  20. package/esm2022/public-api.mjs +7 -1
  21. package/fesm2022/valtech-components.mjs +1913 -330
  22. package/fesm2022/valtech-components.mjs.map +1 -1
  23. package/lib/components/molecules/date-input/date-input.component.d.ts +16 -0
  24. package/lib/components/organisms/about-view/about-view.component.d.ts +88 -0
  25. package/lib/components/organisms/about-view/about-view.i18n.d.ts +14 -0
  26. package/lib/components/organisms/about-view/about.routes.d.ts +31 -0
  27. package/lib/components/organisms/about-view/types.d.ts +76 -0
  28. package/lib/components/organisms/api-keys-modal/api-keys-modal.component.d.ts +44 -0
  29. package/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.d.ts +6 -0
  30. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +47 -0
  31. package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +6 -0
  32. package/lib/components/organisms/organization-view/organization-view.component.d.ts +6 -0
  33. package/lib/services/apikeys/api-keys.service.d.ts +25 -0
  34. package/lib/services/apikeys/types.d.ts +46 -0
  35. package/lib/services/auth/auth.service.d.ts +7 -1
  36. package/lib/services/auth/types.d.ts +23 -0
  37. package/lib/services/org/org.service.d.ts +6 -1
  38. package/lib/services/org/types.d.ts +30 -0
  39. package/lib/services/page-refresh/page-refresh.service.d.ts +10 -2
  40. package/lib/version.d.ts +1 -1
  41. package/package.json +1 -1
  42. package/public-api.d.ts +8 -1
@@ -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, 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, shareSocialOutline, helpCircleOutline, 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.1009';
57
+ const VERSION = '2.0.1011';
58
58
 
59
59
  // Control de estado de refresco (singleton a nivel de módulo)
60
60
  let isRefreshing = false;
@@ -8658,6 +8658,19 @@ class AuthService {
8658
8658
  .post(`${this.baseUrl}/reset-password`, request)
8659
8659
  .pipe(catchError(error => this.handleAuthError(error)));
8660
8660
  }
8661
+ /**
8662
+ * Activa una cuenta pre-aprovisionada por una organización (ADR-023): define
8663
+ * la contraseña con el token del enlace de email. En éxito el backend devuelve
8664
+ * tokens de auto-signin → el usuario queda logueado (mismo flujo que verify-email).
8665
+ */
8666
+ activateAccount(request) {
8667
+ this.stateService.clearError();
8668
+ return this.http.post(`${this.baseUrl}/activate`, request).pipe(tap(response => {
8669
+ if (response.activated && response.accessToken) {
8670
+ this.handleSuccessfulAuth(response);
8671
+ }
8672
+ }), catchError(error => this.handleAuthError(error)));
8673
+ }
8661
8674
  /**
8662
8675
  * Cambia la contraseña del usuario autenticado.
8663
8676
  * Requiere la contraseña actual para verificación.
@@ -15848,6 +15861,24 @@ class DateInputComponent {
15848
15861
  get presentation() {
15849
15862
  return this.resolvedProps?.presentation || 'date';
15850
15863
  }
15864
+ /** Locale del datetime — honra el prop del consumer (default es-ES). */
15865
+ get datetimeLocale() {
15866
+ return this.resolvedProps?.locale || 'es-ES';
15867
+ }
15868
+ /** Primer día de la semana — honra el prop del consumer (default lunes). */
15869
+ get firstDayOfWeek() {
15870
+ return this.resolvedProps?.firstDayOfWeek ?? 1;
15871
+ }
15872
+ /**
15873
+ * Opciones de formato para `ion-datetime` (binding, NO string). Honra el prop
15874
+ * del consumer y cae a un default sensato (date medium / time short).
15875
+ */
15876
+ get datetimeFormatOptions() {
15877
+ return (this.resolvedProps?.formatOptions || {
15878
+ date: { dateStyle: 'medium' },
15879
+ time: { timeStyle: 'short' },
15880
+ });
15881
+ }
15851
15882
  /** Texto del trigger cuando NO hay valor (placeholder explícito o default i18n). */
15852
15883
  get placeholderText() {
15853
15884
  if (this.resolvedProps?.placeholder)
@@ -15870,7 +15901,7 @@ class DateInputComponent {
15870
15901
  if (isNaN(d.getTime()))
15871
15902
  return String(raw);
15872
15903
  this.i18n.lang(); // reactividad
15873
- const locale = 'es-ES';
15904
+ const locale = this.datetimeLocale;
15874
15905
  try {
15875
15906
  switch (this.presentation) {
15876
15907
  case 'time':
@@ -15955,27 +15986,28 @@ class DateInputComponent {
15955
15986
  {{ displayValue || placeholderText }}
15956
15987
  </ion-button>
15957
15988
  </div>
15958
- <ion-modal [trigger]="triggerId" [keepContentsMounted]="true">
15989
+ <ion-modal
15990
+ [trigger]="triggerId"
15991
+ [keepContentsMounted]="true"
15992
+ class="val-date-modal"
15993
+ >
15959
15994
  <ng-template>
15960
15995
  <ion-datetime
15961
15996
  [formControl]="resolvedProps.control"
15962
15997
  [id]="resolvedProps.token"
15963
15998
  [presentation]="presentation"
15964
- locale="es-ES"
15965
- [firstDayOfWeek]="1"
15999
+ [locale]="datetimeLocale"
16000
+ [firstDayOfWeek]="firstDayOfWeek"
15966
16001
  [showDefaultButtons]="true"
15967
16002
  [doneText]="doneText"
15968
16003
  [cancelText]="cancelText"
15969
- formatOptions="{
15970
- date: { dateStyle: 'medium' },
15971
- time: { timeStyle: 'short' }
15972
- }"
16004
+ [formatOptions]="datetimeFormatOptions"
15973
16005
  >
15974
16006
  <span slot="title">{{ resolvedProps.hint }}</span>
15975
16007
  </ion-datetime>
15976
16008
  </ng-template>
15977
16009
  </ion-modal>
15978
- `, 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)}.section{margin-top:1rem}.input{margin:var(--val-form-field-gap, .5rem) 0}@media (min-width: 768px){.input{margin:var(--val-form-field-gap, .75rem) 0}}.submit-actions--sticky{position:sticky;bottom:0;z-index:1;background:var(--ion-background-color);border-top:1px solid var(--ion-color-step-150, rgba(0, 0, 0, .08));padding:.75rem 0 calc(.75rem + env(safe-area-inset-bottom))}.field-description{display:block;font-size:.75rem;color:var(--ion-color-dark);margin-bottom:.25rem;line-height:1.4}.button-container{display:flex;flex-direction:column;align-items:flex-start}.action{margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonDatetime, selector: "ion-datetime", inputs: ["cancelText", "clearText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "formatOptions", "highlightedDates", "hourCycle", "hourValues", "isDateEnabled", "locale", "max", "min", "minuteValues", "mode", "monthValues", "multiple", "name", "preferWheel", "presentation", "readonly", "showAdjacentDays", "showClearButton", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "titleSelectedDatesFormatter", "value", "yearValues"] }, { kind: "component", type: IonModal, selector: "ion-modal" }] }); }
16010
+ `, 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)}.section{margin-top:1rem}.input{margin:var(--val-form-field-gap, .5rem) 0}@media (min-width: 768px){.input{margin:var(--val-form-field-gap, .75rem) 0}}.submit-actions--sticky{position:sticky;bottom:0;z-index:1;background:var(--ion-background-color);border-top:1px solid var(--ion-color-step-150, rgba(0, 0, 0, .08));padding:.75rem 0 calc(.75rem + env(safe-area-inset-bottom))}.field-description{display:block;font-size:.75rem;color:var(--ion-color-dark);margin-bottom:.25rem;line-height:1.4}.button-container{display:flex;flex-direction:column;align-items:flex-start}.action{margin-top:.25rem}ion-modal.val-date-modal{--width: fit-content;--min-width: pxToRem(320);--height: fit-content;--border-radius: pxToRem(16);--box-shadow: 0 pxToRem(12) pxToRem(48) rgba(0, 0, 0, .25)}ion-modal.val-date-modal ion-datetime{margin:0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$7.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$7.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonDatetime, selector: "ion-datetime", inputs: ["cancelText", "clearText", "color", "dayValues", "disabled", "doneText", "firstDayOfWeek", "formatOptions", "highlightedDates", "hourCycle", "hourValues", "isDateEnabled", "locale", "max", "min", "minuteValues", "mode", "monthValues", "multiple", "name", "preferWheel", "presentation", "readonly", "showAdjacentDays", "showClearButton", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "titleSelectedDatesFormatter", "value", "yearValues"] }, { kind: "component", type: IonModal, selector: "ion-modal" }] }); }
15979
16011
  }
15980
16012
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DateInputComponent, decorators: [{
15981
16013
  type: Component,
@@ -15994,27 +16026,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
15994
16026
  {{ displayValue || placeholderText }}
15995
16027
  </ion-button>
15996
16028
  </div>
15997
- <ion-modal [trigger]="triggerId" [keepContentsMounted]="true">
16029
+ <ion-modal
16030
+ [trigger]="triggerId"
16031
+ [keepContentsMounted]="true"
16032
+ class="val-date-modal"
16033
+ >
15998
16034
  <ng-template>
15999
16035
  <ion-datetime
16000
16036
  [formControl]="resolvedProps.control"
16001
16037
  [id]="resolvedProps.token"
16002
16038
  [presentation]="presentation"
16003
- locale="es-ES"
16004
- [firstDayOfWeek]="1"
16039
+ [locale]="datetimeLocale"
16040
+ [firstDayOfWeek]="firstDayOfWeek"
16005
16041
  [showDefaultButtons]="true"
16006
16042
  [doneText]="doneText"
16007
16043
  [cancelText]="cancelText"
16008
- formatOptions="{
16009
- date: { dateStyle: 'medium' },
16010
- time: { timeStyle: 'short' }
16011
- }"
16044
+ [formatOptions]="datetimeFormatOptions"
16012
16045
  >
16013
16046
  <span slot="title">{{ resolvedProps.hint }}</span>
16014
16047
  </ion-datetime>
16015
16048
  </ng-template>
16016
16049
  </ion-modal>
16017
- `, 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)}.section{margin-top:1rem}.input{margin:var(--val-form-field-gap, .5rem) 0}@media (min-width: 768px){.input{margin:var(--val-form-field-gap, .75rem) 0}}.submit-actions--sticky{position:sticky;bottom:0;z-index:1;background:var(--ion-background-color);border-top:1px solid var(--ion-color-step-150, rgba(0, 0, 0, .08));padding:.75rem 0 calc(.75rem + env(safe-area-inset-bottom))}.field-description{display:block;font-size:.75rem;color:var(--ion-color-dark);margin-bottom:.25rem;line-height:1.4}.button-container{display:flex;flex-direction:column;align-items:flex-start}.action{margin-top:.25rem}\n"] }]
16050
+ `, 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)}.section{margin-top:1rem}.input{margin:var(--val-form-field-gap, .5rem) 0}@media (min-width: 768px){.input{margin:var(--val-form-field-gap, .75rem) 0}}.submit-actions--sticky{position:sticky;bottom:0;z-index:1;background:var(--ion-background-color);border-top:1px solid var(--ion-color-step-150, rgba(0, 0, 0, .08));padding:.75rem 0 calc(.75rem + env(safe-area-inset-bottom))}.field-description{display:block;font-size:.75rem;color:var(--ion-color-dark);margin-bottom:.25rem;line-height:1.4}.button-container{display:flex;flex-direction:column;align-items:flex-start}.action{margin-top:.25rem}ion-modal.val-date-modal{--width: fit-content;--min-width: pxToRem(320);--height: fit-content;--border-radius: pxToRem(16);--box-shadow: 0 pxToRem(12) pxToRem(48) rgba(0, 0, 0, .25)}ion-modal.val-date-modal ion-datetime{margin:0 auto}\n"] }]
16018
16051
  }], ctorParameters: () => [], propDecorators: { preset: [{
16019
16052
  type: Input
16020
16053
  }], props: [{
@@ -41609,13 +41642,24 @@ class PageRefreshService {
41609
41642
  this.hasHandler.set(false);
41610
41643
  }
41611
41644
  /**
41612
- * Ejecuta el handler registrado y espera a que termine. Lo invoca
41613
- * `val-page-wrapper` al detectar el gesto de pull. No-op si no hay handler.
41645
+ * Duración mínima (ms) que el spinner permanece visible, aunque el handler
41646
+ * termine antes. Sin esto, un reload rápido (data cacheada) cierra el spinner
41647
+ * al instante → se percibe como "no hubo refresh". Con el mínimo, el gesto se
41648
+ * siente smooth y CONSISTENTE en todas las vistas que usan el page-wrapper.
41649
+ */
41650
+ static { this.MIN_SPINNER_MS = 500; }
41651
+ /**
41652
+ * Ejecuta el handler registrado y espera a que termine, garantizando una
41653
+ * duración mínima de spinner. Lo invoca `val-page-wrapper` al detectar el
41654
+ * gesto de pull. No-op si no hay handler.
41614
41655
  */
41615
41656
  async run() {
41616
41657
  if (!this.handler)
41617
41658
  return;
41618
- await this.handler();
41659
+ const minDelay = new Promise(resolve => setTimeout(resolve, PageRefreshService.MIN_SPINNER_MS));
41660
+ // Espera AMBOS: el trabajo real + el mínimo visual. Si el handler tarda más,
41661
+ // manda el handler; si es instantáneo, el spinner igual se ve ~500ms.
41662
+ await Promise.all([Promise.resolve(this.handler()), minDelay]);
41619
41663
  }
41620
41664
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
41621
41665
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PageRefreshService, providedIn: 'root' }); }
@@ -41737,7 +41781,7 @@ const PROFILE_VIEW_I18N = {
41737
41781
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41738
41782
  const HANDLE_MIN = 3;
41739
41783
  const HANDLE_MAX = 20;
41740
- const DEFAULT_NAMESPACE$f = 'Settings.Profile';
41784
+ const DEFAULT_NAMESPACE$i = 'Settings.Profile';
41741
41785
  /**
41742
41786
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41743
41787
  *
@@ -41787,7 +41831,7 @@ class ProfileViewComponent {
41787
41831
  showPhone: merged.showPhone ?? true,
41788
41832
  showShareCta: merged.showShareCta ?? true,
41789
41833
  profileBaseUrl: merged.profileBaseUrl ?? '',
41790
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
41834
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$i,
41791
41835
  onSaved: merged.onSaved,
41792
41836
  onAvatarUploaded: merged.onAvatarUploaded,
41793
41837
  };
@@ -42718,7 +42762,7 @@ const PREFERENCES_VIEW_I18N = {
42718
42762
  },
42719
42763
  };
42720
42764
 
42721
- const DEFAULT_NAMESPACE$e = 'Settings.General';
42765
+ const DEFAULT_NAMESPACE$h = 'Settings.General';
42722
42766
  const DEFAULT_LANGUAGES = ['es', 'en'];
42723
42767
  /**
42724
42768
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42755,7 +42799,7 @@ class PreferencesViewComponent {
42755
42799
  showLanguage: merged.showLanguage ?? true,
42756
42800
  showFontSize: merged.showFontSize ?? true,
42757
42801
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42758
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
42802
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$h,
42759
42803
  onThemeChanged: merged.onThemeChanged,
42760
42804
  onLanguageChanged: merged.onLanguageChanged,
42761
42805
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -44511,7 +44555,7 @@ const SECURITY_VIEW_I18N = {
44511
44555
  };
44512
44556
 
44513
44557
  addIcons({ laptopOutline, phonePortraitOutline });
44514
- const DEFAULT_NAMESPACE$d = 'Settings.Security';
44558
+ const DEFAULT_NAMESPACE$g = 'Settings.Security';
44515
44559
  /**
44516
44560
  * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44517
44561
  *
@@ -44560,7 +44604,7 @@ class SecurityViewComponent {
44560
44604
  showChangeEmail: merged.showChangeEmail ?? true,
44561
44605
  showMfa: merged.showMfa ?? true,
44562
44606
  showPolicies: merged.showPolicies ?? true,
44563
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
44607
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$g,
44564
44608
  handleDeepLinks: merged.handleDeepLinks ?? true,
44565
44609
  homeRoute: merged.homeRoute ?? '/app/home',
44566
44610
  onMfaCompleted: merged.onMfaCompleted,
@@ -45362,6 +45406,13 @@ class OrgService {
45362
45406
  inviteUser(orgId, req) {
45363
45407
  return this.http.post(`${this.baseUrl}/${orgId}/invite`, req);
45364
45408
  }
45409
+ /**
45410
+ * Crea/asigna miembros en masa (ADR-023). Resultado por-fila (partial success):
45411
+ * `created` (pre-aprovisionado), `assigned` (ya existía), `skipped`, `error`.
45412
+ */
45413
+ importMembers(orgId, req) {
45414
+ return this.http.post(`${this.baseUrl}/${orgId}/members/import`, req);
45415
+ }
45365
45416
  leaveOrg(orgId) {
45366
45417
  return this.http.post(`${this.baseUrl}/${orgId}/leave`, {});
45367
45418
  }
@@ -45466,7 +45517,7 @@ const CREATE_ORG_MODAL_I18N = {
45466
45517
  },
45467
45518
  };
45468
45519
 
45469
- const DEFAULT_NAMESPACE$c = 'CreateOrgModal';
45520
+ const DEFAULT_NAMESPACE$f = 'CreateOrgModal';
45470
45521
  /**
45471
45522
  * `val-create-org-modal` — modal de creación de organización (organism
45472
45523
  * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
@@ -45492,10 +45543,10 @@ class CreateOrgModalComponent {
45492
45543
  this.toast = inject(ToastService);
45493
45544
  this.errors = inject(ValtechErrorService);
45494
45545
  /** Namespace i18n con que la vista resuelve sus textos. */
45495
- this.i18nNamespace = DEFAULT_NAMESPACE$c;
45546
+ this.i18nNamespace = DEFAULT_NAMESPACE$f;
45496
45547
  this._busy = signal(false);
45497
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
45498
- this.i18n.registerContent(DEFAULT_NAMESPACE$c, CREATE_ORG_MODAL_I18N);
45548
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$f)) {
45549
+ this.i18n.registerContent(DEFAULT_NAMESPACE$f, CREATE_ORG_MODAL_I18N);
45499
45550
  }
45500
45551
  this.formMeta = signal(this.buildFormMeta());
45501
45552
  effect(() => {
@@ -45731,7 +45782,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
45731
45782
  },
45732
45783
  };
45733
45784
 
45734
- const DEFAULT_NAMESPACE$b = 'Settings.DeleteAccount';
45785
+ const DEFAULT_NAMESPACE$e = 'Settings.DeleteAccount';
45735
45786
  /**
45736
45787
  * `val-delete-account-modal` — modal de eliminación de cuenta (organism,
45737
45788
  * cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -45757,7 +45808,7 @@ class DeleteAccountModalComponent {
45757
45808
  this.i18n = inject(I18nService);
45758
45809
  this.toast = inject(ToastService);
45759
45810
  /** Namespace i18n con que la vista resuelve sus textos. */
45760
- this.i18nNamespace = DEFAULT_NAMESPACE$b;
45811
+ this.i18nNamespace = DEFAULT_NAMESPACE$e;
45761
45812
  this.hasPassword = signal(null);
45762
45813
  this.codeSent = signal(false);
45763
45814
  this.formState = signal(ComponentStates.ENABLED);
@@ -45824,8 +45875,8 @@ class DeleteAccountModalComponent {
45824
45875
  ],
45825
45876
  actions: this.submitBtn('delete-code-submit', 'confirmCta'),
45826
45877
  }));
45827
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45828
- this.i18n.registerContent(DEFAULT_NAMESPACE$b, DELETE_ACCOUNT_MODAL_I18N);
45878
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$e)) {
45879
+ this.i18n.registerContent(DEFAULT_NAMESPACE$e, DELETE_ACCOUNT_MODAL_I18N);
45829
45880
  }
45830
45881
  }
45831
45882
  ngOnInit() {
@@ -46026,7 +46077,7 @@ const ORG_INFO_SHEET_I18N = {
46026
46077
  },
46027
46078
  };
46028
46079
 
46029
- const DEFAULT_NAMESPACE$a = 'OrgInfoSheet';
46080
+ const DEFAULT_NAMESPACE$d = 'OrgInfoSheet';
46030
46081
  /**
46031
46082
  * `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
46032
46083
  * Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
@@ -46071,15 +46122,15 @@ class OrgInfoSheetComponent {
46071
46122
  text: this.closeProps.text || this.resolvedCloseLabel(),
46072
46123
  }));
46073
46124
  addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
46074
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
46075
- this.i18n.registerContent(DEFAULT_NAMESPACE$a, ORG_INFO_SHEET_I18N);
46125
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$d)) {
46126
+ this.i18n.registerContent(DEFAULT_NAMESPACE$d, ORG_INFO_SHEET_I18N);
46076
46127
  }
46077
46128
  }
46078
46129
  dismiss() {
46079
46130
  this._modalRef?.dismiss(null, 'cancel');
46080
46131
  }
46081
46132
  tt(key) {
46082
- return this.i18n.t(key, DEFAULT_NAMESPACE$a);
46133
+ return this.i18n.t(key, DEFAULT_NAMESPACE$d);
46083
46134
  }
46084
46135
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46085
46136
  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: `
@@ -46246,7 +46297,7 @@ const SWITCH_ORG_MODAL_I18N = {
46246
46297
  },
46247
46298
  };
46248
46299
 
46249
- const DEFAULT_NAMESPACE$9 = 'Settings.SwitchOrg';
46300
+ const DEFAULT_NAMESPACE$c = 'Settings.SwitchOrg';
46250
46301
  addIcons({ businessOutline, checkmarkCircleOutline });
46251
46302
  /**
46252
46303
  * `val-switch-org-modal` — modal de cambio de organización activa (organism
@@ -46272,7 +46323,7 @@ class SwitchOrgModalComponent {
46272
46323
  this.orgSwitch = inject(OrgSwitchService);
46273
46324
  this.errors = inject(ValtechErrorService);
46274
46325
  /** Namespace i18n con que la vista resuelve sus textos. */
46275
- this.i18nNamespace = DEFAULT_NAMESPACE$9;
46326
+ this.i18nNamespace = DEFAULT_NAMESPACE$c;
46276
46327
  this.orgs = signal([]);
46277
46328
  this.loading = signal(false);
46278
46329
  this.query = signal('');
@@ -46288,8 +46339,8 @@ class SwitchOrgModalComponent {
46288
46339
  return list;
46289
46340
  return list.filter(o => o.name.toLowerCase().includes(q));
46290
46341
  });
46291
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46292
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, SWITCH_ORG_MODAL_I18N);
46342
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
46343
+ this.i18n.registerContent(DEFAULT_NAMESPACE$c, SWITCH_ORG_MODAL_I18N);
46293
46344
  }
46294
46345
  this.loadOrgs();
46295
46346
  }
@@ -46607,7 +46658,7 @@ const ACCOUNT_VIEW_I18N = {
46607
46658
  };
46608
46659
 
46609
46660
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
46610
- const DEFAULT_NAMESPACE$8 = 'Settings.Account';
46661
+ const DEFAULT_NAMESPACE$b = 'Settings.Account';
46611
46662
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
46612
46663
  /**
46613
46664
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -46648,7 +46699,7 @@ class AccountViewComponent {
46648
46699
  showNewOrgCta: merged.showNewOrgCta ?? true,
46649
46700
  showLogout: merged.showLogout ?? true,
46650
46701
  showDeleteAccount: merged.showDeleteAccount ?? true,
46651
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$8,
46702
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$b,
46652
46703
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
46653
46704
  onOrgSwitched: merged.onOrgSwitched,
46654
46705
  onOrgCreated: merged.onOrgCreated,
@@ -47373,7 +47424,7 @@ const EDIT_ORG_MODAL_I18N = {
47373
47424
  },
47374
47425
  };
47375
47426
 
47376
- const DEFAULT_NAMESPACE$7 = 'EditOrgModal';
47427
+ const DEFAULT_NAMESPACE$a = 'EditOrgModal';
47377
47428
  /**
47378
47429
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
47379
47430
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -47400,10 +47451,10 @@ class EditOrgModalComponent {
47400
47451
  /** Organización a editar (pasada por `componentProps`). */
47401
47452
  this.org = null;
47402
47453
  /** Namespace i18n con que la vista resuelve sus textos. */
47403
- this.i18nNamespace = DEFAULT_NAMESPACE$7;
47454
+ this.i18nNamespace = DEFAULT_NAMESPACE$a;
47404
47455
  this.saving = signal(false);
47405
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
47406
- this.i18n.registerContent(DEFAULT_NAMESPACE$7, EDIT_ORG_MODAL_I18N);
47456
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
47457
+ this.i18n.registerContent(DEFAULT_NAMESPACE$a, EDIT_ORG_MODAL_I18N);
47407
47458
  }
47408
47459
  this.formMeta = signal(this.buildFormMeta());
47409
47460
  effect(() => {
@@ -47641,7 +47692,7 @@ const INVITE_MEMBER_MODAL_I18N = {
47641
47692
  },
47642
47693
  };
47643
47694
 
47644
- const DEFAULT_NAMESPACE$6 = 'InviteModal';
47695
+ const DEFAULT_NAMESPACE$9 = 'InviteModal';
47645
47696
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
47646
47697
  /**
47647
47698
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -47672,7 +47723,7 @@ class InviteMemberModalComponent {
47672
47723
  /** Ids de miembros ya existentes (excluidos de los resultados). */
47673
47724
  this.existingMemberIds = [];
47674
47725
  /** Namespace i18n con que la vista resuelve sus textos. */
47675
- this.i18nNamespace = DEFAULT_NAMESPACE$6;
47726
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
47676
47727
  this.query = signal('');
47677
47728
  this.searching = signal(false);
47678
47729
  this.searchResults = signal([]);
@@ -47719,8 +47770,8 @@ class InviteMemberModalComponent {
47719
47770
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
47720
47771
  }));
47721
47772
  this.searchSubject = new Subject();
47722
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47723
- this.i18n.registerContent(DEFAULT_NAMESPACE$6, INVITE_MEMBER_MODAL_I18N);
47773
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
47774
+ this.i18n.registerContent(DEFAULT_NAMESPACE$9, INVITE_MEMBER_MODAL_I18N);
47724
47775
  }
47725
47776
  this.loadRoles();
47726
47777
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
@@ -48209,7 +48260,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
48209
48260
  },
48210
48261
  };
48211
48262
 
48212
- const DEFAULT_NAMESPACE$5 = 'Settings.MemberDetail';
48263
+ const DEFAULT_NAMESPACE$8 = 'Settings.MemberDetail';
48213
48264
  /**
48214
48265
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
48215
48266
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -48246,7 +48297,7 @@ class MemberDetailModalComponent {
48246
48297
  { id: 'admin', name: 'admin' },
48247
48298
  ];
48248
48299
  /** Namespace i18n con que la vista resuelve sus textos. */
48249
- this.i18nNamespace = DEFAULT_NAMESPACE$5;
48300
+ this.i18nNamespace = DEFAULT_NAMESPACE$8;
48250
48301
  this.working = signal(false);
48251
48302
  this.currentRole = signal('');
48252
48303
  this.roleControl = new FormControl('');
@@ -48286,8 +48337,8 @@ class MemberDetailModalComponent {
48286
48337
  type: 'button',
48287
48338
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
48288
48339
  }));
48289
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48290
- this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_DETAIL_MODAL_I18N);
48340
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
48341
+ this.i18n.registerContent(DEFAULT_NAMESPACE$8, MEMBER_DETAIL_MODAL_I18N);
48291
48342
  }
48292
48343
  }
48293
48344
  ngOnInit() {
@@ -48624,7 +48675,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
48624
48675
  },
48625
48676
  };
48626
48677
 
48627
- const DEFAULT_NAMESPACE$4 = 'Settings.TransferModal';
48678
+ const DEFAULT_NAMESPACE$7 = 'Settings.TransferModal';
48628
48679
  /**
48629
48680
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
48630
48681
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -48648,7 +48699,7 @@ class TransferOwnershipModalComponent {
48648
48699
  /** Lista de miembros transferibles (todos excepto el owner actual). */
48649
48700
  this.members = [];
48650
48701
  /** Namespace i18n con que la vista resuelve sus textos. */
48651
- this.i18nNamespace = DEFAULT_NAMESPACE$4;
48702
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
48652
48703
  this.query = signal('');
48653
48704
  this.selectedId = signal('');
48654
48705
  this.filtered = computed(() => {
@@ -48660,8 +48711,8 @@ class TransferOwnershipModalComponent {
48660
48711
  m.userId.toLowerCase().includes(q));
48661
48712
  });
48662
48713
  this.noResultsLabel = computed(() => `${this.t('noResults')} "${this.query()}"`);
48663
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$4)) {
48664
- this.i18n.registerContent(DEFAULT_NAMESPACE$4, TRANSFER_OWNERSHIP_MODAL_I18N);
48714
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
48715
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, TRANSFER_OWNERSHIP_MODAL_I18N);
48665
48716
  }
48666
48717
  }
48667
48718
  ngOnInit() {
@@ -48846,6 +48897,849 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48846
48897
  type: Input
48847
48898
  }] } });
48848
48899
 
48900
+ /**
48901
+ * Defaults i18n (es/en) embebidos en `val-member-import-modal` (ADR-023).
48902
+ * Auto-registrados si el consumer no proveyó el namespace `Settings.ImportModal`.
48903
+ */
48904
+ const MEMBER_IMPORT_MODAL_I18N = {
48905
+ es: {
48906
+ title: 'Importar miembros',
48907
+ hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
48908
+ csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48909
+ rolesAvailable: 'Roles disponibles:',
48910
+ onConflictLabel: 'Si el email ya existe',
48911
+ conflictAssign: 'Asignar el rol',
48912
+ conflictSkip: 'Omitir',
48913
+ conflictError: 'Marcar error',
48914
+ sendActivation: 'Enviar email de activación a las cuentas nuevas',
48915
+ submit: 'Importar',
48916
+ submitting: 'Importando...',
48917
+ parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
48918
+ noRows: 'Pega al menos una fila válida (email y rol).',
48919
+ summary: 'Resumen',
48920
+ created: 'creados',
48921
+ assigned: 'asignados',
48922
+ skipped: 'omitidos',
48923
+ failed: 'con error',
48924
+ colEmail: 'Email',
48925
+ colStatus: 'Estado',
48926
+ colDetail: 'Detalle',
48927
+ activationSent: 'activación enviada',
48928
+ activationPending: 'activación pendiente',
48929
+ again: 'Importar otro lote',
48930
+ close: 'Cerrar',
48931
+ genericError: 'No se pudo completar la importación. Intenta de nuevo.',
48932
+ },
48933
+ en: {
48934
+ title: 'Import members',
48935
+ hint: 'Paste a CSV with columns: email, name, role (one row per person).',
48936
+ csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48937
+ rolesAvailable: 'Available roles:',
48938
+ onConflictLabel: 'If the email already exists',
48939
+ conflictAssign: 'Assign the role',
48940
+ conflictSkip: 'Skip',
48941
+ conflictError: 'Mark as error',
48942
+ sendActivation: 'Send activation email to new accounts',
48943
+ submit: 'Import',
48944
+ submitting: 'Importing...',
48945
+ parseError: "Couldn't parse the CSV. Check the format.",
48946
+ noRows: 'Paste at least one valid row (email and role).',
48947
+ summary: 'Summary',
48948
+ created: 'created',
48949
+ assigned: 'assigned',
48950
+ skipped: 'skipped',
48951
+ failed: 'failed',
48952
+ colEmail: 'Email',
48953
+ colStatus: 'Status',
48954
+ colDetail: 'Detail',
48955
+ activationSent: 'activation sent',
48956
+ activationPending: 'activation pending',
48957
+ again: 'Import another batch',
48958
+ close: 'Close',
48959
+ genericError: "Couldn't complete the import. Try again.",
48960
+ },
48961
+ };
48962
+
48963
+ const DEFAULT_NAMESPACE$6 = 'Settings.ImportModal';
48964
+ /**
48965
+ * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
48966
+ *
48967
+ * El admin pega un CSV (`email,nombre,rol`); el modal lo parsea a filas y llama
48968
+ * `OrgService.importMembers`. Muestra el resultado por-fila (partial success):
48969
+ * `created` / `assigned` / `skipped` / `error`. Header canónico (Regla #5).
48970
+ *
48971
+ * Auto-registra defaults i18n (es/en) en `Settings.ImportModal`.
48972
+ */
48973
+ class MemberImportModalComponent {
48974
+ constructor() {
48975
+ this.i18n = inject(I18nService);
48976
+ this.orgService = inject(OrgService);
48977
+ /** Org destino del import. */
48978
+ this.orgId = '';
48979
+ /** Nombres de roles disponibles (para el hint). Opcional. */
48980
+ this.availableRoles = [];
48981
+ /** Namespace i18n. */
48982
+ this.i18nNamespace = DEFAULT_NAMESPACE$6;
48983
+ this.csv = '';
48984
+ this.onConflict = 'assignRole';
48985
+ this.sendActivation = true;
48986
+ this.submitting = signal(false);
48987
+ this.errorMsg = signal('');
48988
+ this.results = signal(null);
48989
+ this.rolesHint = computed(() => this.availableRoles.length ? `${this.t('rolesAvailable')} ${this.availableRoles.join(', ')}` : '');
48990
+ this.summaryLabel = computed(() => {
48991
+ const s = this.results()?.summary;
48992
+ if (!s)
48993
+ return this.t('summary');
48994
+ return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
48995
+ });
48996
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
48997
+ this.i18n.registerContent(DEFAULT_NAMESPACE$6, MEMBER_IMPORT_MODAL_I18N);
48998
+ }
48999
+ }
49000
+ ngOnInit() {
49001
+ if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49002
+ this.i18n.registerContent(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
49003
+ }
49004
+ }
49005
+ /** Parsea el CSV pegado a filas {email,name,roleName}. Tolera header. */
49006
+ parseCsv() {
49007
+ const rows = [];
49008
+ for (const raw of this.csv.split(/\r?\n/)) {
49009
+ const line = raw.trim();
49010
+ if (!line)
49011
+ continue;
49012
+ const cols = line.split(',').map(c => c.trim());
49013
+ const email = (cols[0] ?? '').toLowerCase();
49014
+ // Saltar header (primera col "email") o líneas sin @.
49015
+ if (!email || !email.includes('@'))
49016
+ continue;
49017
+ rows.push({ email, name: cols[1] ?? '', roleName: cols[2] ?? '' });
49018
+ }
49019
+ return rows;
49020
+ }
49021
+ submit() {
49022
+ this.errorMsg.set('');
49023
+ const users = this.parseCsv();
49024
+ if (users.length === 0) {
49025
+ this.errorMsg.set(this.t('noRows'));
49026
+ return;
49027
+ }
49028
+ if (users.some(u => !u.roleName)) {
49029
+ this.errorMsg.set(this.t('parseError'));
49030
+ return;
49031
+ }
49032
+ this.submitting.set(true);
49033
+ this.orgService
49034
+ .importMembers(this.orgId, {
49035
+ users,
49036
+ onConflict: this.onConflict,
49037
+ sendActivation: this.sendActivation,
49038
+ })
49039
+ .subscribe({
49040
+ next: res => {
49041
+ this.submitting.set(false);
49042
+ this.results.set(res);
49043
+ // Refrescar lista del consumer si hubo altas/asignaciones efectivas.
49044
+ if (res.summary.created + res.summary.assigned > 0) {
49045
+ this.onSuccess?.();
49046
+ }
49047
+ },
49048
+ error: err => {
49049
+ this.submitting.set(false);
49050
+ this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49051
+ },
49052
+ });
49053
+ }
49054
+ statusLabel(r) {
49055
+ const base = this.t(r.status === 'error' ? 'failed' : r.status);
49056
+ if (r.status === 'created' && r.activation) {
49057
+ return `${base} · ${this.t(r.activation === 'sent' ? 'activationSent' : 'activationPending')}`;
49058
+ }
49059
+ return base;
49060
+ }
49061
+ reset() {
49062
+ this.csv = '';
49063
+ this.errorMsg.set('');
49064
+ this.results.set(null);
49065
+ }
49066
+ dismiss() {
49067
+ this._modalRef?.dismiss(null, 'cancel');
49068
+ }
49069
+ t(key) {
49070
+ return this.i18n.t(key, this.i18nNamespace);
49071
+ }
49072
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49073
+ 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: `
49074
+ <ion-header>
49075
+ <ion-toolbar>
49076
+ <ion-buttons slot="end">
49077
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49078
+ <strong>{{ t('close') }}</strong>
49079
+ </ion-button>
49080
+ </ion-buttons>
49081
+ </ion-toolbar>
49082
+ </ion-header>
49083
+
49084
+ <ion-content class="ion-padding">
49085
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49086
+
49087
+ @if (!results()) {
49088
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49089
+
49090
+ @if (rolesHint()) {
49091
+ <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49092
+ }
49093
+
49094
+ <ion-textarea
49095
+ class="import-csv"
49096
+ [autoGrow]="true"
49097
+ [rows]="6"
49098
+ [placeholder]="t('csvPlaceholder')"
49099
+ [(ngModel)]="csv"
49100
+ />
49101
+
49102
+ <ion-select
49103
+ class="import-conflict"
49104
+ label="{{ t('onConflictLabel') }}"
49105
+ labelPlacement="stacked"
49106
+ interface="popover"
49107
+ [(ngModel)]="onConflict"
49108
+ >
49109
+ <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49110
+ <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49111
+ <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49112
+ </ion-select>
49113
+
49114
+ <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49115
+ {{ t('sendActivation') }}
49116
+ </ion-checkbox>
49117
+
49118
+ @if (errorMsg()) {
49119
+ <p class="import-error">{{ errorMsg() }}</p>
49120
+ }
49121
+
49122
+ <div class="import-actions">
49123
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49124
+ @if (submitting()) {
49125
+ <ion-spinner name="dots" />
49126
+ &nbsp;{{ t('submitting') }}
49127
+ } @else {
49128
+ {{ t('submit') }}
49129
+ }
49130
+ </ion-button>
49131
+ </div>
49132
+ } @else {
49133
+ <!-- Resultados -->
49134
+ <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49135
+ <div class="import-results">
49136
+ @for (r of results()!.results; track r.email) {
49137
+ <div class="import-row import-row--{{ r.status }}">
49138
+ <span class="import-row__email">{{ r.email }}</span>
49139
+ <span class="import-row__status">{{ statusLabel(r) }}</span>
49140
+ @if (r.reason) {
49141
+ <span class="import-row__reason">{{ r.reason }}</span>
49142
+ }
49143
+ </div>
49144
+ }
49145
+ </div>
49146
+ <div class="import-actions">
49147
+ <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49148
+ {{ t('again') }}
49149
+ </ion-button>
49150
+ </div>
49151
+ }
49152
+ </ion-content>
49153
+ `, 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"] }] }); }
49154
+ }
49155
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
49156
+ type: Component,
49157
+ args: [{ selector: 'val-member-import-modal', standalone: true, imports: [
49158
+ FormsModule,
49159
+ IonHeader,
49160
+ IonToolbar,
49161
+ IonButtons,
49162
+ IonButton,
49163
+ IonContent,
49164
+ IonTextarea,
49165
+ IonSelect,
49166
+ IonSelectOption,
49167
+ IonCheckbox,
49168
+ IonSpinner,
49169
+ DisplayComponent,
49170
+ TextComponent,
49171
+ TitleComponent,
49172
+ ], template: `
49173
+ <ion-header>
49174
+ <ion-toolbar>
49175
+ <ion-buttons slot="end">
49176
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49177
+ <strong>{{ t('close') }}</strong>
49178
+ </ion-button>
49179
+ </ion-buttons>
49180
+ </ion-toolbar>
49181
+ </ion-header>
49182
+
49183
+ <ion-content class="ion-padding">
49184
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49185
+
49186
+ @if (!results()) {
49187
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49188
+
49189
+ @if (rolesHint()) {
49190
+ <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49191
+ }
49192
+
49193
+ <ion-textarea
49194
+ class="import-csv"
49195
+ [autoGrow]="true"
49196
+ [rows]="6"
49197
+ [placeholder]="t('csvPlaceholder')"
49198
+ [(ngModel)]="csv"
49199
+ />
49200
+
49201
+ <ion-select
49202
+ class="import-conflict"
49203
+ label="{{ t('onConflictLabel') }}"
49204
+ labelPlacement="stacked"
49205
+ interface="popover"
49206
+ [(ngModel)]="onConflict"
49207
+ >
49208
+ <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49209
+ <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49210
+ <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49211
+ </ion-select>
49212
+
49213
+ <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49214
+ {{ t('sendActivation') }}
49215
+ </ion-checkbox>
49216
+
49217
+ @if (errorMsg()) {
49218
+ <p class="import-error">{{ errorMsg() }}</p>
49219
+ }
49220
+
49221
+ <div class="import-actions">
49222
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49223
+ @if (submitting()) {
49224
+ <ion-spinner name="dots" />
49225
+ &nbsp;{{ t('submitting') }}
49226
+ } @else {
49227
+ {{ t('submit') }}
49228
+ }
49229
+ </ion-button>
49230
+ </div>
49231
+ } @else {
49232
+ <!-- Resultados -->
49233
+ <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49234
+ <div class="import-results">
49235
+ @for (r of results()!.results; track r.email) {
49236
+ <div class="import-row import-row--{{ r.status }}">
49237
+ <span class="import-row__email">{{ r.email }}</span>
49238
+ <span class="import-row__status">{{ statusLabel(r) }}</span>
49239
+ @if (r.reason) {
49240
+ <span class="import-row__reason">{{ r.reason }}</span>
49241
+ }
49242
+ </div>
49243
+ }
49244
+ </div>
49245
+ <div class="import-actions">
49246
+ <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49247
+ {{ t('again') }}
49248
+ </ion-button>
49249
+ </div>
49250
+ }
49251
+ </ion-content>
49252
+ `, 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"] }]
49253
+ }], ctorParameters: () => [], propDecorators: { orgId: [{
49254
+ type: Input
49255
+ }], availableRoles: [{
49256
+ type: Input
49257
+ }], onSuccess: [{
49258
+ type: Input
49259
+ }], _modalRef: [{
49260
+ type: Input
49261
+ }], i18nNamespace: [{
49262
+ type: Input
49263
+ }] } });
49264
+
49265
+ /**
49266
+ * Cliente HTTP de Client API Keys org-scopeadas (ADR-023). La org activa se
49267
+ * resuelve en backend desde el JWT, así que el path no la incluye.
49268
+ */
49269
+ class ApiKeyService {
49270
+ constructor(config, http) {
49271
+ this.config = config;
49272
+ this.http = http;
49273
+ }
49274
+ get baseUrl() {
49275
+ return `${this.config.apiUrl}/org/api-keys`;
49276
+ }
49277
+ /** Lista las API keys de la org activa. */
49278
+ list() {
49279
+ return this.http.get(`${this.baseUrl}/`).pipe(map$1(r => r.keys ?? []));
49280
+ }
49281
+ /** Crea una API key. La respuesta incluye el secreto (se muestra UNA vez). */
49282
+ create(req) {
49283
+ return this.http.post(`${this.baseUrl}/`, req);
49284
+ }
49285
+ /** Revoca (elimina) una API key por id. */
49286
+ revoke(keyId) {
49287
+ return this.http.delete(`${this.baseUrl}/${keyId}`);
49288
+ }
49289
+ /** Catálogo de permisos asignables a una key. */
49290
+ availablePermissions() {
49291
+ return this.http
49292
+ .get(`${this.baseUrl}/permissions`)
49293
+ .pipe(map$1(r => r.permissions ?? []));
49294
+ }
49295
+ 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 }); }
49296
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, providedIn: 'root' }); }
49297
+ }
49298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, decorators: [{
49299
+ type: Injectable,
49300
+ args: [{ providedIn: 'root' }]
49301
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
49302
+ type: Inject,
49303
+ args: [VALTECH_AUTH_CONFIG]
49304
+ }] }, { type: i1$3.HttpClient }] });
49305
+
49306
+ /**
49307
+ * Defaults i18n (es/en) embebidos en `val-api-keys-modal` (ADR-023).
49308
+ * Auto-registrados si el consumer no proveyó el namespace `Settings.ApiKeysModal`.
49309
+ */
49310
+ const API_KEYS_MODAL_I18N = {
49311
+ es: {
49312
+ title: 'Client API Keys',
49313
+ hint: 'Llaves M2M org-scopeadas para integraciones (p. ej. importar miembros).',
49314
+ empty: 'Aún no hay API keys en esta organización.',
49315
+ newKey: 'Nueva API key',
49316
+ nameLabel: 'Nombre',
49317
+ namePlaceholder: 'Ej: Integración RRHH',
49318
+ permissionsLabel: 'Permisos',
49319
+ expiresLabel: 'Expira en (días, opcional)',
49320
+ create: 'Crear key',
49321
+ creating: 'Creando...',
49322
+ cancel: 'Cancelar',
49323
+ revoke: 'Revocar',
49324
+ revokeConfirm: '¿Revocar esta API key? Las integraciones que la usen dejarán de funcionar.',
49325
+ secretTitle: 'Copia tu API key ahora',
49326
+ secretWarning: 'Este secreto se muestra una sola vez. Guárdalo en un lugar seguro.',
49327
+ copy: 'Copiar',
49328
+ copied: 'Copiada',
49329
+ done: 'Listo',
49330
+ statusActive: 'activa',
49331
+ lastUsed: 'Último uso',
49332
+ never: 'nunca',
49333
+ permsCount: 'permisos',
49334
+ noName: 'El nombre es obligatorio.',
49335
+ noPerms: 'Selecciona al menos un permiso.',
49336
+ close: 'Cerrar',
49337
+ genericError: 'No se pudo completar la operación. Intenta de nuevo.',
49338
+ },
49339
+ en: {
49340
+ title: 'Client API Keys',
49341
+ hint: 'Org-scoped M2M keys for integrations (e.g. member import).',
49342
+ empty: 'No API keys in this organization yet.',
49343
+ newKey: 'New API key',
49344
+ nameLabel: 'Name',
49345
+ namePlaceholder: 'E.g: HR integration',
49346
+ permissionsLabel: 'Permissions',
49347
+ expiresLabel: 'Expires in (days, optional)',
49348
+ create: 'Create key',
49349
+ creating: 'Creating...',
49350
+ cancel: 'Cancel',
49351
+ revoke: 'Revoke',
49352
+ revokeConfirm: 'Revoke this API key? Integrations using it will stop working.',
49353
+ secretTitle: 'Copy your API key now',
49354
+ secretWarning: 'This secret is shown only once. Store it somewhere safe.',
49355
+ copy: 'Copy',
49356
+ copied: 'Copied',
49357
+ done: 'Done',
49358
+ statusActive: 'active',
49359
+ lastUsed: 'Last used',
49360
+ never: 'never',
49361
+ permsCount: 'permissions',
49362
+ noName: 'Name is required.',
49363
+ noPerms: 'Select at least one permission.',
49364
+ close: 'Close',
49365
+ genericError: "Couldn't complete the operation. Try again.",
49366
+ },
49367
+ };
49368
+
49369
+ const DEFAULT_NAMESPACE$5 = 'Settings.ApiKeysModal';
49370
+ /**
49371
+ * `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
49372
+ *
49373
+ * Lista las keys de la org, permite crear (mostrando el secreto UNA vez con copia)
49374
+ * y revocar. Pensado para habilitar integraciones M2M del import de miembros, pero
49375
+ * sirve a cualquier permiso del catálogo. Header canónico (Regla #5).
49376
+ */
49377
+ class ApiKeysModalComponent {
49378
+ constructor() {
49379
+ this.i18n = inject(I18nService);
49380
+ this.apiKeys = inject(ApiKeyService);
49381
+ /** Namespace i18n. */
49382
+ this.i18nNamespace = DEFAULT_NAMESPACE$5;
49383
+ this.keys = signal([]);
49384
+ this.availablePermissions = signal([]);
49385
+ this.loading = signal(true);
49386
+ this.showCreate = signal(false);
49387
+ this.submitting = signal(false);
49388
+ this.errorMsg = signal('');
49389
+ this.secret = signal(null);
49390
+ this.copied = signal(false);
49391
+ this.name = '';
49392
+ this.expiresInDays = null;
49393
+ this.selectedPerms = signal(new Set());
49394
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
49395
+ this.i18n.registerContent(DEFAULT_NAMESPACE$5, API_KEYS_MODAL_I18N);
49396
+ }
49397
+ }
49398
+ ngOnInit() {
49399
+ if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49400
+ this.i18n.registerContent(this.i18nNamespace, API_KEYS_MODAL_I18N);
49401
+ }
49402
+ this.refresh();
49403
+ this.apiKeys.availablePermissions().subscribe({
49404
+ next: perms => this.availablePermissions.set(perms),
49405
+ error: () => { },
49406
+ });
49407
+ }
49408
+ refresh() {
49409
+ this.loading.set(true);
49410
+ this.apiKeys.list().subscribe({
49411
+ next: keys => {
49412
+ this.keys.set(keys);
49413
+ this.loading.set(false);
49414
+ },
49415
+ error: () => this.loading.set(false),
49416
+ });
49417
+ }
49418
+ openCreate() {
49419
+ this.errorMsg.set('');
49420
+ this.name = '';
49421
+ this.expiresInDays = null;
49422
+ this.selectedPerms.set(new Set());
49423
+ this.showCreate.set(true);
49424
+ }
49425
+ togglePerm(perm, ev) {
49426
+ const checked = ev.detail?.checked ?? false;
49427
+ const next = new Set(this.selectedPerms());
49428
+ if (checked) {
49429
+ next.add(perm);
49430
+ }
49431
+ else {
49432
+ next.delete(perm);
49433
+ }
49434
+ this.selectedPerms.set(next);
49435
+ }
49436
+ submit() {
49437
+ this.errorMsg.set('');
49438
+ if (!this.name.trim()) {
49439
+ this.errorMsg.set(this.t('noName'));
49440
+ return;
49441
+ }
49442
+ const permissions = Array.from(this.selectedPerms());
49443
+ if (permissions.length === 0) {
49444
+ this.errorMsg.set(this.t('noPerms'));
49445
+ return;
49446
+ }
49447
+ this.submitting.set(true);
49448
+ this.apiKeys
49449
+ .create({
49450
+ name: this.name.trim(),
49451
+ permissions,
49452
+ expiresInDays: this.expiresInDays && this.expiresInDays > 0 ? this.expiresInDays : undefined,
49453
+ })
49454
+ .subscribe({
49455
+ next: res => {
49456
+ this.submitting.set(false);
49457
+ this.showCreate.set(false);
49458
+ this.secret.set(res.key);
49459
+ },
49460
+ error: err => {
49461
+ this.submitting.set(false);
49462
+ this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49463
+ },
49464
+ });
49465
+ }
49466
+ revoke(key) {
49467
+ if (!confirm(this.t('revokeConfirm')))
49468
+ return;
49469
+ this.apiKeys.revoke(key.keyId).subscribe({
49470
+ next: () => this.refresh(),
49471
+ error: err => this.errorMsg.set(err?.error?.message ?? this.t('genericError')),
49472
+ });
49473
+ }
49474
+ async copy(secret) {
49475
+ try {
49476
+ await navigator.clipboard.writeText(secret);
49477
+ this.copied.set(true);
49478
+ }
49479
+ catch {
49480
+ // clipboard no disponible — el user puede seleccionar el texto manualmente
49481
+ }
49482
+ }
49483
+ finishSecret() {
49484
+ this.secret.set(null);
49485
+ this.copied.set(false);
49486
+ this.refresh();
49487
+ }
49488
+ dismiss() {
49489
+ this._modalRef?.dismiss(null, 'cancel');
49490
+ }
49491
+ t(key) {
49492
+ return this.i18n.t(key, this.i18nNamespace);
49493
+ }
49494
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49495
+ 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: `
49496
+ <ion-header>
49497
+ <ion-toolbar>
49498
+ <ion-buttons slot="end">
49499
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49500
+ <strong>{{ t('close') }}</strong>
49501
+ </ion-button>
49502
+ </ion-buttons>
49503
+ </ion-toolbar>
49504
+ </ion-header>
49505
+
49506
+ <ion-content class="ion-padding">
49507
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49508
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49509
+
49510
+ @if (secret(); as s) {
49511
+ <!-- Secreto recién creado: mostrar UNA vez -->
49512
+ <div class="apikey-secret">
49513
+ <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49514
+ <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49515
+ <div class="apikey-secret__box">
49516
+ <code>{{ s.secret }}</code>
49517
+ </div>
49518
+ <div class="apikey-actions">
49519
+ <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49520
+ {{ copied() ? t('copied') : t('copy') }}
49521
+ </ion-button>
49522
+ <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49523
+ {{ t('done') }}
49524
+ </ion-button>
49525
+ </div>
49526
+ </div>
49527
+ } @else {
49528
+ @if (loading()) {
49529
+ <ion-spinner name="dots" />
49530
+ } @else {
49531
+ <!-- Lista -->
49532
+ @if (keys().length === 0) {
49533
+ <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49534
+ } @else {
49535
+ <div class="apikey-list">
49536
+ @for (k of keys(); track k.keyId) {
49537
+ <div class="apikey-row">
49538
+ <div class="apikey-row__main">
49539
+ <span class="apikey-row__name">{{ k.name }}</span>
49540
+ <span class="apikey-row__meta">
49541
+ {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49542
+ {{ k.lastUsedAt || t('never') }}
49543
+ </span>
49544
+ </div>
49545
+ <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49546
+ {{ t('revoke') }}
49547
+ </ion-button>
49548
+ </div>
49549
+ }
49550
+ </div>
49551
+ }
49552
+
49553
+ @if (errorMsg()) {
49554
+ <p class="apikey-error">{{ errorMsg() }}</p>
49555
+ }
49556
+
49557
+ <!-- Crear -->
49558
+ @if (showCreate()) {
49559
+ <div class="apikey-create">
49560
+ <ion-input
49561
+ label="{{ t('nameLabel') }}"
49562
+ labelPlacement="stacked"
49563
+ [placeholder]="t('namePlaceholder')"
49564
+ [(ngModel)]="name"
49565
+ />
49566
+ <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49567
+ <div class="apikey-perms">
49568
+ @for (p of availablePermissions(); track p) {
49569
+ <ion-checkbox
49570
+ labelPlacement="end"
49571
+ [checked]="selectedPerms().has(p)"
49572
+ (ionChange)="togglePerm(p, $event)"
49573
+ >
49574
+ {{ p }}
49575
+ </ion-checkbox>
49576
+ }
49577
+ </div>
49578
+ <ion-input
49579
+ label="{{ t('expiresLabel') }}"
49580
+ labelPlacement="stacked"
49581
+ type="number"
49582
+ [(ngModel)]="expiresInDays"
49583
+ />
49584
+ <div class="apikey-actions">
49585
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49586
+ @if (submitting()) {
49587
+ <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49588
+ } @else {
49589
+ {{ t('create') }}
49590
+ }
49591
+ </ion-button>
49592
+ <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49593
+ {{ t('cancel') }}
49594
+ </ion-button>
49595
+ </div>
49596
+ </div>
49597
+ } @else {
49598
+ <div class="apikey-actions">
49599
+ <ion-button expand="block" color="primary" (click)="openCreate()">
49600
+ {{ t('newKey') }}
49601
+ </ion-button>
49602
+ </div>
49603
+ }
49604
+ }
49605
+ }
49606
+ </ion-content>
49607
+ `, 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"] }] }); }
49608
+ }
49609
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, decorators: [{
49610
+ type: Component,
49611
+ args: [{ selector: 'val-api-keys-modal', standalone: true, imports: [
49612
+ FormsModule,
49613
+ IonHeader,
49614
+ IonToolbar,
49615
+ IonButtons,
49616
+ IonButton,
49617
+ IonContent,
49618
+ IonInput,
49619
+ IonCheckbox,
49620
+ IonSpinner,
49621
+ DisplayComponent,
49622
+ TextComponent,
49623
+ TitleComponent,
49624
+ ], template: `
49625
+ <ion-header>
49626
+ <ion-toolbar>
49627
+ <ion-buttons slot="end">
49628
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49629
+ <strong>{{ t('close') }}</strong>
49630
+ </ion-button>
49631
+ </ion-buttons>
49632
+ </ion-toolbar>
49633
+ </ion-header>
49634
+
49635
+ <ion-content class="ion-padding">
49636
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49637
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49638
+
49639
+ @if (secret(); as s) {
49640
+ <!-- Secreto recién creado: mostrar UNA vez -->
49641
+ <div class="apikey-secret">
49642
+ <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49643
+ <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49644
+ <div class="apikey-secret__box">
49645
+ <code>{{ s.secret }}</code>
49646
+ </div>
49647
+ <div class="apikey-actions">
49648
+ <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49649
+ {{ copied() ? t('copied') : t('copy') }}
49650
+ </ion-button>
49651
+ <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49652
+ {{ t('done') }}
49653
+ </ion-button>
49654
+ </div>
49655
+ </div>
49656
+ } @else {
49657
+ @if (loading()) {
49658
+ <ion-spinner name="dots" />
49659
+ } @else {
49660
+ <!-- Lista -->
49661
+ @if (keys().length === 0) {
49662
+ <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49663
+ } @else {
49664
+ <div class="apikey-list">
49665
+ @for (k of keys(); track k.keyId) {
49666
+ <div class="apikey-row">
49667
+ <div class="apikey-row__main">
49668
+ <span class="apikey-row__name">{{ k.name }}</span>
49669
+ <span class="apikey-row__meta">
49670
+ {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49671
+ {{ k.lastUsedAt || t('never') }}
49672
+ </span>
49673
+ </div>
49674
+ <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49675
+ {{ t('revoke') }}
49676
+ </ion-button>
49677
+ </div>
49678
+ }
49679
+ </div>
49680
+ }
49681
+
49682
+ @if (errorMsg()) {
49683
+ <p class="apikey-error">{{ errorMsg() }}</p>
49684
+ }
49685
+
49686
+ <!-- Crear -->
49687
+ @if (showCreate()) {
49688
+ <div class="apikey-create">
49689
+ <ion-input
49690
+ label="{{ t('nameLabel') }}"
49691
+ labelPlacement="stacked"
49692
+ [placeholder]="t('namePlaceholder')"
49693
+ [(ngModel)]="name"
49694
+ />
49695
+ <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49696
+ <div class="apikey-perms">
49697
+ @for (p of availablePermissions(); track p) {
49698
+ <ion-checkbox
49699
+ labelPlacement="end"
49700
+ [checked]="selectedPerms().has(p)"
49701
+ (ionChange)="togglePerm(p, $event)"
49702
+ >
49703
+ {{ p }}
49704
+ </ion-checkbox>
49705
+ }
49706
+ </div>
49707
+ <ion-input
49708
+ label="{{ t('expiresLabel') }}"
49709
+ labelPlacement="stacked"
49710
+ type="number"
49711
+ [(ngModel)]="expiresInDays"
49712
+ />
49713
+ <div class="apikey-actions">
49714
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49715
+ @if (submitting()) {
49716
+ <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49717
+ } @else {
49718
+ {{ t('create') }}
49719
+ }
49720
+ </ion-button>
49721
+ <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49722
+ {{ t('cancel') }}
49723
+ </ion-button>
49724
+ </div>
49725
+ </div>
49726
+ } @else {
49727
+ <div class="apikey-actions">
49728
+ <ion-button expand="block" color="primary" (click)="openCreate()">
49729
+ {{ t('newKey') }}
49730
+ </ion-button>
49731
+ </div>
49732
+ }
49733
+ }
49734
+ }
49735
+ </ion-content>
49736
+ `, 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"] }]
49737
+ }], ctorParameters: () => [], propDecorators: { _modalRef: [{
49738
+ type: Input
49739
+ }], i18nNamespace: [{
49740
+ type: Input
49741
+ }] } });
49742
+
48849
49743
  /**
48850
49744
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
48851
49745
  * el constructor del componente si el consumer no proveyó el namespace
@@ -48901,6 +49795,16 @@ const ORGANIZATION_VIEW_I18N = {
48901
49795
  inviteCtaTitle: '¿Deseas agregar a alguien?',
48902
49796
  inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
48903
49797
  inviteOpen: 'Invitar',
49798
+ importOpen: 'Importar',
49799
+ apiKeysOpen: 'Gestionar',
49800
+ apiKeysSectionTitle: 'Integraciones (API Keys)',
49801
+ 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).',
49802
+ apiKeysMoreInfo: 'Más información',
49803
+ orgsMoreInfo: 'Más información',
49804
+ orgsInfoTitle: '¿Qué son las organizaciones?',
49805
+ orgsInfoSubtitle: 'Todo lo que necesitas saber',
49806
+ orgsInfoClose: 'Cerrar',
49807
+ 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.',
48904
49808
  viewMemberCta: 'Ver miembro',
48905
49809
  roleViewer: 'Visor',
48906
49810
  roleEditor: 'Editor',
@@ -48970,6 +49874,16 @@ const ORGANIZATION_VIEW_I18N = {
48970
49874
  inviteCtaTitle: 'Want to add someone?',
48971
49875
  inviteCtaHint: 'Invite users to join your organization.',
48972
49876
  inviteOpen: 'Invite',
49877
+ importOpen: 'Import',
49878
+ apiKeysOpen: 'Manage',
49879
+ apiKeysSectionTitle: 'Integrations (API Keys)',
49880
+ 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).',
49881
+ apiKeysMoreInfo: 'More information',
49882
+ orgsMoreInfo: 'More information',
49883
+ orgsInfoTitle: 'What are organizations?',
49884
+ orgsInfoSubtitle: 'Everything you need to know',
49885
+ orgsInfoClose: 'Close',
49886
+ 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.',
48973
49887
  viewMemberCta: 'View member',
48974
49888
  roleViewer: 'Viewer',
48975
49889
  roleEditor: 'Editor',
@@ -48994,7 +49908,7 @@ const ORGANIZATION_VIEW_I18N = {
48994
49908
  },
48995
49909
  };
48996
49910
 
48997
- const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49911
+ const DEFAULT_NAMESPACE$4 = 'Settings.Organization';
48998
49912
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
48999
49913
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49000
49914
  /**
@@ -49051,7 +49965,7 @@ class OrganizationViewComponent {
49051
49965
  showInvite: merged.showInvite ?? true,
49052
49966
  showTransferOwnership: merged.showTransferOwnership ?? true,
49053
49967
  showLeave: merged.showLeave ?? true,
49054
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49968
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$4,
49055
49969
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49056
49970
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49057
49971
  onOrgEdited: merged.onOrgEdited,
@@ -49083,6 +49997,22 @@ class OrganizationViewComponent {
49083
49997
  this.members = signal([]);
49084
49998
  this.membersLoading = signal(false);
49085
49999
  this.membersLoadError = signal(null);
50000
+ this.membersErrorState = computed(() => {
50001
+ this.i18n.lang();
50002
+ const err = this.membersLoadError();
50003
+ if (!err)
50004
+ return null;
50005
+ return createErrorStateProps(err, {
50006
+ title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
50007
+ description: {
50008
+ offline: this.tt('offlineHint'),
50009
+ error: interpretError(err).message,
50010
+ },
50011
+ retryLabel: this.tt('retry'),
50012
+ onRetry: () => this.loadMembers(),
50013
+ retrying: this.membersLoading(),
50014
+ });
50015
+ });
49086
50016
  this.membersNextToken = signal(null);
49087
50017
  this.loadingMoreMembers = signal(false);
49088
50018
  this.showAllMembers = signal(false);
@@ -49169,6 +50099,22 @@ class OrganizationViewComponent {
49169
50099
  shape: 'round',
49170
50100
  type: 'button',
49171
50101
  }));
50102
+ this.openImportButtonProps = computed(() => ({
50103
+ text: this.tt('importOpen'),
50104
+ color: 'dark',
50105
+ fill: 'outline',
50106
+ size: 'default',
50107
+ shape: 'round',
50108
+ type: 'button',
50109
+ }));
50110
+ this.openApiKeysButtonProps = computed(() => ({
50111
+ text: this.tt('apiKeysOpen'),
50112
+ color: 'dark',
50113
+ fill: 'clear',
50114
+ size: 'default',
50115
+ shape: 'round',
50116
+ type: 'button',
50117
+ }));
49172
50118
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
49173
50119
  this.lastLoadedOrgId = null;
49174
50120
  const ns = this.ns;
@@ -49269,6 +50215,59 @@ class OrganizationViewComponent {
49269
50215
  backdropDismiss: true,
49270
50216
  });
49271
50217
  }
50218
+ onOpenImport() {
50219
+ void this.modalService.open({
50220
+ component: MemberImportModalComponent,
50221
+ componentProps: {
50222
+ orgId: this.activeOrgId(),
50223
+ availableRoles: this.availableRoles().map(r => r.name),
50224
+ onSuccess: () => {
50225
+ this.loadMembers();
50226
+ this.resolvedConfig().onMemberInvited?.();
50227
+ },
50228
+ },
50229
+ breakpoints: {
50230
+ breakpoints: [0, 0.92],
50231
+ initialBreakpoint: 0.92,
50232
+ showHandle: true,
50233
+ },
50234
+ backdropDismiss: true,
50235
+ });
50236
+ }
50237
+ onOpenApiKeys() {
50238
+ void this.modalService.open({
50239
+ component: ApiKeysModalComponent,
50240
+ breakpoints: {
50241
+ breakpoints: [0, 0.92],
50242
+ initialBreakpoint: 0.92,
50243
+ showHandle: true,
50244
+ },
50245
+ backdropDismiss: true,
50246
+ });
50247
+ }
50248
+ // "Más información" sobre organizaciones — mismo sheet explicativo que muestra
50249
+ // account-view sin org, pero disponible siempre (todo user tiene org por defecto).
50250
+ onOrgInfo() {
50251
+ void this.modalService.open({
50252
+ component: OrgInfoSheetComponent,
50253
+ componentProps: {
50254
+ title: this.tt('orgsInfoTitle'),
50255
+ subtitle: this.tt('orgsInfoSubtitle'),
50256
+ body: this.tt('orgsInfoContent'),
50257
+ closeLabel: this.tt('orgsInfoClose'),
50258
+ closeProps: {
50259
+ text: this.tt('orgsInfoClose'),
50260
+ fill: 'outline',
50261
+ color: 'dark',
50262
+ size: 'default',
50263
+ expand: 'block',
50264
+ type: 'button',
50265
+ },
50266
+ },
50267
+ breakpoints: { breakpoints: [0, 0.85], initialBreakpoint: 0.85, showHandle: true },
50268
+ backdropDismiss: true,
50269
+ });
50270
+ }
49272
50271
  onViewPermissions() {
49273
50272
  this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
49274
50273
  }
@@ -49350,6 +50349,7 @@ class OrganizationViewComponent {
49350
50349
  if (!orgId)
49351
50350
  return Promise.resolve();
49352
50351
  this.membersLoading.set(true);
50352
+ this.membersLoadError.set(null);
49353
50353
  this.membersNextToken.set(null);
49354
50354
  this.showAllMembers.set(false);
49355
50355
  return new Promise(resolve => {
@@ -49513,6 +50513,12 @@ class OrganizationViewComponent {
49513
50513
  }
49514
50514
  </div>
49515
50515
 
50516
+ <!-- "Más información" sobre organizaciones — disponible siempre (todo
50517
+ user tiene org por defecto y no ve el empty-state de account). -->
50518
+ <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50519
+ {{ tt('orgsMoreInfo') }}
50520
+ </button>
50521
+
49516
50522
  <div class="section-body">
49517
50523
  @if (loading()) {
49518
50524
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -49560,6 +50566,8 @@ class OrganizationViewComponent {
49560
50566
  <div class="section-body">
49561
50567
  @if (membersLoading()) {
49562
50568
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50569
+ } @else if (membersErrorState()) {
50570
+ <val-empty-state [props]="membersErrorState()!" />
49563
50571
  } @else if (members().length === 0) {
49564
50572
  <val-text
49565
50573
  [props]="{
@@ -49652,10 +50660,25 @@ class OrganizationViewComponent {
49652
50660
  />
49653
50661
  </div>
49654
50662
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50663
+ <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
49655
50664
  </div>
49656
50665
  </section>
49657
50666
  }
49658
50667
 
50668
+ <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50669
+ <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50670
+ <div class="invite-cta">
50671
+ <div class="invite-cta__text">
50672
+ <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50673
+ <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50674
+ <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50675
+ {{ tt('apiKeysMoreInfo') }}
50676
+ </button>
50677
+ </div>
50678
+ <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50679
+ </div>
50680
+ </section>
50681
+
49659
50682
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
49660
50683
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
49661
50684
  <section class="settings-section" data-testid="org-transfer-section">
@@ -49746,7 +50769,7 @@ class OrganizationViewComponent {
49746
50769
  </section>
49747
50770
  }
49748
50771
  </div>
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"] }] }); }
50772
+ `, 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"] }] }); }
49750
50773
  }
49751
50774
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
49752
50775
  type: Component,
@@ -49760,6 +50783,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49760
50783
  ButtonComponent,
49761
50784
  MemberCardComponent,
49762
50785
  TransferOwnershipModalComponent,
50786
+ MemberImportModalComponent,
50787
+ ApiKeysModalComponent,
50788
+ OrgInfoSheetComponent,
49763
50789
  ], template: `
49764
50790
  <div class="page">
49765
50791
  <header class="page-header">
@@ -49791,6 +50817,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49791
50817
  }
49792
50818
  </div>
49793
50819
 
50820
+ <!-- "Más información" sobre organizaciones — disponible siempre (todo
50821
+ user tiene org por defecto y no ve el empty-state de account). -->
50822
+ <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50823
+ {{ tt('orgsMoreInfo') }}
50824
+ </button>
50825
+
49794
50826
  <div class="section-body">
49795
50827
  @if (loading()) {
49796
50828
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -49838,6 +50870,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49838
50870
  <div class="section-body">
49839
50871
  @if (membersLoading()) {
49840
50872
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50873
+ } @else if (membersErrorState()) {
50874
+ <val-empty-state [props]="membersErrorState()!" />
49841
50875
  } @else if (members().length === 0) {
49842
50876
  <val-text
49843
50877
  [props]="{
@@ -49930,10 +50964,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49930
50964
  />
49931
50965
  </div>
49932
50966
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50967
+ <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
49933
50968
  </div>
49934
50969
  </section>
49935
50970
  }
49936
50971
 
50972
+ <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50973
+ <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50974
+ <div class="invite-cta">
50975
+ <div class="invite-cta__text">
50976
+ <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50977
+ <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50978
+ <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50979
+ {{ tt('apiKeysMoreInfo') }}
50980
+ </button>
50981
+ </div>
50982
+ <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50983
+ </div>
50984
+ </section>
50985
+
49937
50986
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
49938
50987
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
49939
50988
  <section class="settings-section" data-testid="org-transfer-section">
@@ -50024,7 +51073,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50024
51073
  </section>
50025
51074
  }
50026
51075
  </div>
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"] }]
51076
+ `, 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"] }]
50028
51077
  }], ctorParameters: () => [], propDecorators: { config: [{
50029
51078
  type: Input
50030
51079
  }] } });
@@ -50138,7 +51187,7 @@ addIcons({
50138
51187
  notificationsOutline,
50139
51188
  refreshOutline,
50140
51189
  });
50141
- const DEFAULT_NAMESPACE$2 = 'Settings.Notifications';
51190
+ const DEFAULT_NAMESPACE$3 = 'Settings.Notifications';
50142
51191
  /**
50143
51192
  * `val-notification-preferences-view` — **preferencias de notificaciones push**
50144
51193
  * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
@@ -50181,7 +51230,7 @@ class NotificationPreferencesViewComponent {
50181
51230
  const merged = { ...fromRoute, ...(this.config ?? {}) };
50182
51231
  return {
50183
51232
  showDetails: merged.showDetails ?? true,
50184
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
51233
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
50185
51234
  onPushEnabled: merged.onPushEnabled,
50186
51235
  onPushDisabled: merged.onPushDisabled,
50187
51236
  };
@@ -50952,7 +52001,7 @@ const PERMISSIONS_VIEW_I18N = {
50952
52001
  },
50953
52002
  };
50954
52003
 
50955
- const DEFAULT_NAMESPACE$1 = 'Settings.Permissions';
52004
+ const DEFAULT_NAMESPACE$2 = 'Settings.Permissions';
50956
52005
  /**
50957
52006
  * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
50958
52007
  * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
@@ -50992,7 +52041,7 @@ class PermissionsViewComponent {
50992
52041
  return {
50993
52042
  showSystemRoles: merged.showSystemRoles ?? true,
50994
52043
  showCustomRoles: merged.showCustomRoles ?? true,
50995
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
52044
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
50996
52045
  };
50997
52046
  });
50998
52047
  this.loading = signal(false);
@@ -51769,7 +52818,7 @@ addIcons({
51769
52818
  openOutline,
51770
52819
  personOutline,
51771
52820
  });
51772
- const DEFAULT_NAMESPACE = 'Notifications';
52821
+ const DEFAULT_NAMESPACE$1 = 'Notifications';
51773
52822
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
51774
52823
  const DEFAULT_PAGE_SIZE = 10;
51775
52824
  const DEFAULT_MAX_LOAD = 50;
@@ -51829,7 +52878,7 @@ class NotificationsViewComponent {
51829
52878
  showPushOffCta: merged.showPushOffCta ?? true,
51830
52879
  enableMarkAll: merged.enableMarkAll ?? true,
51831
52880
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
51832
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
52881
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
51833
52882
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
51834
52883
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
51835
52884
  onNotificationOpen: merged.onNotificationOpen,
@@ -52497,6 +53546,797 @@ function provideValtechNotificationsRoutes(opts) {
52497
53546
  ];
52498
53547
  }
52499
53548
 
53549
+ /**
53550
+ * Defaults i18n del componente (es/en). El componente los auto-registra en su
53551
+ * constructor si el consumer no proveyó el namespace `ContentReaction`. Esto
53552
+ * garantiza que NUNCA haya una key faltante: una key faltante se evalúa en el
53553
+ * template por cada change-detection → `console.warn` por CD → con el
53554
+ * `val-debug-console` activo (espeja console.* a un signal) se dispara un loop
53555
+ * infinito que congela el navegador en dev. Auto-registrar lo elimina de raíz.
53556
+ */
53557
+ const CONTENT_REACTION_I18N = {
53558
+ es: {
53559
+ question: '¿Te resultó útil este contenido?',
53560
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
53561
+ submit: 'Enviar opinión',
53562
+ update: 'Actualizar opinión',
53563
+ thankYou: '¡Gracias por tu opinión!',
53564
+ submitted: 'Tu opinión ha sido registrada',
53565
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53566
+ positiveLabel: 'Sí, mucho',
53567
+ negativeLabel: 'No tanto',
53568
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53569
+ },
53570
+ en: {
53571
+ question: 'Was this content helpful?',
53572
+ commentPlaceholder: 'Tell us more (optional)...',
53573
+ submit: 'Send feedback',
53574
+ update: 'Update feedback',
53575
+ thankYou: 'Thanks for your feedback!',
53576
+ submitted: 'Your feedback has been recorded',
53577
+ errorSubmitting: 'Failed to send. Please try again.',
53578
+ positiveLabel: 'Yes, a lot',
53579
+ negativeLabel: 'Not really',
53580
+ negativeFeedbackTitle: "Tell us what you didn't like?",
53581
+ },
53582
+ };
53583
+ /**
53584
+ * Componente para reacciones de contenido con emojis.
53585
+ *
53586
+ * @example
53587
+ * ```html
53588
+ * <val-content-reaction
53589
+ * [props]="{
53590
+ * entityRef: { entityType: 'article', entityId: 'art-123' },
53591
+ * question: '¿Te fue útil este artículo?'
53592
+ * }"
53593
+ * (reactionSubmit)="onReactionSubmit($event)"
53594
+ * />
53595
+ * ```
53596
+ */
53597
+ class ContentReactionComponent {
53598
+ constructor() {
53599
+ this.feedbackService = inject(FeedbackService);
53600
+ this.toast = inject(ToastService);
53601
+ this.i18n = inject(I18nService);
53602
+ this.auth = inject(AuthService, { optional: true });
53603
+ this.isConfigured = computed(() => this.feedbackService.isConfigured());
53604
+ this.props = {};
53605
+ this.reactionSubmit = new EventEmitter();
53606
+ this.reactionChange = new EventEmitter();
53607
+ // Estado reactivo
53608
+ this.state = signal({
53609
+ selectedValue: null,
53610
+ comment: '',
53611
+ isLoading: false,
53612
+ isSubmitted: false,
53613
+ hadPreviousReaction: false,
53614
+ error: null,
53615
+ });
53616
+ // Valores por defecto
53617
+ this.defaultEmojis = ['😞', '😐', '😊'];
53618
+ this.defaultLabels = ['No me ayudó', 'Regular', 'Muy útil'];
53619
+ // Computed properties
53620
+ this.resolvedProps = computed(() => ({
53621
+ entityRef: this.props.entityRef,
53622
+ question: this.props.question || this.t('question'),
53623
+ showComment: this.props.showComment ?? true,
53624
+ commentOnValues: this.props.commentOnValues,
53625
+ commentPlaceholder: this.props.commentPlaceholder || this.t('commentPlaceholder'),
53626
+ maxCommentLength: this.props.maxCommentLength ?? 500,
53627
+ emojis: this.props.emojis || this.defaultEmojis,
53628
+ emojiLabels: this.props.emojiLabels || this.defaultLabels,
53629
+ showThankYou: this.props.showThankYou ?? true,
53630
+ thankYouMessage: this.props.thankYouMessage || this.t('thankYou'),
53631
+ disabled: this.props.disabled ?? false,
53632
+ readonly: this.props.readonly ?? false,
53633
+ // Variante: 'emoji' cuando se declara explícitamente o cuando se pasan emojis custom.
53634
+ variant: this.props.variant ?? (this.props.emojis ? 'emoji' : 'buttons'),
53635
+ }));
53636
+ this.activeReactionValues = computed(() => this.resolvedProps().emojis.length === 2 ? ['negative', 'positive'] : ['negative', 'neutral', 'positive']);
53637
+ this.showCommentField = computed(() => {
53638
+ const sel = this.state().selectedValue;
53639
+ if (!sel)
53640
+ return false;
53641
+ const commentOn = this.resolvedProps().commentOnValues;
53642
+ if (commentOn !== undefined)
53643
+ return commentOn.includes(sel);
53644
+ return this.resolvedProps().showComment;
53645
+ });
53646
+ this.canSubmit = computed(() => this.state().selectedValue !== null && !this.state().isLoading);
53647
+ // Auto-registro de defaults i18n (es/en) si el consumer no proveyó el
53648
+ // namespace. Garantiza que `t()` nunca caiga en key faltante. Ver
53649
+ // CONTENT_REACTION_I18N. Idempotente: solo la 1ª instancia registra.
53650
+ if (!this.i18n.hasNamespace('ContentReaction')) {
53651
+ this.i18n.registerContent('ContentReaction', CONTENT_REACTION_I18N);
53652
+ }
53653
+ }
53654
+ ngOnInit() {
53655
+ this.loadPreviousReaction();
53656
+ }
53657
+ ngOnChanges(changes) {
53658
+ if (changes['props'] && !changes['props'].firstChange) {
53659
+ this.loadPreviousReaction();
53660
+ }
53661
+ }
53662
+ async loadPreviousReaction() {
53663
+ if (!this.props.entityRef)
53664
+ return;
53665
+ // Si hay initialValue, usarlo directamente sin consultar
53666
+ if (this.props.initialValue) {
53667
+ this.state.update(s => ({
53668
+ ...s,
53669
+ selectedValue: this.props.initialValue,
53670
+ hadPreviousReaction: true,
53671
+ isLoading: false,
53672
+ isSubmitted: true,
53673
+ }));
53674
+ return;
53675
+ }
53676
+ // Si skipInitialCheck está activo, no consultar
53677
+ if (this.props.skipInitialCheck) {
53678
+ return;
53679
+ }
53680
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
53681
+ try {
53682
+ const check = await this.feedbackService.checkFeedback(this.props.entityRef.entityType, this.props.entityRef.entityId);
53683
+ if (check.hasFeedback && check.reactionValue) {
53684
+ this.state.update(s => ({
53685
+ ...s,
53686
+ selectedValue: check.reactionValue,
53687
+ hadPreviousReaction: true,
53688
+ isLoading: false,
53689
+ isSubmitted: true,
53690
+ }));
53691
+ }
53692
+ else {
53693
+ this.state.update(s => ({ ...s, isLoading: false }));
53694
+ }
53695
+ }
53696
+ catch (error) {
53697
+ console.error('Error loading previous reaction:', error);
53698
+ this.state.update(s => ({ ...s, isLoading: false }));
53699
+ }
53700
+ }
53701
+ selectReaction(value) {
53702
+ if (this.resolvedProps().disabled || this.resolvedProps().readonly)
53703
+ return;
53704
+ const previousValue = this.state().selectedValue;
53705
+ this.state.update(s => ({
53706
+ ...s,
53707
+ selectedValue: value,
53708
+ isSubmitted: false,
53709
+ error: null,
53710
+ }));
53711
+ this.reactionChange.emit({ value, previousValue });
53712
+ }
53713
+ async submitReaction() {
53714
+ const currentState = this.state();
53715
+ const props = this.resolvedProps();
53716
+ if (!currentState.selectedValue || props.disabled)
53717
+ return;
53718
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
53719
+ try {
53720
+ // Determinar si usar endpoint anónimo o autenticado
53721
+ const isAuthenticated = this.auth?.isAuthenticated() ?? false;
53722
+ const useAnonymous = this.props.allowAnonymous && !isAuthenticated;
53723
+ if (useAnonymous) {
53724
+ // Usar endpoint anónimo (público)
53725
+ await this.feedbackService.createAnonymousReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53726
+ }
53727
+ else {
53728
+ // Usar endpoint autenticado
53729
+ await this.feedbackService.createReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53730
+ }
53731
+ this.state.update(s => ({
53732
+ ...s,
53733
+ isLoading: false,
53734
+ isSubmitted: true,
53735
+ comment: '',
53736
+ hadPreviousReaction: !useAnonymous, // Solo para autenticados
53737
+ }));
53738
+ this.reactionSubmit.emit({
53739
+ value: currentState.selectedValue,
53740
+ comment: currentState.comment || undefined,
53741
+ entityRef: props.entityRef,
53742
+ isUpdate: currentState.hadPreviousReaction,
53743
+ });
53744
+ if (props.showThankYou) {
53745
+ this.toast.show({
53746
+ message: props.thankYouMessage,
53747
+ duration: 2000,
53748
+ });
53749
+ }
53750
+ }
53751
+ catch (error) {
53752
+ console.error('Error submitting reaction:', error);
53753
+ this.state.update(s => ({
53754
+ ...s,
53755
+ isLoading: false,
53756
+ error: this.t('errorSubmitting'),
53757
+ }));
53758
+ this.toast.show({
53759
+ message: this.t('errorSubmitting'),
53760
+ duration: 3000,
53761
+ });
53762
+ }
53763
+ }
53764
+ updateComment(event) {
53765
+ const value = event.detail.value || '';
53766
+ this.state.update(s => ({ ...s, comment: value }));
53767
+ }
53768
+ getEmoji(index) {
53769
+ return this.resolvedProps().emojis[index];
53770
+ }
53771
+ getEmojiLabel(index) {
53772
+ return this.resolvedProps().emojiLabels[index];
53773
+ }
53774
+ isSelected(value) {
53775
+ return this.state().selectedValue === value;
53776
+ }
53777
+ t(key) {
53778
+ const translations = {
53779
+ question: '¿Te resultó útil este contenido?',
53780
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
53781
+ submit: 'Enviar opinión',
53782
+ update: 'Actualizar opinión',
53783
+ thankYou: '¡Gracias por tu opinión!',
53784
+ submitted: 'Tu opinión ha sido registrada',
53785
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53786
+ positiveLabel: 'Sí, mucho',
53787
+ negativeLabel: 'No tanto',
53788
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53789
+ };
53790
+ return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
53791
+ }
53792
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53793
+ 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"] }] }); }
53794
+ }
53795
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
53796
+ type: Component,
53797
+ 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"] }]
53798
+ }], ctorParameters: () => [], propDecorators: { props: [{
53799
+ type: Input
53800
+ }], reactionSubmit: [{
53801
+ type: Output
53802
+ }], reactionChange: [{
53803
+ type: Output
53804
+ }] } });
53805
+
53806
+ /**
53807
+ * Defaults i18n (es/en) embebidos en `val-about-view`. Auto-registrados en el
53808
+ * constructor del componente si el consumer no proveyó el namespace
53809
+ * (`Settings.About` por default). SOLO incluye las keys que la vista usa.
53810
+ *
53811
+ * Textos portados EXACTOS del override `Settings.About` de showcase
53812
+ * (`frontend/showcase/src/i18n/index.ts`). Las keys de las acciones
53813
+ * showcase-specific (`feedback*`, `share*`, `faq*`, `donate*`) NO se incluyen:
53814
+ * esas acciones se modelan ahora como `actions[]` de la config (el consumer
53815
+ * aporta sus propios textos). `copyrightText` se incluye aquí (antes salía del
53816
+ * namespace `Layout`) para que la vista sea autocontenida.
53817
+ */
53818
+ const ABOUT_VIEW_I18N = {
53819
+ es: {
53820
+ pageTitle: 'Acerca de',
53821
+ pageDescription: 'Demo del factory Valtech',
53822
+ tagline: 'App de demo — explora las funcionalidades en vivo',
53823
+ descTitle: 'Acerca de Showcase',
53824
+ 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.',
53825
+ actionsTitle: 'Acciones',
53826
+ rateTitle: 'Calificar la app',
53827
+ rateDesc: 'Cuéntanos qué te ha parecido la experiencia.',
53828
+ rateModalTitle: 'Tu opinión',
53829
+ rateQuestion: '¿Qué te ha parecido la experiencia hasta ahora?',
53830
+ rateComment: 'Cuéntanos más (opcional)…',
53831
+ rateThanks: '¡Gracias por tu opinión!',
53832
+ rateClose: 'Cerrar',
53833
+ rateBad: 'No tanto',
53834
+ rateMid: 'Bien',
53835
+ rateGood: '¡Me encanta!',
53836
+ socialTitle: 'Síguenos',
53837
+ socialDesc: 'Estamos en redes sociales.',
53838
+ versionLabel: 'Versión',
53839
+ versionCopied: 'v{v} copiado',
53840
+ copyrightText: '© {year}, Soluciones Valtech Ltda. o sus afiliados. Todos los derechos reservados.',
53841
+ },
53842
+ en: {
53843
+ pageTitle: 'About',
53844
+ pageDescription: 'Valtech factory demo',
53845
+ tagline: 'Demo app — explore features live',
53846
+ descTitle: 'About Showcase',
53847
+ descBody: 'Showcase is the reference app for the Valtech factory. Explore the full flow live: authentication, profile, organizations, notifications and account settings.',
53848
+ actionsTitle: 'Actions',
53849
+ rateTitle: 'Rate the app',
53850
+ rateDesc: 'Tell us how your experience has been.',
53851
+ rateModalTitle: 'Your feedback',
53852
+ rateQuestion: 'How has your experience been so far?',
53853
+ rateComment: 'Tell us more (optional)…',
53854
+ rateThanks: 'Thanks for your feedback!',
53855
+ rateClose: 'Close',
53856
+ rateBad: 'Not great',
53857
+ rateMid: 'Good',
53858
+ rateGood: 'Love it!',
53859
+ socialTitle: 'Follow us',
53860
+ socialDesc: "We're on social media.",
53861
+ versionLabel: 'Version',
53862
+ versionCopied: 'v{v} copied',
53863
+ copyrightText: '© {year}, Soluciones Valtech Ltda. or its affiliates. All rights reserved.',
53864
+ },
53865
+ };
53866
+
53867
+ addIcons({
53868
+ copyOutline,
53869
+ starOutline,
53870
+ chatbubbleEllipsesOutline,
53871
+ shareSocialOutline,
53872
+ helpCircleOutline,
53873
+ logoFacebook,
53874
+ logoInstagram,
53875
+ logoLinkedin,
53876
+ logoTwitter,
53877
+ logoYoutube,
53878
+ logoTiktok,
53879
+ });
53880
+ const DEFAULT_NAMESPACE = 'Settings.About';
53881
+ const DEFAULT_BRAND_NAME = 'Valtech';
53882
+ const DEFAULT_LOGO_SRC = 'assets/images/main-icon.png';
53883
+ const DEFAULT_RATING_ENTITY_ID = 'experience';
53884
+ /**
53885
+ * `val-about-view` — vista About full-feature autocontenida (organism).
53886
+ * Promovida desde `showcase` bajo el proceso de ADR-021. Reúne:
53887
+ * - **HERO** — logo + nombre de marca + tagline + pill de versión (copiable).
53888
+ * - **Descripción** — título + cuerpo.
53889
+ * - **Acciones** — action-card de rating built-in (abre `val-content-reaction`
53890
+ * DIRECTAMENTE vía `ModalService`, sin wrapper local — decisión CTO) + las
53891
+ * acciones genéricas que el consumer aporte por `config.actions`.
53892
+ * - **Redes** — links sociales (`config.socialLinks`); sección oculta si vacía.
53893
+ * - **Footer** — copyright con el año actual.
53894
+ *
53895
+ * Lo app-specific (logo, nombre, versión, redes, acciones share/faq) viaja como
53896
+ * DATA de config — NO como slots (ADR-021). La versión la pasa el consumer
53897
+ * (`config.version` = su `APP_VERSION`); la lib NO importa el `version.ts` de la
53898
+ * app. Si no se pasa versión, el pill se oculta.
53899
+ *
53900
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute`
53901
+ * se inyecta `{ optional: true }` solo para leer el route data de config.
53902
+ *
53903
+ * Auto-registra sus defaults i18n (es/en) en el constructor si el consumer no
53904
+ * proveyó el namespace configurado (default `Settings.About`).
53905
+ *
53906
+ * GOTCHA i18n ContentReaction: `val-content-reaction` exige su namespace
53907
+ * `ContentReaction` registrado donde se use (key faltante en binding + CD =
53908
+ * navegador congelado en dev). `ContentReactionComponent` se auto-registra ese
53909
+ * namespace en SU constructor, y como se abre vía `ModalService` ese constructor
53910
+ * corre antes de que su template binde — queda cubierto. Igual lo importamos
53911
+ * como dependencia explícita de esta vista para que la garantía sea local.
53912
+ */
53913
+ class AboutViewComponent {
53914
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
53915
+ get ns() {
53916
+ return this.resolvedConfig().i18nNamespace;
53917
+ }
53918
+ constructor() {
53919
+ this.nav = inject(NavigationService);
53920
+ this.i18n = inject(I18nService);
53921
+ this.modalService = inject(ModalService);
53922
+ this.toast = inject(ToastService);
53923
+ this.route = inject(ActivatedRoute, { optional: true });
53924
+ this.resolvedConfig = computed(() => {
53925
+ const fromRoute = (this.route?.snapshot.data['aboutConfig'] ?? {});
53926
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
53927
+ return {
53928
+ brand: {
53929
+ name: merged.brand?.name ?? DEFAULT_BRAND_NAME,
53930
+ logoSrc: merged.brand?.logoSrc ?? DEFAULT_LOGO_SRC,
53931
+ },
53932
+ version: merged.version ?? '',
53933
+ socialLinks: merged.socialLinks ?? [],
53934
+ showRating: merged.showRating ?? true,
53935
+ ratingEntityId: merged.ratingEntityId ?? DEFAULT_RATING_ENTITY_ID,
53936
+ actions: merged.actions ?? [],
53937
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
53938
+ onRated: merged.onRated,
53939
+ };
53940
+ });
53941
+ this.brandName = computed(() => this.resolvedConfig().brand.name);
53942
+ this.logoSrc = computed(() => this.resolvedConfig().brand.logoSrc);
53943
+ this.version = computed(() => this.resolvedConfig().version);
53944
+ this.socialLinks = computed(() => this.resolvedConfig().socialLinks);
53945
+ this.showRating = computed(() => this.resolvedConfig().showRating);
53946
+ this.actions = computed(() => this.resolvedConfig().actions);
53947
+ /** Copyright del footer — año actual interpolado en `copyrightText`. */
53948
+ this.copyright = computed(() => {
53949
+ this.i18n.lang();
53950
+ const year = String(new Date().getFullYear());
53951
+ return this.tt('copyrightText').replace('{year}', year);
53952
+ });
53953
+ /** Title metadata para secciones — bold + size small. */
53954
+ this.rateActionCard = computed(() => {
53955
+ this.i18n.lang();
53956
+ return {
53957
+ icon: { ionicon: 'star-outline', color: 'primary' },
53958
+ title: this.tt('rateTitle'),
53959
+ description: this.tt('rateDesc'),
53960
+ size: 'medium',
53961
+ bordered: false,
53962
+ shadowed: false,
53963
+ };
53964
+ });
53965
+ const ns = this.ns;
53966
+ if (!this.i18n.hasNamespace(ns)) {
53967
+ this.i18n.registerContent(ns, ABOUT_VIEW_I18N);
53968
+ }
53969
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
53970
+ }
53971
+ /** Helper i18n bound al namespace resuelto. Reactivo vía `lang()`. */
53972
+ tt(key) {
53973
+ this.i18n.lang();
53974
+ return this.i18n.t(key, this.ns);
53975
+ }
53976
+ /** Title metadata para secciones — bold + size small. */
53977
+ sectionTitle(key) {
53978
+ this.i18n.lang();
53979
+ return {
53980
+ content: this.tt(key),
53981
+ size: 'small',
53982
+ color: 'dark',
53983
+ bold: true,
53984
+ };
53985
+ }
53986
+ /** ActionCardMetadata para una acción genérica del consumer. */
53987
+ extraActionCard(action) {
53988
+ return {
53989
+ icon: { ionicon: action.icon ?? 'chatbubble-ellipses-outline', color: 'primary' },
53990
+ title: action.label,
53991
+ description: action.description,
53992
+ size: 'medium',
53993
+ bordered: false,
53994
+ shadowed: false,
53995
+ };
53996
+ }
53997
+ /** Copia la versión al portapapeles + toast (color dark). */
53998
+ async copyVersion() {
53999
+ const v = this.version();
54000
+ if (!v)
54001
+ return;
54002
+ try {
54003
+ await navigator.clipboard.writeText(v);
54004
+ this.toast.show({
54005
+ message: this.tt('versionCopied').replace('{v}', v),
54006
+ color: 'dark',
54007
+ duration: 1500,
54008
+ });
54009
+ }
54010
+ catch {
54011
+ // clipboard no disponible — silencioso
54012
+ }
54013
+ }
54014
+ /** Abre una URL externa en nueva tab. `mailto:` queda en la misma tab. */
54015
+ openUrl(url) {
54016
+ if (url.startsWith('mailto:')) {
54017
+ window.location.href = url;
54018
+ return;
54019
+ }
54020
+ window.open(url, '_blank', 'noopener,noreferrer');
54021
+ }
54022
+ /**
54023
+ * "Calificar la app" — abre `val-content-reaction` DIRECTAMENTE como sheet
54024
+ * modal (sin wrapper local — decisión CTO). El componente persiste la reacción
54025
+ * contra el backend de feedback y muestra su propio estado de "gracias"; el
54026
+ * sheet se cierra arrastrando (breakpoint 0). El namespace i18n
54027
+ * `ContentReaction` lo auto-registra `ContentReactionComponent` en su
54028
+ * constructor, que corre al crear el modal — sin riesgo de key faltante.
54029
+ *
54030
+ * Sheet (bottom sheet): el contenido scrollea dentro y el botón "Enviar" queda
54031
+ * siempre alcanzable, también en viewports chicos.
54032
+ */
54033
+ async openRating() {
54034
+ const reactionProps = {
54035
+ // entityType "app" distingue esta reacción de las de artículos/FAQs.
54036
+ entityRef: { entityType: 'app', entityId: this.resolvedConfig().ratingEntityId },
54037
+ question: this.tt('rateQuestion'),
54038
+ commentPlaceholder: this.tt('rateComment'),
54039
+ thankYouMessage: this.tt('rateThanks'),
54040
+ showComment: true,
54041
+ showThankYou: true,
54042
+ // Modal one-shot: NO bloquear el render con el check de rating previo.
54043
+ skipInitialCheck: true,
54044
+ emojis: ['😕', '🙂', '😍'],
54045
+ emojiLabels: [this.tt('rateBad'), this.tt('rateMid'), this.tt('rateGood')],
54046
+ };
54047
+ await this.modalService.open({
54048
+ component: ContentReactionComponent,
54049
+ componentProps: { props: reactionProps },
54050
+ breakpoints: {
54051
+ initialBreakpoint: 0.9,
54052
+ breakpoints: [0, 0.9, 1],
54053
+ showHandle: true,
54054
+ handleBehavior: 'cycle',
54055
+ },
54056
+ });
54057
+ this.resolvedConfig().onRated?.();
54058
+ }
54059
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
54060
+ 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: `
54061
+ <ion-grid class="about-grid">
54062
+ <!-- HERO: logo + name + version -->
54063
+ <div class="about-hero">
54064
+ <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54065
+ <val-title
54066
+ [props]="{
54067
+ content: brandName(),
54068
+ size: 'xlarge',
54069
+ color: 'dark',
54070
+ bold: true,
54071
+ }"
54072
+ />
54073
+ <val-text
54074
+ [props]="{
54075
+ content: tt('tagline'),
54076
+ size: 'medium',
54077
+ color: 'dark',
54078
+ bold: false,
54079
+ }"
54080
+ />
54081
+ @if (version()) {
54082
+ <div
54083
+ class="version-row"
54084
+ (click)="copyVersion()"
54085
+ role="button"
54086
+ [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54087
+ >
54088
+ <val-pill
54089
+ [props]="{
54090
+ label: tt('versionLabel') + ' ' + version(),
54091
+ color: 'dark',
54092
+ weight: 'bold',
54093
+ size: 'small',
54094
+ }"
54095
+ />
54096
+ <ion-icon name="copy-outline" class="version-copy-icon" />
54097
+ </div>
54098
+ }
54099
+ </div>
54100
+
54101
+ <!-- DESCRIPTION -->
54102
+ <section class="about-section">
54103
+ <val-title [props]="sectionTitle('descTitle')" />
54104
+ <val-text
54105
+ [props]="{
54106
+ content: tt('descBody'),
54107
+ size: 'medium',
54108
+ color: 'dark',
54109
+ bold: false,
54110
+ }"
54111
+ />
54112
+ </section>
54113
+
54114
+ <!-- ACTIONS — rating built-in + acciones del consumer. -->
54115
+ @if (showRating() || actions().length > 0) {
54116
+ <section class="about-section about-section--reviews">
54117
+ <val-title [props]="sectionTitle('actionsTitle')" />
54118
+ <div class="about-actions">
54119
+ @if (showRating()) {
54120
+ <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54121
+ }
54122
+ @for (action of actions(); track action.label) {
54123
+ <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54124
+ }
54125
+ </div>
54126
+ </section>
54127
+ }
54128
+
54129
+ <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54130
+ @if (socialLinks().length > 0) {
54131
+ <section class="about-section">
54132
+ <val-title [props]="sectionTitle('socialTitle')" />
54133
+ <val-text
54134
+ [props]="{
54135
+ content: tt('socialDesc'),
54136
+ size: 'medium',
54137
+ color: 'dark',
54138
+ bold: false,
54139
+ }"
54140
+ />
54141
+ <div class="about-social">
54142
+ @for (s of socialLinks(); track s.url) {
54143
+ <ion-button
54144
+ fill="clear"
54145
+ size="small"
54146
+ class="social-btn"
54147
+ [attr.aria-label]="s.label"
54148
+ (click)="openUrl(s.url)"
54149
+ >
54150
+ <ion-icon [name]="s.icon" slot="icon-only" />
54151
+ </ion-button>
54152
+ }
54153
+ </div>
54154
+ </section>
54155
+ }
54156
+
54157
+ <!-- FOOTER -->
54158
+ <footer class="about-footer">
54159
+ <val-text
54160
+ [props]="{
54161
+ content: copyright(),
54162
+ size: 'small',
54163
+ color: 'dark',
54164
+ bold: false,
54165
+ }"
54166
+ />
54167
+ </footer>
54168
+ </ion-grid>
54169
+ `, 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"] }] }); }
54170
+ }
54171
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, decorators: [{
54172
+ type: Component,
54173
+ args: [{ selector: 'val-about-view', standalone: true, imports: [IonButton, IonGrid, IonIcon, PillComponent, TitleComponent, TextComponent, ActionCardComponent], template: `
54174
+ <ion-grid class="about-grid">
54175
+ <!-- HERO: logo + name + version -->
54176
+ <div class="about-hero">
54177
+ <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54178
+ <val-title
54179
+ [props]="{
54180
+ content: brandName(),
54181
+ size: 'xlarge',
54182
+ color: 'dark',
54183
+ bold: true,
54184
+ }"
54185
+ />
54186
+ <val-text
54187
+ [props]="{
54188
+ content: tt('tagline'),
54189
+ size: 'medium',
54190
+ color: 'dark',
54191
+ bold: false,
54192
+ }"
54193
+ />
54194
+ @if (version()) {
54195
+ <div
54196
+ class="version-row"
54197
+ (click)="copyVersion()"
54198
+ role="button"
54199
+ [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54200
+ >
54201
+ <val-pill
54202
+ [props]="{
54203
+ label: tt('versionLabel') + ' ' + version(),
54204
+ color: 'dark',
54205
+ weight: 'bold',
54206
+ size: 'small',
54207
+ }"
54208
+ />
54209
+ <ion-icon name="copy-outline" class="version-copy-icon" />
54210
+ </div>
54211
+ }
54212
+ </div>
54213
+
54214
+ <!-- DESCRIPTION -->
54215
+ <section class="about-section">
54216
+ <val-title [props]="sectionTitle('descTitle')" />
54217
+ <val-text
54218
+ [props]="{
54219
+ content: tt('descBody'),
54220
+ size: 'medium',
54221
+ color: 'dark',
54222
+ bold: false,
54223
+ }"
54224
+ />
54225
+ </section>
54226
+
54227
+ <!-- ACTIONS — rating built-in + acciones del consumer. -->
54228
+ @if (showRating() || actions().length > 0) {
54229
+ <section class="about-section about-section--reviews">
54230
+ <val-title [props]="sectionTitle('actionsTitle')" />
54231
+ <div class="about-actions">
54232
+ @if (showRating()) {
54233
+ <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54234
+ }
54235
+ @for (action of actions(); track action.label) {
54236
+ <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54237
+ }
54238
+ </div>
54239
+ </section>
54240
+ }
54241
+
54242
+ <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54243
+ @if (socialLinks().length > 0) {
54244
+ <section class="about-section">
54245
+ <val-title [props]="sectionTitle('socialTitle')" />
54246
+ <val-text
54247
+ [props]="{
54248
+ content: tt('socialDesc'),
54249
+ size: 'medium',
54250
+ color: 'dark',
54251
+ bold: false,
54252
+ }"
54253
+ />
54254
+ <div class="about-social">
54255
+ @for (s of socialLinks(); track s.url) {
54256
+ <ion-button
54257
+ fill="clear"
54258
+ size="small"
54259
+ class="social-btn"
54260
+ [attr.aria-label]="s.label"
54261
+ (click)="openUrl(s.url)"
54262
+ >
54263
+ <ion-icon [name]="s.icon" slot="icon-only" />
54264
+ </ion-button>
54265
+ }
54266
+ </div>
54267
+ </section>
54268
+ }
54269
+
54270
+ <!-- FOOTER -->
54271
+ <footer class="about-footer">
54272
+ <val-text
54273
+ [props]="{
54274
+ content: copyright(),
54275
+ size: 'small',
54276
+ color: 'dark',
54277
+ bold: false,
54278
+ }"
54279
+ />
54280
+ </footer>
54281
+ </ion-grid>
54282
+ `, 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"] }]
54283
+ }], ctorParameters: () => [], propDecorators: { config: [{
54284
+ type: Input
54285
+ }] } });
54286
+
54287
+ /**
54288
+ * Configuración acotada de `val-about-view` (object-first). La vista About es
54289
+ * app-specific por naturaleza (logo, nombre, versión, redes, acciones extra),
54290
+ * así que lo propio de cada app se parametriza como **DATA de config** — NO como
54291
+ * slots arbitrarios (ADR-021: una vista full-feature no abre slots; si falta
54292
+ * algo, se promueve un nuevo punto de extensión).
54293
+ *
54294
+ * Ejes de variación permitidos:
54295
+ * - **branding** — `brand.name` / `brand.logoSrc` (qué muestra el HERO).
54296
+ * - **versión** — `version` la pasa el consumer (la lib NO puede importar el
54297
+ * `version.ts` de la app). Si no se pasa, se oculta el pill de versión.
54298
+ * - **redes** — `socialLinks`; si está vacío se oculta la sección social.
54299
+ * - **rating** — `showRating` + `ratingEntityId` + `onRated`.
54300
+ * - **acciones extra** — `actions[]` genéricas que el consumer agrega junto a
54301
+ * la acción de rating built-in.
54302
+ * - **branding i18n** — `i18nNamespace` (default `'Settings.About'`).
54303
+ */
54304
+
54305
+ /**
54306
+ * Helper para montar la vista About (`val-about-view`) como ruta en una app del
54307
+ * factory. El `config` se pasa por route `data` (`aboutConfig`) y el componente
54308
+ * lo lee como fallback de su `@Input() config` (el `@Input` gana).
54309
+ *
54310
+ * @example
54311
+ * ```ts
54312
+ * // settings.routes.ts
54313
+ * import { APP_VERSION } from '../version';
54314
+ *
54315
+ * export const settingsRoutes: Routes = [
54316
+ * ...provideValtechAboutRoutes({ config: { version: APP_VERSION } }), // → /app/about
54317
+ * { path: 'preferences', loadComponent: () => ... },
54318
+ * ];
54319
+ *
54320
+ * // con redes y acciones extra:
54321
+ * ...provideValtechAboutRoutes({
54322
+ * config: {
54323
+ * version: APP_VERSION,
54324
+ * socialLinks: VALTECH_SOCIAL_LINKS.map(s => ({ icon: s.icon, url: s.url, label: s.name })),
54325
+ * actions: [{ label: 'Compartir', icon: 'share-social-outline', onClick: () => share() }],
54326
+ * },
54327
+ * }),
54328
+ * ```
54329
+ */
54330
+ function provideValtechAboutRoutes(opts) {
54331
+ return [
54332
+ {
54333
+ path: opts?.path ?? 'about',
54334
+ component: AboutViewComponent,
54335
+ data: { aboutConfig: opts?.config },
54336
+ },
54337
+ ];
54338
+ }
54339
+
52500
54340
  /** Built-in label sets for the three platform locales. */
52501
54341
  const CALLOUT_LABELS = {
52502
54342
  es: {
@@ -58626,263 +60466,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
58626
60466
  type: Output
58627
60467
  }] } });
58628
60468
 
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
-
58886
60469
  const DEFAULT_SPLASH_SCREEN_CONFIG = {
58887
60470
  fadeOutDuration: 300,
58888
60471
  };
@@ -62735,5 +64318,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
62735
64318
  * Generated bundle index. Do not edit.
62736
64319
  */
62737
64320
 
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 };
64321
+ 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 };
62739
64322
  //# sourceMappingURL=valtech-components.mjs.map