valtech-components 2.0.1009 → 2.0.1010

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 (40) 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/version.mjs +2 -2
  19. package/esm2022/public-api.mjs +7 -1
  20. package/fesm2022/valtech-components.mjs +1899 -327
  21. package/fesm2022/valtech-components.mjs.map +1 -1
  22. package/lib/components/molecules/date-input/date-input.component.d.ts +16 -0
  23. package/lib/components/organisms/about-view/about-view.component.d.ts +88 -0
  24. package/lib/components/organisms/about-view/about-view.i18n.d.ts +14 -0
  25. package/lib/components/organisms/about-view/about.routes.d.ts +31 -0
  26. package/lib/components/organisms/about-view/types.d.ts +76 -0
  27. package/lib/components/organisms/api-keys-modal/api-keys-modal.component.d.ts +44 -0
  28. package/lib/components/organisms/api-keys-modal/api-keys-modal.i18n.d.ts +6 -0
  29. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +47 -0
  30. package/lib/components/organisms/member-import-modal/member-import-modal.i18n.d.ts +6 -0
  31. package/lib/components/organisms/organization-view/organization-view.component.d.ts +6 -0
  32. package/lib/services/apikeys/api-keys.service.d.ts +25 -0
  33. package/lib/services/apikeys/types.d.ts +46 -0
  34. package/lib/services/auth/auth.service.d.ts +7 -1
  35. package/lib/services/auth/types.d.ts +23 -0
  36. package/lib/services/org/org.service.d.ts +6 -1
  37. package/lib/services/org/types.d.ts +30 -0
  38. package/lib/version.d.ts +1 -1
  39. package/package.json +1 -1
  40. 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.1010';
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: [{
@@ -41737,7 +41770,7 @@ const PROFILE_VIEW_I18N = {
41737
41770
  const HANDLE_PATTERN = /^[a-z_][a-z0-9_]*$/;
41738
41771
  const HANDLE_MIN = 3;
41739
41772
  const HANDLE_MAX = 20;
41740
- const DEFAULT_NAMESPACE$f = 'Settings.Profile';
41773
+ const DEFAULT_NAMESPACE$i = 'Settings.Profile';
41741
41774
  /**
41742
41775
  * `val-profile-view` — vista Perfil full-feature autocontenida (organism).
41743
41776
  *
@@ -41787,7 +41820,7 @@ class ProfileViewComponent {
41787
41820
  showPhone: merged.showPhone ?? true,
41788
41821
  showShareCta: merged.showShareCta ?? true,
41789
41822
  profileBaseUrl: merged.profileBaseUrl ?? '',
41790
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$f,
41823
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$i,
41791
41824
  onSaved: merged.onSaved,
41792
41825
  onAvatarUploaded: merged.onAvatarUploaded,
41793
41826
  };
@@ -42718,7 +42751,7 @@ const PREFERENCES_VIEW_I18N = {
42718
42751
  },
42719
42752
  };
42720
42753
 
42721
- const DEFAULT_NAMESPACE$e = 'Settings.General';
42754
+ const DEFAULT_NAMESPACE$h = 'Settings.General';
42722
42755
  const DEFAULT_LANGUAGES = ['es', 'en'];
42723
42756
  /**
42724
42757
  * `val-preferences-view` — vista de preferencias de UI cross-app (tema +
@@ -42755,7 +42788,7 @@ class PreferencesViewComponent {
42755
42788
  showLanguage: merged.showLanguage ?? true,
42756
42789
  showFontSize: merged.showFontSize ?? true,
42757
42790
  supportedLanguages: merged.supportedLanguages ?? DEFAULT_LANGUAGES,
42758
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$e,
42791
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$h,
42759
42792
  onThemeChanged: merged.onThemeChanged,
42760
42793
  onLanguageChanged: merged.onLanguageChanged,
42761
42794
  onFontSizeChanged: merged.onFontSizeChanged,
@@ -44511,7 +44544,7 @@ const SECURITY_VIEW_I18N = {
44511
44544
  };
44512
44545
 
44513
44546
  addIcons({ laptopOutline, phonePortraitOutline });
44514
- const DEFAULT_NAMESPACE$d = 'Settings.Security';
44547
+ const DEFAULT_NAMESPACE$g = 'Settings.Security';
44515
44548
  /**
44516
44549
  * `val-security-view` — vista Seguridad full-feature autocontenida (organism).
44517
44550
  *
@@ -44560,7 +44593,7 @@ class SecurityViewComponent {
44560
44593
  showChangeEmail: merged.showChangeEmail ?? true,
44561
44594
  showMfa: merged.showMfa ?? true,
44562
44595
  showPolicies: merged.showPolicies ?? true,
44563
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$d,
44596
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$g,
44564
44597
  handleDeepLinks: merged.handleDeepLinks ?? true,
44565
44598
  homeRoute: merged.homeRoute ?? '/app/home',
44566
44599
  onMfaCompleted: merged.onMfaCompleted,
@@ -45362,6 +45395,13 @@ class OrgService {
45362
45395
  inviteUser(orgId, req) {
45363
45396
  return this.http.post(`${this.baseUrl}/${orgId}/invite`, req);
45364
45397
  }
45398
+ /**
45399
+ * Crea/asigna miembros en masa (ADR-023). Resultado por-fila (partial success):
45400
+ * `created` (pre-aprovisionado), `assigned` (ya existía), `skipped`, `error`.
45401
+ */
45402
+ importMembers(orgId, req) {
45403
+ return this.http.post(`${this.baseUrl}/${orgId}/members/import`, req);
45404
+ }
45365
45405
  leaveOrg(orgId) {
45366
45406
  return this.http.post(`${this.baseUrl}/${orgId}/leave`, {});
45367
45407
  }
@@ -45466,7 +45506,7 @@ const CREATE_ORG_MODAL_I18N = {
45466
45506
  },
45467
45507
  };
45468
45508
 
45469
- const DEFAULT_NAMESPACE$c = 'CreateOrgModal';
45509
+ const DEFAULT_NAMESPACE$f = 'CreateOrgModal';
45470
45510
  /**
45471
45511
  * `val-create-org-modal` — modal de creación de organización (organism
45472
45512
  * compartido). Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por
@@ -45492,10 +45532,10 @@ class CreateOrgModalComponent {
45492
45532
  this.toast = inject(ToastService);
45493
45533
  this.errors = inject(ValtechErrorService);
45494
45534
  /** Namespace i18n con que la vista resuelve sus textos. */
45495
- this.i18nNamespace = DEFAULT_NAMESPACE$c;
45535
+ this.i18nNamespace = DEFAULT_NAMESPACE$f;
45496
45536
  this._busy = signal(false);
45497
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
45498
- this.i18n.registerContent(DEFAULT_NAMESPACE$c, CREATE_ORG_MODAL_I18N);
45537
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$f)) {
45538
+ this.i18n.registerContent(DEFAULT_NAMESPACE$f, CREATE_ORG_MODAL_I18N);
45499
45539
  }
45500
45540
  this.formMeta = signal(this.buildFormMeta());
45501
45541
  effect(() => {
@@ -45731,7 +45771,7 @@ const DELETE_ACCOUNT_MODAL_I18N = {
45731
45771
  },
45732
45772
  };
45733
45773
 
45734
- const DEFAULT_NAMESPACE$b = 'Settings.DeleteAccount';
45774
+ const DEFAULT_NAMESPACE$e = 'Settings.DeleteAccount';
45735
45775
  /**
45736
45776
  * `val-delete-account-modal` — modal de eliminación de cuenta (organism,
45737
45777
  * cuenta-específico). Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -45757,7 +45797,7 @@ class DeleteAccountModalComponent {
45757
45797
  this.i18n = inject(I18nService);
45758
45798
  this.toast = inject(ToastService);
45759
45799
  /** Namespace i18n con que la vista resuelve sus textos. */
45760
- this.i18nNamespace = DEFAULT_NAMESPACE$b;
45800
+ this.i18nNamespace = DEFAULT_NAMESPACE$e;
45761
45801
  this.hasPassword = signal(null);
45762
45802
  this.codeSent = signal(false);
45763
45803
  this.formState = signal(ComponentStates.ENABLED);
@@ -45824,8 +45864,8 @@ class DeleteAccountModalComponent {
45824
45864
  ],
45825
45865
  actions: this.submitBtn('delete-code-submit', 'confirmCta'),
45826
45866
  }));
45827
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$b)) {
45828
- this.i18n.registerContent(DEFAULT_NAMESPACE$b, DELETE_ACCOUNT_MODAL_I18N);
45867
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$e)) {
45868
+ this.i18n.registerContent(DEFAULT_NAMESPACE$e, DELETE_ACCOUNT_MODAL_I18N);
45829
45869
  }
45830
45870
  }
45831
45871
  ngOnInit() {
@@ -46026,7 +46066,7 @@ const ORG_INFO_SHEET_I18N = {
46026
46066
  },
46027
46067
  };
46028
46068
 
46029
- const DEFAULT_NAMESPACE$a = 'OrgInfoSheet';
46069
+ const DEFAULT_NAMESPACE$d = 'OrgInfoSheet';
46030
46070
  /**
46031
46071
  * `val-org-info-sheet` — sheet informativo "¿Qué son las organizaciones?".
46032
46072
  * Extraído del `OrgInfoSheetComponent` inline de `AccountPage` bajo el proceso de
@@ -46071,15 +46111,15 @@ class OrgInfoSheetComponent {
46071
46111
  text: this.closeProps.text || this.resolvedCloseLabel(),
46072
46112
  }));
46073
46113
  addIcons({ peopleOutline, swapHorizontalOutline, mailOutline });
46074
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
46075
- this.i18n.registerContent(DEFAULT_NAMESPACE$a, ORG_INFO_SHEET_I18N);
46114
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$d)) {
46115
+ this.i18n.registerContent(DEFAULT_NAMESPACE$d, ORG_INFO_SHEET_I18N);
46076
46116
  }
46077
46117
  }
46078
46118
  dismiss() {
46079
46119
  this._modalRef?.dismiss(null, 'cancel');
46080
46120
  }
46081
46121
  tt(key) {
46082
- return this.i18n.t(key, DEFAULT_NAMESPACE$a);
46122
+ return this.i18n.t(key, DEFAULT_NAMESPACE$d);
46083
46123
  }
46084
46124
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrgInfoSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
46085
46125
  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 +46286,7 @@ const SWITCH_ORG_MODAL_I18N = {
46246
46286
  },
46247
46287
  };
46248
46288
 
46249
- const DEFAULT_NAMESPACE$9 = 'Settings.SwitchOrg';
46289
+ const DEFAULT_NAMESPACE$c = 'Settings.SwitchOrg';
46250
46290
  addIcons({ businessOutline, checkmarkCircleOutline });
46251
46291
  /**
46252
46292
  * `val-switch-org-modal` — modal de cambio de organización activa (organism
@@ -46272,7 +46312,7 @@ class SwitchOrgModalComponent {
46272
46312
  this.orgSwitch = inject(OrgSwitchService);
46273
46313
  this.errors = inject(ValtechErrorService);
46274
46314
  /** Namespace i18n con que la vista resuelve sus textos. */
46275
- this.i18nNamespace = DEFAULT_NAMESPACE$9;
46315
+ this.i18nNamespace = DEFAULT_NAMESPACE$c;
46276
46316
  this.orgs = signal([]);
46277
46317
  this.loading = signal(false);
46278
46318
  this.query = signal('');
@@ -46288,8 +46328,8 @@ class SwitchOrgModalComponent {
46288
46328
  return list;
46289
46329
  return list.filter(o => o.name.toLowerCase().includes(q));
46290
46330
  });
46291
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
46292
- this.i18n.registerContent(DEFAULT_NAMESPACE$9, SWITCH_ORG_MODAL_I18N);
46331
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$c)) {
46332
+ this.i18n.registerContent(DEFAULT_NAMESPACE$c, SWITCH_ORG_MODAL_I18N);
46293
46333
  }
46294
46334
  this.loadOrgs();
46295
46335
  }
@@ -46607,7 +46647,7 @@ const ACCOUNT_VIEW_I18N = {
46607
46647
  };
46608
46648
 
46609
46649
  addIcons({ peopleOutline, businessOutline, chevronForwardOutline, swapHorizontalOutline, mailOutline });
46610
- const DEFAULT_NAMESPACE$8 = 'Settings.Account';
46650
+ const DEFAULT_NAMESPACE$b = 'Settings.Account';
46611
46651
  const DEFAULT_MANAGE_ORG_ROUTE = '/app/settings/organization';
46612
46652
  /**
46613
46653
  * `val-account-view` — vista Cuenta full-feature autocontenida (organism).
@@ -46648,7 +46688,7 @@ class AccountViewComponent {
46648
46688
  showNewOrgCta: merged.showNewOrgCta ?? true,
46649
46689
  showLogout: merged.showLogout ?? true,
46650
46690
  showDeleteAccount: merged.showDeleteAccount ?? true,
46651
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$8,
46691
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$b,
46652
46692
  manageOrgRoute: merged.manageOrgRoute ?? DEFAULT_MANAGE_ORG_ROUTE,
46653
46693
  onOrgSwitched: merged.onOrgSwitched,
46654
46694
  onOrgCreated: merged.onOrgCreated,
@@ -47373,7 +47413,7 @@ const EDIT_ORG_MODAL_I18N = {
47373
47413
  },
47374
47414
  };
47375
47415
 
47376
- const DEFAULT_NAMESPACE$7 = 'EditOrgModal';
47416
+ const DEFAULT_NAMESPACE$a = 'EditOrgModal';
47377
47417
  /**
47378
47418
  * `val-edit-org-modal` — modal de edición de organización (nombre + descripción).
47379
47419
  * Promovido desde `showcase` bajo el proceso de ADR-021. Reusado por la vista de
@@ -47400,10 +47440,10 @@ class EditOrgModalComponent {
47400
47440
  /** Organización a editar (pasada por `componentProps`). */
47401
47441
  this.org = null;
47402
47442
  /** Namespace i18n con que la vista resuelve sus textos. */
47403
- this.i18nNamespace = DEFAULT_NAMESPACE$7;
47443
+ this.i18nNamespace = DEFAULT_NAMESPACE$a;
47404
47444
  this.saving = signal(false);
47405
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
47406
- this.i18n.registerContent(DEFAULT_NAMESPACE$7, EDIT_ORG_MODAL_I18N);
47445
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$a)) {
47446
+ this.i18n.registerContent(DEFAULT_NAMESPACE$a, EDIT_ORG_MODAL_I18N);
47407
47447
  }
47408
47448
  this.formMeta = signal(this.buildFormMeta());
47409
47449
  effect(() => {
@@ -47641,7 +47681,7 @@ const INVITE_MEMBER_MODAL_I18N = {
47641
47681
  },
47642
47682
  };
47643
47683
 
47644
- const DEFAULT_NAMESPACE$6 = 'InviteModal';
47684
+ const DEFAULT_NAMESPACE$9 = 'InviteModal';
47645
47685
  addIcons({ personOutline, mailOutline, closeOutline, checkmarkCircleOutline });
47646
47686
  /**
47647
47687
  * `val-invite-member-modal` — invitación de miembros a una organización.
@@ -47672,7 +47712,7 @@ class InviteMemberModalComponent {
47672
47712
  /** Ids de miembros ya existentes (excluidos de los resultados). */
47673
47713
  this.existingMemberIds = [];
47674
47714
  /** Namespace i18n con que la vista resuelve sus textos. */
47675
- this.i18nNamespace = DEFAULT_NAMESPACE$6;
47715
+ this.i18nNamespace = DEFAULT_NAMESPACE$9;
47676
47716
  this.query = signal('');
47677
47717
  this.searching = signal(false);
47678
47718
  this.searchResults = signal([]);
@@ -47719,8 +47759,8 @@ class InviteMemberModalComponent {
47719
47759
  state: this.sending() ? ComponentStates.WORKING : ComponentStates.ENABLED,
47720
47760
  }));
47721
47761
  this.searchSubject = new Subject();
47722
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
47723
- this.i18n.registerContent(DEFAULT_NAMESPACE$6, INVITE_MEMBER_MODAL_I18N);
47762
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$9)) {
47763
+ this.i18n.registerContent(DEFAULT_NAMESPACE$9, INVITE_MEMBER_MODAL_I18N);
47724
47764
  }
47725
47765
  this.loadRoles();
47726
47766
  this.roleSub = this.roleControl.valueChanges.subscribe(v => {
@@ -48209,7 +48249,7 @@ const MEMBER_DETAIL_MODAL_I18N = {
48209
48249
  },
48210
48250
  };
48211
48251
 
48212
- const DEFAULT_NAMESPACE$5 = 'Settings.MemberDetail';
48252
+ const DEFAULT_NAMESPACE$8 = 'Settings.MemberDetail';
48213
48253
  /**
48214
48254
  * `val-member-detail-modal` — detalle de miembro de organización: perfil, rol y
48215
48255
  * permisos. Promovido desde `showcase` bajo el proceso de ADR-021.
@@ -48246,7 +48286,7 @@ class MemberDetailModalComponent {
48246
48286
  { id: 'admin', name: 'admin' },
48247
48287
  ];
48248
48288
  /** Namespace i18n con que la vista resuelve sus textos. */
48249
- this.i18nNamespace = DEFAULT_NAMESPACE$5;
48289
+ this.i18nNamespace = DEFAULT_NAMESPACE$8;
48250
48290
  this.working = signal(false);
48251
48291
  this.currentRole = signal('');
48252
48292
  this.roleControl = new FormControl('');
@@ -48286,8 +48326,8 @@ class MemberDetailModalComponent {
48286
48326
  type: 'button',
48287
48327
  state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
48288
48328
  }));
48289
- if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
48290
- this.i18n.registerContent(DEFAULT_NAMESPACE$5, MEMBER_DETAIL_MODAL_I18N);
48329
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$8)) {
48330
+ this.i18n.registerContent(DEFAULT_NAMESPACE$8, MEMBER_DETAIL_MODAL_I18N);
48291
48331
  }
48292
48332
  }
48293
48333
  ngOnInit() {
@@ -48624,7 +48664,7 @@ const TRANSFER_OWNERSHIP_MODAL_I18N = {
48624
48664
  },
48625
48665
  };
48626
48666
 
48627
- const DEFAULT_NAMESPACE$4 = 'Settings.TransferModal';
48667
+ const DEFAULT_NAMESPACE$7 = 'Settings.TransferModal';
48628
48668
  /**
48629
48669
  * `val-transfer-ownership-modal` — selector de miembro para transferir la
48630
48670
  * propiedad de la organización. Promovido desde `showcase` bajo el proceso de
@@ -48648,7 +48688,7 @@ class TransferOwnershipModalComponent {
48648
48688
  /** Lista de miembros transferibles (todos excepto el owner actual). */
48649
48689
  this.members = [];
48650
48690
  /** Namespace i18n con que la vista resuelve sus textos. */
48651
- this.i18nNamespace = DEFAULT_NAMESPACE$4;
48691
+ this.i18nNamespace = DEFAULT_NAMESPACE$7;
48652
48692
  this.query = signal('');
48653
48693
  this.selectedId = signal('');
48654
48694
  this.filtered = computed(() => {
@@ -48660,8 +48700,8 @@ class TransferOwnershipModalComponent {
48660
48700
  m.userId.toLowerCase().includes(q));
48661
48701
  });
48662
48702
  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);
48703
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$7)) {
48704
+ this.i18n.registerContent(DEFAULT_NAMESPACE$7, TRANSFER_OWNERSHIP_MODAL_I18N);
48665
48705
  }
48666
48706
  }
48667
48707
  ngOnInit() {
@@ -48846,6 +48886,849 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
48846
48886
  type: Input
48847
48887
  }] } });
48848
48888
 
48889
+ /**
48890
+ * Defaults i18n (es/en) embebidos en `val-member-import-modal` (ADR-023).
48891
+ * Auto-registrados si el consumer no proveyó el namespace `Settings.ImportModal`.
48892
+ */
48893
+ const MEMBER_IMPORT_MODAL_I18N = {
48894
+ es: {
48895
+ title: 'Importar miembros',
48896
+ hint: 'Pega un CSV con columnas: email, nombre, rol (una fila por persona).',
48897
+ csvPlaceholder: 'email,nombre,rol\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48898
+ rolesAvailable: 'Roles disponibles:',
48899
+ onConflictLabel: 'Si el email ya existe',
48900
+ conflictAssign: 'Asignar el rol',
48901
+ conflictSkip: 'Omitir',
48902
+ conflictError: 'Marcar error',
48903
+ sendActivation: 'Enviar email de activación a las cuentas nuevas',
48904
+ submit: 'Importar',
48905
+ submitting: 'Importando...',
48906
+ parseError: 'No se pudo interpretar el CSV. Revisa el formato.',
48907
+ noRows: 'Pega al menos una fila válida (email y rol).',
48908
+ summary: 'Resumen',
48909
+ created: 'creados',
48910
+ assigned: 'asignados',
48911
+ skipped: 'omitidos',
48912
+ failed: 'con error',
48913
+ colEmail: 'Email',
48914
+ colStatus: 'Estado',
48915
+ colDetail: 'Detalle',
48916
+ activationSent: 'activación enviada',
48917
+ activationPending: 'activación pendiente',
48918
+ again: 'Importar otro lote',
48919
+ close: 'Cerrar',
48920
+ genericError: 'No se pudo completar la importación. Intenta de nuevo.',
48921
+ },
48922
+ en: {
48923
+ title: 'Import members',
48924
+ hint: 'Paste a CSV with columns: email, name, role (one row per person).',
48925
+ csvPlaceholder: 'email,name,role\nana@x.com,Ana,editor\nbeto@x.com,Beto,viewer',
48926
+ rolesAvailable: 'Available roles:',
48927
+ onConflictLabel: 'If the email already exists',
48928
+ conflictAssign: 'Assign the role',
48929
+ conflictSkip: 'Skip',
48930
+ conflictError: 'Mark as error',
48931
+ sendActivation: 'Send activation email to new accounts',
48932
+ submit: 'Import',
48933
+ submitting: 'Importing...',
48934
+ parseError: "Couldn't parse the CSV. Check the format.",
48935
+ noRows: 'Paste at least one valid row (email and role).',
48936
+ summary: 'Summary',
48937
+ created: 'created',
48938
+ assigned: 'assigned',
48939
+ skipped: 'skipped',
48940
+ failed: 'failed',
48941
+ colEmail: 'Email',
48942
+ colStatus: 'Status',
48943
+ colDetail: 'Detail',
48944
+ activationSent: 'activation sent',
48945
+ activationPending: 'activation pending',
48946
+ again: 'Import another batch',
48947
+ close: 'Close',
48948
+ genericError: "Couldn't complete the import. Try again.",
48949
+ },
48950
+ };
48951
+
48952
+ const DEFAULT_NAMESPACE$6 = 'Settings.ImportModal';
48953
+ /**
48954
+ * `val-member-import-modal` — panel de carga masiva de miembros (ADR-023 fase 6).
48955
+ *
48956
+ * El admin pega un CSV (`email,nombre,rol`); el modal lo parsea a filas y llama
48957
+ * `OrgService.importMembers`. Muestra el resultado por-fila (partial success):
48958
+ * `created` / `assigned` / `skipped` / `error`. Header canónico (Regla #5).
48959
+ *
48960
+ * Auto-registra defaults i18n (es/en) en `Settings.ImportModal`.
48961
+ */
48962
+ class MemberImportModalComponent {
48963
+ constructor() {
48964
+ this.i18n = inject(I18nService);
48965
+ this.orgService = inject(OrgService);
48966
+ /** Org destino del import. */
48967
+ this.orgId = '';
48968
+ /** Nombres de roles disponibles (para el hint). Opcional. */
48969
+ this.availableRoles = [];
48970
+ /** Namespace i18n. */
48971
+ this.i18nNamespace = DEFAULT_NAMESPACE$6;
48972
+ this.csv = '';
48973
+ this.onConflict = 'assignRole';
48974
+ this.sendActivation = true;
48975
+ this.submitting = signal(false);
48976
+ this.errorMsg = signal('');
48977
+ this.results = signal(null);
48978
+ this.rolesHint = computed(() => this.availableRoles.length ? `${this.t('rolesAvailable')} ${this.availableRoles.join(', ')}` : '');
48979
+ this.summaryLabel = computed(() => {
48980
+ const s = this.results()?.summary;
48981
+ if (!s)
48982
+ return this.t('summary');
48983
+ return `${this.t('summary')}: ${s.created} ${this.t('created')} · ${s.assigned} ${this.t('assigned')} · ${s.skipped} ${this.t('skipped')} · ${s.failed} ${this.t('failed')}`;
48984
+ });
48985
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$6)) {
48986
+ this.i18n.registerContent(DEFAULT_NAMESPACE$6, MEMBER_IMPORT_MODAL_I18N);
48987
+ }
48988
+ }
48989
+ ngOnInit() {
48990
+ if (!this.i18n.hasNamespace(this.i18nNamespace)) {
48991
+ this.i18n.registerContent(this.i18nNamespace, MEMBER_IMPORT_MODAL_I18N);
48992
+ }
48993
+ }
48994
+ /** Parsea el CSV pegado a filas {email,name,roleName}. Tolera header. */
48995
+ parseCsv() {
48996
+ const rows = [];
48997
+ for (const raw of this.csv.split(/\r?\n/)) {
48998
+ const line = raw.trim();
48999
+ if (!line)
49000
+ continue;
49001
+ const cols = line.split(',').map(c => c.trim());
49002
+ const email = (cols[0] ?? '').toLowerCase();
49003
+ // Saltar header (primera col "email") o líneas sin @.
49004
+ if (!email || !email.includes('@'))
49005
+ continue;
49006
+ rows.push({ email, name: cols[1] ?? '', roleName: cols[2] ?? '' });
49007
+ }
49008
+ return rows;
49009
+ }
49010
+ submit() {
49011
+ this.errorMsg.set('');
49012
+ const users = this.parseCsv();
49013
+ if (users.length === 0) {
49014
+ this.errorMsg.set(this.t('noRows'));
49015
+ return;
49016
+ }
49017
+ if (users.some(u => !u.roleName)) {
49018
+ this.errorMsg.set(this.t('parseError'));
49019
+ return;
49020
+ }
49021
+ this.submitting.set(true);
49022
+ this.orgService
49023
+ .importMembers(this.orgId, {
49024
+ users,
49025
+ onConflict: this.onConflict,
49026
+ sendActivation: this.sendActivation,
49027
+ })
49028
+ .subscribe({
49029
+ next: res => {
49030
+ this.submitting.set(false);
49031
+ this.results.set(res);
49032
+ // Refrescar lista del consumer si hubo altas/asignaciones efectivas.
49033
+ if (res.summary.created + res.summary.assigned > 0) {
49034
+ this.onSuccess?.();
49035
+ }
49036
+ },
49037
+ error: err => {
49038
+ this.submitting.set(false);
49039
+ this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49040
+ },
49041
+ });
49042
+ }
49043
+ statusLabel(r) {
49044
+ const base = this.t(r.status === 'error' ? 'failed' : r.status);
49045
+ if (r.status === 'created' && r.activation) {
49046
+ return `${base} · ${this.t(r.activation === 'sent' ? 'activationSent' : 'activationPending')}`;
49047
+ }
49048
+ return base;
49049
+ }
49050
+ reset() {
49051
+ this.csv = '';
49052
+ this.errorMsg.set('');
49053
+ this.results.set(null);
49054
+ }
49055
+ dismiss() {
49056
+ this._modalRef?.dismiss(null, 'cancel');
49057
+ }
49058
+ t(key) {
49059
+ return this.i18n.t(key, this.i18nNamespace);
49060
+ }
49061
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49062
+ 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: `
49063
+ <ion-header>
49064
+ <ion-toolbar>
49065
+ <ion-buttons slot="end">
49066
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49067
+ <strong>{{ t('close') }}</strong>
49068
+ </ion-button>
49069
+ </ion-buttons>
49070
+ </ion-toolbar>
49071
+ </ion-header>
49072
+
49073
+ <ion-content class="ion-padding">
49074
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49075
+
49076
+ @if (!results()) {
49077
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49078
+
49079
+ @if (rolesHint()) {
49080
+ <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49081
+ }
49082
+
49083
+ <ion-textarea
49084
+ class="import-csv"
49085
+ [autoGrow]="true"
49086
+ [rows]="6"
49087
+ [placeholder]="t('csvPlaceholder')"
49088
+ [(ngModel)]="csv"
49089
+ />
49090
+
49091
+ <ion-select
49092
+ class="import-conflict"
49093
+ label="{{ t('onConflictLabel') }}"
49094
+ labelPlacement="stacked"
49095
+ interface="popover"
49096
+ [(ngModel)]="onConflict"
49097
+ >
49098
+ <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49099
+ <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49100
+ <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49101
+ </ion-select>
49102
+
49103
+ <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49104
+ {{ t('sendActivation') }}
49105
+ </ion-checkbox>
49106
+
49107
+ @if (errorMsg()) {
49108
+ <p class="import-error">{{ errorMsg() }}</p>
49109
+ }
49110
+
49111
+ <div class="import-actions">
49112
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49113
+ @if (submitting()) {
49114
+ <ion-spinner name="dots" />
49115
+ &nbsp;{{ t('submitting') }}
49116
+ } @else {
49117
+ {{ t('submit') }}
49118
+ }
49119
+ </ion-button>
49120
+ </div>
49121
+ } @else {
49122
+ <!-- Resultados -->
49123
+ <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49124
+ <div class="import-results">
49125
+ @for (r of results()!.results; track r.email) {
49126
+ <div class="import-row import-row--{{ r.status }}">
49127
+ <span class="import-row__email">{{ r.email }}</span>
49128
+ <span class="import-row__status">{{ statusLabel(r) }}</span>
49129
+ @if (r.reason) {
49130
+ <span class="import-row__reason">{{ r.reason }}</span>
49131
+ }
49132
+ </div>
49133
+ }
49134
+ </div>
49135
+ <div class="import-actions">
49136
+ <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49137
+ {{ t('again') }}
49138
+ </ion-button>
49139
+ </div>
49140
+ }
49141
+ </ion-content>
49142
+ `, 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"] }] }); }
49143
+ }
49144
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MemberImportModalComponent, decorators: [{
49145
+ type: Component,
49146
+ args: [{ selector: 'val-member-import-modal', standalone: true, imports: [
49147
+ FormsModule,
49148
+ IonHeader,
49149
+ IonToolbar,
49150
+ IonButtons,
49151
+ IonButton,
49152
+ IonContent,
49153
+ IonTextarea,
49154
+ IonSelect,
49155
+ IonSelectOption,
49156
+ IonCheckbox,
49157
+ IonSpinner,
49158
+ DisplayComponent,
49159
+ TextComponent,
49160
+ TitleComponent,
49161
+ ], template: `
49162
+ <ion-header>
49163
+ <ion-toolbar>
49164
+ <ion-buttons slot="end">
49165
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49166
+ <strong>{{ t('close') }}</strong>
49167
+ </ion-button>
49168
+ </ion-buttons>
49169
+ </ion-toolbar>
49170
+ </ion-header>
49171
+
49172
+ <ion-content class="ion-padding">
49173
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49174
+
49175
+ @if (!results()) {
49176
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49177
+
49178
+ @if (rolesHint()) {
49179
+ <val-text [props]="{ content: rolesHint(), size: 'small', color: 'medium', bold: false }" />
49180
+ }
49181
+
49182
+ <ion-textarea
49183
+ class="import-csv"
49184
+ [autoGrow]="true"
49185
+ [rows]="6"
49186
+ [placeholder]="t('csvPlaceholder')"
49187
+ [(ngModel)]="csv"
49188
+ />
49189
+
49190
+ <ion-select
49191
+ class="import-conflict"
49192
+ label="{{ t('onConflictLabel') }}"
49193
+ labelPlacement="stacked"
49194
+ interface="popover"
49195
+ [(ngModel)]="onConflict"
49196
+ >
49197
+ <ion-select-option value="assignRole">{{ t('conflictAssign') }}</ion-select-option>
49198
+ <ion-select-option value="skip">{{ t('conflictSkip') }}</ion-select-option>
49199
+ <ion-select-option value="error">{{ t('conflictError') }}</ion-select-option>
49200
+ </ion-select>
49201
+
49202
+ <ion-checkbox class="import-activation" labelPlacement="end" [(ngModel)]="sendActivation">
49203
+ {{ t('sendActivation') }}
49204
+ </ion-checkbox>
49205
+
49206
+ @if (errorMsg()) {
49207
+ <p class="import-error">{{ errorMsg() }}</p>
49208
+ }
49209
+
49210
+ <div class="import-actions">
49211
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49212
+ @if (submitting()) {
49213
+ <ion-spinner name="dots" />
49214
+ &nbsp;{{ t('submitting') }}
49215
+ } @else {
49216
+ {{ t('submit') }}
49217
+ }
49218
+ </ion-button>
49219
+ </div>
49220
+ } @else {
49221
+ <!-- Resultados -->
49222
+ <val-title [props]="{ content: summaryLabel(), size: 'large', color: '', bold: true }" />
49223
+ <div class="import-results">
49224
+ @for (r of results()!.results; track r.email) {
49225
+ <div class="import-row import-row--{{ r.status }}">
49226
+ <span class="import-row__email">{{ r.email }}</span>
49227
+ <span class="import-row__status">{{ statusLabel(r) }}</span>
49228
+ @if (r.reason) {
49229
+ <span class="import-row__reason">{{ r.reason }}</span>
49230
+ }
49231
+ </div>
49232
+ }
49233
+ </div>
49234
+ <div class="import-actions">
49235
+ <ion-button expand="block" fill="outline" color="dark" (click)="reset()">
49236
+ {{ t('again') }}
49237
+ </ion-button>
49238
+ </div>
49239
+ }
49240
+ </ion-content>
49241
+ `, 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"] }]
49242
+ }], ctorParameters: () => [], propDecorators: { orgId: [{
49243
+ type: Input
49244
+ }], availableRoles: [{
49245
+ type: Input
49246
+ }], onSuccess: [{
49247
+ type: Input
49248
+ }], _modalRef: [{
49249
+ type: Input
49250
+ }], i18nNamespace: [{
49251
+ type: Input
49252
+ }] } });
49253
+
49254
+ /**
49255
+ * Cliente HTTP de Client API Keys org-scopeadas (ADR-023). La org activa se
49256
+ * resuelve en backend desde el JWT, así que el path no la incluye.
49257
+ */
49258
+ class ApiKeyService {
49259
+ constructor(config, http) {
49260
+ this.config = config;
49261
+ this.http = http;
49262
+ }
49263
+ get baseUrl() {
49264
+ return `${this.config.apiUrl}/org/api-keys`;
49265
+ }
49266
+ /** Lista las API keys de la org activa. */
49267
+ list() {
49268
+ return this.http.get(`${this.baseUrl}/`).pipe(map$1(r => r.keys ?? []));
49269
+ }
49270
+ /** Crea una API key. La respuesta incluye el secreto (se muestra UNA vez). */
49271
+ create(req) {
49272
+ return this.http.post(`${this.baseUrl}/`, req);
49273
+ }
49274
+ /** Revoca (elimina) una API key por id. */
49275
+ revoke(keyId) {
49276
+ return this.http.delete(`${this.baseUrl}/${keyId}`);
49277
+ }
49278
+ /** Catálogo de permisos asignables a una key. */
49279
+ availablePermissions() {
49280
+ return this.http
49281
+ .get(`${this.baseUrl}/permissions`)
49282
+ .pipe(map$1(r => r.permissions ?? []));
49283
+ }
49284
+ 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 }); }
49285
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, providedIn: 'root' }); }
49286
+ }
49287
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeyService, decorators: [{
49288
+ type: Injectable,
49289
+ args: [{ providedIn: 'root' }]
49290
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
49291
+ type: Inject,
49292
+ args: [VALTECH_AUTH_CONFIG]
49293
+ }] }, { type: i1$3.HttpClient }] });
49294
+
49295
+ /**
49296
+ * Defaults i18n (es/en) embebidos en `val-api-keys-modal` (ADR-023).
49297
+ * Auto-registrados si el consumer no proveyó el namespace `Settings.ApiKeysModal`.
49298
+ */
49299
+ const API_KEYS_MODAL_I18N = {
49300
+ es: {
49301
+ title: 'Client API Keys',
49302
+ hint: 'Llaves M2M org-scopeadas para integraciones (p. ej. importar miembros).',
49303
+ empty: 'Aún no hay API keys en esta organización.',
49304
+ newKey: 'Nueva API key',
49305
+ nameLabel: 'Nombre',
49306
+ namePlaceholder: 'Ej: Integración RRHH',
49307
+ permissionsLabel: 'Permisos',
49308
+ expiresLabel: 'Expira en (días, opcional)',
49309
+ create: 'Crear key',
49310
+ creating: 'Creando...',
49311
+ cancel: 'Cancelar',
49312
+ revoke: 'Revocar',
49313
+ revokeConfirm: '¿Revocar esta API key? Las integraciones que la usen dejarán de funcionar.',
49314
+ secretTitle: 'Copia tu API key ahora',
49315
+ secretWarning: 'Este secreto se muestra una sola vez. Guárdalo en un lugar seguro.',
49316
+ copy: 'Copiar',
49317
+ copied: 'Copiada',
49318
+ done: 'Listo',
49319
+ statusActive: 'activa',
49320
+ lastUsed: 'Último uso',
49321
+ never: 'nunca',
49322
+ permsCount: 'permisos',
49323
+ noName: 'El nombre es obligatorio.',
49324
+ noPerms: 'Selecciona al menos un permiso.',
49325
+ close: 'Cerrar',
49326
+ genericError: 'No se pudo completar la operación. Intenta de nuevo.',
49327
+ },
49328
+ en: {
49329
+ title: 'Client API Keys',
49330
+ hint: 'Org-scoped M2M keys for integrations (e.g. member import).',
49331
+ empty: 'No API keys in this organization yet.',
49332
+ newKey: 'New API key',
49333
+ nameLabel: 'Name',
49334
+ namePlaceholder: 'E.g: HR integration',
49335
+ permissionsLabel: 'Permissions',
49336
+ expiresLabel: 'Expires in (days, optional)',
49337
+ create: 'Create key',
49338
+ creating: 'Creating...',
49339
+ cancel: 'Cancel',
49340
+ revoke: 'Revoke',
49341
+ revokeConfirm: 'Revoke this API key? Integrations using it will stop working.',
49342
+ secretTitle: 'Copy your API key now',
49343
+ secretWarning: 'This secret is shown only once. Store it somewhere safe.',
49344
+ copy: 'Copy',
49345
+ copied: 'Copied',
49346
+ done: 'Done',
49347
+ statusActive: 'active',
49348
+ lastUsed: 'Last used',
49349
+ never: 'never',
49350
+ permsCount: 'permissions',
49351
+ noName: 'Name is required.',
49352
+ noPerms: 'Select at least one permission.',
49353
+ close: 'Close',
49354
+ genericError: "Couldn't complete the operation. Try again.",
49355
+ },
49356
+ };
49357
+
49358
+ const DEFAULT_NAMESPACE$5 = 'Settings.ApiKeysModal';
49359
+ /**
49360
+ * `val-api-keys-modal` — gestión de Client API Keys org-scopeadas (ADR-023 fase 6).
49361
+ *
49362
+ * Lista las keys de la org, permite crear (mostrando el secreto UNA vez con copia)
49363
+ * y revocar. Pensado para habilitar integraciones M2M del import de miembros, pero
49364
+ * sirve a cualquier permiso del catálogo. Header canónico (Regla #5).
49365
+ */
49366
+ class ApiKeysModalComponent {
49367
+ constructor() {
49368
+ this.i18n = inject(I18nService);
49369
+ this.apiKeys = inject(ApiKeyService);
49370
+ /** Namespace i18n. */
49371
+ this.i18nNamespace = DEFAULT_NAMESPACE$5;
49372
+ this.keys = signal([]);
49373
+ this.availablePermissions = signal([]);
49374
+ this.loading = signal(true);
49375
+ this.showCreate = signal(false);
49376
+ this.submitting = signal(false);
49377
+ this.errorMsg = signal('');
49378
+ this.secret = signal(null);
49379
+ this.copied = signal(false);
49380
+ this.name = '';
49381
+ this.expiresInDays = null;
49382
+ this.selectedPerms = signal(new Set());
49383
+ if (!this.i18n.hasNamespace(DEFAULT_NAMESPACE$5)) {
49384
+ this.i18n.registerContent(DEFAULT_NAMESPACE$5, API_KEYS_MODAL_I18N);
49385
+ }
49386
+ }
49387
+ ngOnInit() {
49388
+ if (!this.i18n.hasNamespace(this.i18nNamespace)) {
49389
+ this.i18n.registerContent(this.i18nNamespace, API_KEYS_MODAL_I18N);
49390
+ }
49391
+ this.refresh();
49392
+ this.apiKeys.availablePermissions().subscribe({
49393
+ next: perms => this.availablePermissions.set(perms),
49394
+ error: () => { },
49395
+ });
49396
+ }
49397
+ refresh() {
49398
+ this.loading.set(true);
49399
+ this.apiKeys.list().subscribe({
49400
+ next: keys => {
49401
+ this.keys.set(keys);
49402
+ this.loading.set(false);
49403
+ },
49404
+ error: () => this.loading.set(false),
49405
+ });
49406
+ }
49407
+ openCreate() {
49408
+ this.errorMsg.set('');
49409
+ this.name = '';
49410
+ this.expiresInDays = null;
49411
+ this.selectedPerms.set(new Set());
49412
+ this.showCreate.set(true);
49413
+ }
49414
+ togglePerm(perm, ev) {
49415
+ const checked = ev.detail?.checked ?? false;
49416
+ const next = new Set(this.selectedPerms());
49417
+ if (checked) {
49418
+ next.add(perm);
49419
+ }
49420
+ else {
49421
+ next.delete(perm);
49422
+ }
49423
+ this.selectedPerms.set(next);
49424
+ }
49425
+ submit() {
49426
+ this.errorMsg.set('');
49427
+ if (!this.name.trim()) {
49428
+ this.errorMsg.set(this.t('noName'));
49429
+ return;
49430
+ }
49431
+ const permissions = Array.from(this.selectedPerms());
49432
+ if (permissions.length === 0) {
49433
+ this.errorMsg.set(this.t('noPerms'));
49434
+ return;
49435
+ }
49436
+ this.submitting.set(true);
49437
+ this.apiKeys
49438
+ .create({
49439
+ name: this.name.trim(),
49440
+ permissions,
49441
+ expiresInDays: this.expiresInDays && this.expiresInDays > 0 ? this.expiresInDays : undefined,
49442
+ })
49443
+ .subscribe({
49444
+ next: res => {
49445
+ this.submitting.set(false);
49446
+ this.showCreate.set(false);
49447
+ this.secret.set(res.key);
49448
+ },
49449
+ error: err => {
49450
+ this.submitting.set(false);
49451
+ this.errorMsg.set(err?.error?.message ?? this.t('genericError'));
49452
+ },
49453
+ });
49454
+ }
49455
+ revoke(key) {
49456
+ if (!confirm(this.t('revokeConfirm')))
49457
+ return;
49458
+ this.apiKeys.revoke(key.keyId).subscribe({
49459
+ next: () => this.refresh(),
49460
+ error: err => this.errorMsg.set(err?.error?.message ?? this.t('genericError')),
49461
+ });
49462
+ }
49463
+ async copy(secret) {
49464
+ try {
49465
+ await navigator.clipboard.writeText(secret);
49466
+ this.copied.set(true);
49467
+ }
49468
+ catch {
49469
+ // clipboard no disponible — el user puede seleccionar el texto manualmente
49470
+ }
49471
+ }
49472
+ finishSecret() {
49473
+ this.secret.set(null);
49474
+ this.copied.set(false);
49475
+ this.refresh();
49476
+ }
49477
+ dismiss() {
49478
+ this._modalRef?.dismiss(null, 'cancel');
49479
+ }
49480
+ t(key) {
49481
+ return this.i18n.t(key, this.i18nNamespace);
49482
+ }
49483
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
49484
+ 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: `
49485
+ <ion-header>
49486
+ <ion-toolbar>
49487
+ <ion-buttons slot="end">
49488
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49489
+ <strong>{{ t('close') }}</strong>
49490
+ </ion-button>
49491
+ </ion-buttons>
49492
+ </ion-toolbar>
49493
+ </ion-header>
49494
+
49495
+ <ion-content class="ion-padding">
49496
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49497
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49498
+
49499
+ @if (secret(); as s) {
49500
+ <!-- Secreto recién creado: mostrar UNA vez -->
49501
+ <div class="apikey-secret">
49502
+ <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49503
+ <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49504
+ <div class="apikey-secret__box">
49505
+ <code>{{ s.secret }}</code>
49506
+ </div>
49507
+ <div class="apikey-actions">
49508
+ <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49509
+ {{ copied() ? t('copied') : t('copy') }}
49510
+ </ion-button>
49511
+ <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49512
+ {{ t('done') }}
49513
+ </ion-button>
49514
+ </div>
49515
+ </div>
49516
+ } @else {
49517
+ @if (loading()) {
49518
+ <ion-spinner name="dots" />
49519
+ } @else {
49520
+ <!-- Lista -->
49521
+ @if (keys().length === 0) {
49522
+ <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49523
+ } @else {
49524
+ <div class="apikey-list">
49525
+ @for (k of keys(); track k.keyId) {
49526
+ <div class="apikey-row">
49527
+ <div class="apikey-row__main">
49528
+ <span class="apikey-row__name">{{ k.name }}</span>
49529
+ <span class="apikey-row__meta">
49530
+ {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49531
+ {{ k.lastUsedAt || t('never') }}
49532
+ </span>
49533
+ </div>
49534
+ <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49535
+ {{ t('revoke') }}
49536
+ </ion-button>
49537
+ </div>
49538
+ }
49539
+ </div>
49540
+ }
49541
+
49542
+ @if (errorMsg()) {
49543
+ <p class="apikey-error">{{ errorMsg() }}</p>
49544
+ }
49545
+
49546
+ <!-- Crear -->
49547
+ @if (showCreate()) {
49548
+ <div class="apikey-create">
49549
+ <ion-input
49550
+ label="{{ t('nameLabel') }}"
49551
+ labelPlacement="stacked"
49552
+ [placeholder]="t('namePlaceholder')"
49553
+ [(ngModel)]="name"
49554
+ />
49555
+ <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49556
+ <div class="apikey-perms">
49557
+ @for (p of availablePermissions(); track p) {
49558
+ <ion-checkbox
49559
+ labelPlacement="end"
49560
+ [checked]="selectedPerms().has(p)"
49561
+ (ionChange)="togglePerm(p, $event)"
49562
+ >
49563
+ {{ p }}
49564
+ </ion-checkbox>
49565
+ }
49566
+ </div>
49567
+ <ion-input
49568
+ label="{{ t('expiresLabel') }}"
49569
+ labelPlacement="stacked"
49570
+ type="number"
49571
+ [(ngModel)]="expiresInDays"
49572
+ />
49573
+ <div class="apikey-actions">
49574
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49575
+ @if (submitting()) {
49576
+ <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49577
+ } @else {
49578
+ {{ t('create') }}
49579
+ }
49580
+ </ion-button>
49581
+ <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49582
+ {{ t('cancel') }}
49583
+ </ion-button>
49584
+ </div>
49585
+ </div>
49586
+ } @else {
49587
+ <div class="apikey-actions">
49588
+ <ion-button expand="block" color="primary" (click)="openCreate()">
49589
+ {{ t('newKey') }}
49590
+ </ion-button>
49591
+ </div>
49592
+ }
49593
+ }
49594
+ }
49595
+ </ion-content>
49596
+ `, 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"] }] }); }
49597
+ }
49598
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ApiKeysModalComponent, decorators: [{
49599
+ type: Component,
49600
+ args: [{ selector: 'val-api-keys-modal', standalone: true, imports: [
49601
+ FormsModule,
49602
+ IonHeader,
49603
+ IonToolbar,
49604
+ IonButtons,
49605
+ IonButton,
49606
+ IonContent,
49607
+ IonInput,
49608
+ IonCheckbox,
49609
+ IonSpinner,
49610
+ DisplayComponent,
49611
+ TextComponent,
49612
+ TitleComponent,
49613
+ ], template: `
49614
+ <ion-header>
49615
+ <ion-toolbar>
49616
+ <ion-buttons slot="end">
49617
+ <ion-button fill="clear" color="dark" (click)="dismiss()">
49618
+ <strong>{{ t('close') }}</strong>
49619
+ </ion-button>
49620
+ </ion-buttons>
49621
+ </ion-toolbar>
49622
+ </ion-header>
49623
+
49624
+ <ion-content class="ion-padding">
49625
+ <val-display [props]="{ content: t('title'), size: 'small', color: 'dark' }" />
49626
+ <val-title [props]="{ content: t('hint'), size: 'large', color: '', bold: false }" />
49627
+
49628
+ @if (secret(); as s) {
49629
+ <!-- Secreto recién creado: mostrar UNA vez -->
49630
+ <div class="apikey-secret">
49631
+ <val-title [props]="{ content: t('secretTitle'), size: 'large', color: '', bold: true }" />
49632
+ <p class="apikey-secret__warn">{{ t('secretWarning') }}</p>
49633
+ <div class="apikey-secret__box">
49634
+ <code>{{ s.secret }}</code>
49635
+ </div>
49636
+ <div class="apikey-actions">
49637
+ <ion-button expand="block" color="dark" (click)="copy(s.secret)">
49638
+ {{ copied() ? t('copied') : t('copy') }}
49639
+ </ion-button>
49640
+ <ion-button expand="block" fill="outline" color="dark" (click)="finishSecret()">
49641
+ {{ t('done') }}
49642
+ </ion-button>
49643
+ </div>
49644
+ </div>
49645
+ } @else {
49646
+ @if (loading()) {
49647
+ <ion-spinner name="dots" />
49648
+ } @else {
49649
+ <!-- Lista -->
49650
+ @if (keys().length === 0) {
49651
+ <val-text [props]="{ content: t('empty'), size: 'medium', color: 'medium', bold: false }" />
49652
+ } @else {
49653
+ <div class="apikey-list">
49654
+ @for (k of keys(); track k.keyId) {
49655
+ <div class="apikey-row">
49656
+ <div class="apikey-row__main">
49657
+ <span class="apikey-row__name">{{ k.name }}</span>
49658
+ <span class="apikey-row__meta">
49659
+ {{ k.permissions.length }} {{ t('permsCount') }} · {{ t('lastUsed') }}:
49660
+ {{ k.lastUsedAt || t('never') }}
49661
+ </span>
49662
+ </div>
49663
+ <ion-button fill="clear" color="danger" size="small" (click)="revoke(k)">
49664
+ {{ t('revoke') }}
49665
+ </ion-button>
49666
+ </div>
49667
+ }
49668
+ </div>
49669
+ }
49670
+
49671
+ @if (errorMsg()) {
49672
+ <p class="apikey-error">{{ errorMsg() }}</p>
49673
+ }
49674
+
49675
+ <!-- Crear -->
49676
+ @if (showCreate()) {
49677
+ <div class="apikey-create">
49678
+ <ion-input
49679
+ label="{{ t('nameLabel') }}"
49680
+ labelPlacement="stacked"
49681
+ [placeholder]="t('namePlaceholder')"
49682
+ [(ngModel)]="name"
49683
+ />
49684
+ <p class="apikey-create__label">{{ t('permissionsLabel') }}</p>
49685
+ <div class="apikey-perms">
49686
+ @for (p of availablePermissions(); track p) {
49687
+ <ion-checkbox
49688
+ labelPlacement="end"
49689
+ [checked]="selectedPerms().has(p)"
49690
+ (ionChange)="togglePerm(p, $event)"
49691
+ >
49692
+ {{ p }}
49693
+ </ion-checkbox>
49694
+ }
49695
+ </div>
49696
+ <ion-input
49697
+ label="{{ t('expiresLabel') }}"
49698
+ labelPlacement="stacked"
49699
+ type="number"
49700
+ [(ngModel)]="expiresInDays"
49701
+ />
49702
+ <div class="apikey-actions">
49703
+ <ion-button expand="block" color="dark" [disabled]="submitting()" (click)="submit()">
49704
+ @if (submitting()) {
49705
+ <ion-spinner name="dots" />&nbsp;{{ t('creating') }}
49706
+ } @else {
49707
+ {{ t('create') }}
49708
+ }
49709
+ </ion-button>
49710
+ <ion-button expand="block" fill="clear" color="medium" (click)="showCreate.set(false)">
49711
+ {{ t('cancel') }}
49712
+ </ion-button>
49713
+ </div>
49714
+ </div>
49715
+ } @else {
49716
+ <div class="apikey-actions">
49717
+ <ion-button expand="block" color="primary" (click)="openCreate()">
49718
+ {{ t('newKey') }}
49719
+ </ion-button>
49720
+ </div>
49721
+ }
49722
+ }
49723
+ }
49724
+ </ion-content>
49725
+ `, 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"] }]
49726
+ }], ctorParameters: () => [], propDecorators: { _modalRef: [{
49727
+ type: Input
49728
+ }], i18nNamespace: [{
49729
+ type: Input
49730
+ }] } });
49731
+
48849
49732
  /**
48850
49733
  * Defaults i18n (es/en) embebidos en `val-organization-view`. Auto-registrados en
48851
49734
  * el constructor del componente si el consumer no proveyó el namespace
@@ -48901,6 +49784,16 @@ const ORGANIZATION_VIEW_I18N = {
48901
49784
  inviteCtaTitle: '¿Deseas agregar a alguien?',
48902
49785
  inviteCtaHint: 'Invita a usuarios a unirse a tu organización.',
48903
49786
  inviteOpen: 'Invitar',
49787
+ importOpen: 'Importar',
49788
+ apiKeysOpen: 'Gestionar',
49789
+ apiKeysSectionTitle: 'Integraciones (API Keys)',
49790
+ 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).',
49791
+ apiKeysMoreInfo: 'Más información',
49792
+ orgsMoreInfo: 'Más información',
49793
+ orgsInfoTitle: '¿Qué son las organizaciones?',
49794
+ orgsInfoSubtitle: 'Todo lo que necesitas saber',
49795
+ orgsInfoClose: 'Cerrar',
49796
+ 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
49797
  viewMemberCta: 'Ver miembro',
48905
49798
  roleViewer: 'Visor',
48906
49799
  roleEditor: 'Editor',
@@ -48970,6 +49863,16 @@ const ORGANIZATION_VIEW_I18N = {
48970
49863
  inviteCtaTitle: 'Want to add someone?',
48971
49864
  inviteCtaHint: 'Invite users to join your organization.',
48972
49865
  inviteOpen: 'Invite',
49866
+ importOpen: 'Import',
49867
+ apiKeysOpen: 'Manage',
49868
+ apiKeysSectionTitle: 'Integrations (API Keys)',
49869
+ 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).',
49870
+ apiKeysMoreInfo: 'More information',
49871
+ orgsMoreInfo: 'More information',
49872
+ orgsInfoTitle: 'What are organizations?',
49873
+ orgsInfoSubtitle: 'Everything you need to know',
49874
+ orgsInfoClose: 'Close',
49875
+ 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
49876
  viewMemberCta: 'View member',
48974
49877
  roleViewer: 'Viewer',
48975
49878
  roleEditor: 'Editor',
@@ -48994,7 +49897,7 @@ const ORGANIZATION_VIEW_I18N = {
48994
49897
  },
48995
49898
  };
48996
49899
 
48997
- const DEFAULT_NAMESPACE$3 = 'Settings.Organization';
49900
+ const DEFAULT_NAMESPACE$4 = 'Settings.Organization';
48998
49901
  const DEFAULT_VIEW_PERMISSIONS_ROUTE = '/app/settings/permissions';
48999
49902
  const DEFAULT_LEAVE_REDIRECT_ROUTE = '/app/settings/account';
49000
49903
  /**
@@ -49051,7 +49954,7 @@ class OrganizationViewComponent {
49051
49954
  showInvite: merged.showInvite ?? true,
49052
49955
  showTransferOwnership: merged.showTransferOwnership ?? true,
49053
49956
  showLeave: merged.showLeave ?? true,
49054
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
49957
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$4,
49055
49958
  viewPermissionsRoute: merged.viewPermissionsRoute ?? DEFAULT_VIEW_PERMISSIONS_ROUTE,
49056
49959
  leaveRedirectRoute: merged.leaveRedirectRoute ?? DEFAULT_LEAVE_REDIRECT_ROUTE,
49057
49960
  onOrgEdited: merged.onOrgEdited,
@@ -49083,6 +49986,22 @@ class OrganizationViewComponent {
49083
49986
  this.members = signal([]);
49084
49987
  this.membersLoading = signal(false);
49085
49988
  this.membersLoadError = signal(null);
49989
+ this.membersErrorState = computed(() => {
49990
+ this.i18n.lang();
49991
+ const err = this.membersLoadError();
49992
+ if (!err)
49993
+ return null;
49994
+ return createErrorStateProps(err, {
49995
+ title: { offline: this.tt('offlineTitle'), error: this.tt('errorTitle') },
49996
+ description: {
49997
+ offline: this.tt('offlineHint'),
49998
+ error: interpretError(err).message,
49999
+ },
50000
+ retryLabel: this.tt('retry'),
50001
+ onRetry: () => this.loadMembers(),
50002
+ retrying: this.membersLoading(),
50003
+ });
50004
+ });
49086
50005
  this.membersNextToken = signal(null);
49087
50006
  this.loadingMoreMembers = signal(false);
49088
50007
  this.showAllMembers = signal(false);
@@ -49169,6 +50088,22 @@ class OrganizationViewComponent {
49169
50088
  shape: 'round',
49170
50089
  type: 'button',
49171
50090
  }));
50091
+ this.openImportButtonProps = computed(() => ({
50092
+ text: this.tt('importOpen'),
50093
+ color: 'dark',
50094
+ fill: 'outline',
50095
+ size: 'default',
50096
+ shape: 'round',
50097
+ type: 'button',
50098
+ }));
50099
+ this.openApiKeysButtonProps = computed(() => ({
50100
+ text: this.tt('apiKeysOpen'),
50101
+ color: 'dark',
50102
+ fill: 'clear',
50103
+ size: 'default',
50104
+ shape: 'round',
50105
+ type: 'button',
50106
+ }));
49172
50107
  /** Guarda para detectar el primer disparo del effect (evita doble carga). */
49173
50108
  this.lastLoadedOrgId = null;
49174
50109
  const ns = this.ns;
@@ -49269,6 +50204,59 @@ class OrganizationViewComponent {
49269
50204
  backdropDismiss: true,
49270
50205
  });
49271
50206
  }
50207
+ onOpenImport() {
50208
+ void this.modalService.open({
50209
+ component: MemberImportModalComponent,
50210
+ componentProps: {
50211
+ orgId: this.activeOrgId(),
50212
+ availableRoles: this.availableRoles().map(r => r.name),
50213
+ onSuccess: () => {
50214
+ this.loadMembers();
50215
+ this.resolvedConfig().onMemberInvited?.();
50216
+ },
50217
+ },
50218
+ breakpoints: {
50219
+ breakpoints: [0, 0.92],
50220
+ initialBreakpoint: 0.92,
50221
+ showHandle: true,
50222
+ },
50223
+ backdropDismiss: true,
50224
+ });
50225
+ }
50226
+ onOpenApiKeys() {
50227
+ void this.modalService.open({
50228
+ component: ApiKeysModalComponent,
50229
+ breakpoints: {
50230
+ breakpoints: [0, 0.92],
50231
+ initialBreakpoint: 0.92,
50232
+ showHandle: true,
50233
+ },
50234
+ backdropDismiss: true,
50235
+ });
50236
+ }
50237
+ // "Más información" sobre organizaciones — mismo sheet explicativo que muestra
50238
+ // account-view sin org, pero disponible siempre (todo user tiene org por defecto).
50239
+ onOrgInfo() {
50240
+ void this.modalService.open({
50241
+ component: OrgInfoSheetComponent,
50242
+ componentProps: {
50243
+ title: this.tt('orgsInfoTitle'),
50244
+ subtitle: this.tt('orgsInfoSubtitle'),
50245
+ body: this.tt('orgsInfoContent'),
50246
+ closeLabel: this.tt('orgsInfoClose'),
50247
+ closeProps: {
50248
+ text: this.tt('orgsInfoClose'),
50249
+ fill: 'outline',
50250
+ color: 'dark',
50251
+ size: 'default',
50252
+ expand: 'block',
50253
+ type: 'button',
50254
+ },
50255
+ },
50256
+ breakpoints: { breakpoints: [0, 0.85], initialBreakpoint: 0.85, showHandle: true },
50257
+ backdropDismiss: true,
50258
+ });
50259
+ }
49272
50260
  onViewPermissions() {
49273
50261
  this.nav.navigateByUrl(this.resolvedConfig().viewPermissionsRoute);
49274
50262
  }
@@ -49350,6 +50338,7 @@ class OrganizationViewComponent {
49350
50338
  if (!orgId)
49351
50339
  return Promise.resolve();
49352
50340
  this.membersLoading.set(true);
50341
+ this.membersLoadError.set(null);
49353
50342
  this.membersNextToken.set(null);
49354
50343
  this.showAllMembers.set(false);
49355
50344
  return new Promise(resolve => {
@@ -49513,6 +50502,12 @@ class OrganizationViewComponent {
49513
50502
  }
49514
50503
  </div>
49515
50504
 
50505
+ <!-- "Más información" sobre organizaciones — disponible siempre (todo
50506
+ user tiene org por defecto y no ve el empty-state de account). -->
50507
+ <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50508
+ {{ tt('orgsMoreInfo') }}
50509
+ </button>
50510
+
49516
50511
  <div class="section-body">
49517
50512
  @if (loading()) {
49518
50513
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -49560,6 +50555,8 @@ class OrganizationViewComponent {
49560
50555
  <div class="section-body">
49561
50556
  @if (membersLoading()) {
49562
50557
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50558
+ } @else if (membersErrorState()) {
50559
+ <val-empty-state [props]="membersErrorState()!" />
49563
50560
  } @else if (members().length === 0) {
49564
50561
  <val-text
49565
50562
  [props]="{
@@ -49652,10 +50649,25 @@ class OrganizationViewComponent {
49652
50649
  />
49653
50650
  </div>
49654
50651
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50652
+ <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
49655
50653
  </div>
49656
50654
  </section>
49657
50655
  }
49658
50656
 
50657
+ <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50658
+ <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50659
+ <div class="invite-cta">
50660
+ <div class="invite-cta__text">
50661
+ <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50662
+ <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50663
+ <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50664
+ {{ tt('apiKeysMoreInfo') }}
50665
+ </button>
50666
+ </div>
50667
+ <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50668
+ </div>
50669
+ </section>
50670
+
49659
50671
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
49660
50672
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
49661
50673
  <section class="settings-section" data-testid="org-transfer-section">
@@ -49746,7 +50758,7 @@ class OrganizationViewComponent {
49746
50758
  </section>
49747
50759
  }
49748
50760
  </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"] }] }); }
50761
+ `, 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
50762
  }
49751
50763
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: OrganizationViewComponent, decorators: [{
49752
50764
  type: Component,
@@ -49760,6 +50772,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49760
50772
  ButtonComponent,
49761
50773
  MemberCardComponent,
49762
50774
  TransferOwnershipModalComponent,
50775
+ MemberImportModalComponent,
50776
+ ApiKeysModalComponent,
50777
+ OrgInfoSheetComponent,
49763
50778
  ], template: `
49764
50779
  <div class="page">
49765
50780
  <header class="page-header">
@@ -49791,6 +50806,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49791
50806
  }
49792
50807
  </div>
49793
50808
 
50809
+ <!-- "Más información" sobre organizaciones — disponible siempre (todo
50810
+ user tiene org por defecto y no ve el empty-state de account). -->
50811
+ <button class="org-more-info-link" type="button" (click)="onOrgInfo()">
50812
+ {{ tt('orgsMoreInfo') }}
50813
+ </button>
50814
+
49794
50815
  <div class="section-body">
49795
50816
  @if (loading()) {
49796
50817
  <val-skeleton-layout [props]="{ preset: 'detail', rows: 4 }" />
@@ -49838,6 +50859,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49838
50859
  <div class="section-body">
49839
50860
  @if (membersLoading()) {
49840
50861
  <val-skeleton-layout [props]="{ preset: 'list', rows: 3 }" />
50862
+ } @else if (membersErrorState()) {
50863
+ <val-empty-state [props]="membersErrorState()!" />
49841
50864
  } @else if (members().length === 0) {
49842
50865
  <val-text
49843
50866
  [props]="{
@@ -49930,10 +50953,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
49930
50953
  />
49931
50954
  </div>
49932
50955
  <val-button [props]="openInviteButtonProps()" (click)="onOpenInvite()" />
50956
+ <val-button [props]="openImportButtonProps()" (click)="onOpenImport()" />
49933
50957
  </div>
49934
50958
  </section>
49935
50959
  }
49936
50960
 
50961
+ <!-- API Keys: sección propia. Integraciones M2M (import de miembros, etc.). -->
50962
+ <section *valHasPermission="'api-keys:read'" class="settings-section" data-testid="org-apikeys-section">
50963
+ <div class="invite-cta">
50964
+ <div class="invite-cta__text">
50965
+ <val-title [props]="{ size: 'medium', color: 'dark', bold: true, content: tt('apiKeysSectionTitle') }" />
50966
+ <val-text [props]="{ size: 'medium', color: 'dark', bold: false, content: tt('apiKeysSectionHint') }" />
50967
+ <button class="org-more-info-link" type="button" (click)="onOpenApiKeys()">
50968
+ {{ tt('apiKeysMoreInfo') }}
50969
+ </button>
50970
+ </div>
50971
+ <val-button [props]="openApiKeysButtonProps()" (click)="onOpenApiKeys()" />
50972
+ </div>
50973
+ </section>
50974
+
49937
50975
  @if (resolvedConfig().showTransferOwnership && isOwner()) {
49938
50976
  <!-- Transfer ownership: solo owner (gating RBAC interno) -->
49939
50977
  <section class="settings-section" data-testid="org-transfer-section">
@@ -50024,7 +51062,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
50024
51062
  </section>
50025
51063
  }
50026
51064
  </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"] }]
51065
+ `, 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
51066
  }], ctorParameters: () => [], propDecorators: { config: [{
50029
51067
  type: Input
50030
51068
  }] } });
@@ -50138,7 +51176,7 @@ addIcons({
50138
51176
  notificationsOutline,
50139
51177
  refreshOutline,
50140
51178
  });
50141
- const DEFAULT_NAMESPACE$2 = 'Settings.Notifications';
51179
+ const DEFAULT_NAMESPACE$3 = 'Settings.Notifications';
50142
51180
  /**
50143
51181
  * `val-notification-preferences-view` — **preferencias de notificaciones push**
50144
51182
  * (FCM) full-feature autocontenida (organism). Promovida desde `showcase` bajo el
@@ -50181,7 +51219,7 @@ class NotificationPreferencesViewComponent {
50181
51219
  const merged = { ...fromRoute, ...(this.config ?? {}) };
50182
51220
  return {
50183
51221
  showDetails: merged.showDetails ?? true,
50184
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
51222
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$3,
50185
51223
  onPushEnabled: merged.onPushEnabled,
50186
51224
  onPushDisabled: merged.onPushDisabled,
50187
51225
  };
@@ -50952,7 +51990,7 @@ const PERMISSIONS_VIEW_I18N = {
50952
51990
  },
50953
51991
  };
50954
51992
 
50955
- const DEFAULT_NAMESPACE$1 = 'Settings.Permissions';
51993
+ const DEFAULT_NAMESPACE$2 = 'Settings.Permissions';
50956
51994
  /**
50957
51995
  * `val-permissions-view` — vista Permisos full-feature autocontenida (organism).
50958
51996
  * Visor RBAC de solo lectura: muestra los roles del sistema (Visor/Editor/Admin +
@@ -50992,7 +52030,7 @@ class PermissionsViewComponent {
50992
52030
  return {
50993
52031
  showSystemRoles: merged.showSystemRoles ?? true,
50994
52032
  showCustomRoles: merged.showCustomRoles ?? true,
50995
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
52033
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$2,
50996
52034
  };
50997
52035
  });
50998
52036
  this.loading = signal(false);
@@ -51769,7 +52807,7 @@ addIcons({
51769
52807
  openOutline,
51770
52808
  personOutline,
51771
52809
  });
51772
- const DEFAULT_NAMESPACE = 'Notifications';
52810
+ const DEFAULT_NAMESPACE$1 = 'Notifications';
51773
52811
  const DEFAULT_SETTINGS_PATH = '/app/settings/notifications';
51774
52812
  const DEFAULT_PAGE_SIZE = 10;
51775
52813
  const DEFAULT_MAX_LOAD = 50;
@@ -51829,7 +52867,7 @@ class NotificationsViewComponent {
51829
52867
  showPushOffCta: merged.showPushOffCta ?? true,
51830
52868
  enableMarkAll: merged.enableMarkAll ?? true,
51831
52869
  settingsNotificationsPath: merged.settingsNotificationsPath ?? DEFAULT_SETTINGS_PATH,
51832
- i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
52870
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE$1,
51833
52871
  pageSize: merged.pageSize ?? DEFAULT_PAGE_SIZE,
51834
52872
  maxLoad: merged.maxLoad ?? DEFAULT_MAX_LOAD,
51835
52873
  onNotificationOpen: merged.onNotificationOpen,
@@ -52497,6 +53535,797 @@ function provideValtechNotificationsRoutes(opts) {
52497
53535
  ];
52498
53536
  }
52499
53537
 
53538
+ /**
53539
+ * Defaults i18n del componente (es/en). El componente los auto-registra en su
53540
+ * constructor si el consumer no proveyó el namespace `ContentReaction`. Esto
53541
+ * garantiza que NUNCA haya una key faltante: una key faltante se evalúa en el
53542
+ * template por cada change-detection → `console.warn` por CD → con el
53543
+ * `val-debug-console` activo (espeja console.* a un signal) se dispara un loop
53544
+ * infinito que congela el navegador en dev. Auto-registrar lo elimina de raíz.
53545
+ */
53546
+ const CONTENT_REACTION_I18N = {
53547
+ es: {
53548
+ question: '¿Te resultó útil este contenido?',
53549
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
53550
+ submit: 'Enviar opinión',
53551
+ update: 'Actualizar opinión',
53552
+ thankYou: '¡Gracias por tu opinión!',
53553
+ submitted: 'Tu opinión ha sido registrada',
53554
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53555
+ positiveLabel: 'Sí, mucho',
53556
+ negativeLabel: 'No tanto',
53557
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53558
+ },
53559
+ en: {
53560
+ question: 'Was this content helpful?',
53561
+ commentPlaceholder: 'Tell us more (optional)...',
53562
+ submit: 'Send feedback',
53563
+ update: 'Update feedback',
53564
+ thankYou: 'Thanks for your feedback!',
53565
+ submitted: 'Your feedback has been recorded',
53566
+ errorSubmitting: 'Failed to send. Please try again.',
53567
+ positiveLabel: 'Yes, a lot',
53568
+ negativeLabel: 'Not really',
53569
+ negativeFeedbackTitle: "Tell us what you didn't like?",
53570
+ },
53571
+ };
53572
+ /**
53573
+ * Componente para reacciones de contenido con emojis.
53574
+ *
53575
+ * @example
53576
+ * ```html
53577
+ * <val-content-reaction
53578
+ * [props]="{
53579
+ * entityRef: { entityType: 'article', entityId: 'art-123' },
53580
+ * question: '¿Te fue útil este artículo?'
53581
+ * }"
53582
+ * (reactionSubmit)="onReactionSubmit($event)"
53583
+ * />
53584
+ * ```
53585
+ */
53586
+ class ContentReactionComponent {
53587
+ constructor() {
53588
+ this.feedbackService = inject(FeedbackService);
53589
+ this.toast = inject(ToastService);
53590
+ this.i18n = inject(I18nService);
53591
+ this.auth = inject(AuthService, { optional: true });
53592
+ this.isConfigured = computed(() => this.feedbackService.isConfigured());
53593
+ this.props = {};
53594
+ this.reactionSubmit = new EventEmitter();
53595
+ this.reactionChange = new EventEmitter();
53596
+ // Estado reactivo
53597
+ this.state = signal({
53598
+ selectedValue: null,
53599
+ comment: '',
53600
+ isLoading: false,
53601
+ isSubmitted: false,
53602
+ hadPreviousReaction: false,
53603
+ error: null,
53604
+ });
53605
+ // Valores por defecto
53606
+ this.defaultEmojis = ['😞', '😐', '😊'];
53607
+ this.defaultLabels = ['No me ayudó', 'Regular', 'Muy útil'];
53608
+ // Computed properties
53609
+ this.resolvedProps = computed(() => ({
53610
+ entityRef: this.props.entityRef,
53611
+ question: this.props.question || this.t('question'),
53612
+ showComment: this.props.showComment ?? true,
53613
+ commentOnValues: this.props.commentOnValues,
53614
+ commentPlaceholder: this.props.commentPlaceholder || this.t('commentPlaceholder'),
53615
+ maxCommentLength: this.props.maxCommentLength ?? 500,
53616
+ emojis: this.props.emojis || this.defaultEmojis,
53617
+ emojiLabels: this.props.emojiLabels || this.defaultLabels,
53618
+ showThankYou: this.props.showThankYou ?? true,
53619
+ thankYouMessage: this.props.thankYouMessage || this.t('thankYou'),
53620
+ disabled: this.props.disabled ?? false,
53621
+ readonly: this.props.readonly ?? false,
53622
+ // Variante: 'emoji' cuando se declara explícitamente o cuando se pasan emojis custom.
53623
+ variant: this.props.variant ?? (this.props.emojis ? 'emoji' : 'buttons'),
53624
+ }));
53625
+ this.activeReactionValues = computed(() => this.resolvedProps().emojis.length === 2 ? ['negative', 'positive'] : ['negative', 'neutral', 'positive']);
53626
+ this.showCommentField = computed(() => {
53627
+ const sel = this.state().selectedValue;
53628
+ if (!sel)
53629
+ return false;
53630
+ const commentOn = this.resolvedProps().commentOnValues;
53631
+ if (commentOn !== undefined)
53632
+ return commentOn.includes(sel);
53633
+ return this.resolvedProps().showComment;
53634
+ });
53635
+ this.canSubmit = computed(() => this.state().selectedValue !== null && !this.state().isLoading);
53636
+ // Auto-registro de defaults i18n (es/en) si el consumer no proveyó el
53637
+ // namespace. Garantiza que `t()` nunca caiga en key faltante. Ver
53638
+ // CONTENT_REACTION_I18N. Idempotente: solo la 1ª instancia registra.
53639
+ if (!this.i18n.hasNamespace('ContentReaction')) {
53640
+ this.i18n.registerContent('ContentReaction', CONTENT_REACTION_I18N);
53641
+ }
53642
+ }
53643
+ ngOnInit() {
53644
+ this.loadPreviousReaction();
53645
+ }
53646
+ ngOnChanges(changes) {
53647
+ if (changes['props'] && !changes['props'].firstChange) {
53648
+ this.loadPreviousReaction();
53649
+ }
53650
+ }
53651
+ async loadPreviousReaction() {
53652
+ if (!this.props.entityRef)
53653
+ return;
53654
+ // Si hay initialValue, usarlo directamente sin consultar
53655
+ if (this.props.initialValue) {
53656
+ this.state.update(s => ({
53657
+ ...s,
53658
+ selectedValue: this.props.initialValue,
53659
+ hadPreviousReaction: true,
53660
+ isLoading: false,
53661
+ isSubmitted: true,
53662
+ }));
53663
+ return;
53664
+ }
53665
+ // Si skipInitialCheck está activo, no consultar
53666
+ if (this.props.skipInitialCheck) {
53667
+ return;
53668
+ }
53669
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
53670
+ try {
53671
+ const check = await this.feedbackService.checkFeedback(this.props.entityRef.entityType, this.props.entityRef.entityId);
53672
+ if (check.hasFeedback && check.reactionValue) {
53673
+ this.state.update(s => ({
53674
+ ...s,
53675
+ selectedValue: check.reactionValue,
53676
+ hadPreviousReaction: true,
53677
+ isLoading: false,
53678
+ isSubmitted: true,
53679
+ }));
53680
+ }
53681
+ else {
53682
+ this.state.update(s => ({ ...s, isLoading: false }));
53683
+ }
53684
+ }
53685
+ catch (error) {
53686
+ console.error('Error loading previous reaction:', error);
53687
+ this.state.update(s => ({ ...s, isLoading: false }));
53688
+ }
53689
+ }
53690
+ selectReaction(value) {
53691
+ if (this.resolvedProps().disabled || this.resolvedProps().readonly)
53692
+ return;
53693
+ const previousValue = this.state().selectedValue;
53694
+ this.state.update(s => ({
53695
+ ...s,
53696
+ selectedValue: value,
53697
+ isSubmitted: false,
53698
+ error: null,
53699
+ }));
53700
+ this.reactionChange.emit({ value, previousValue });
53701
+ }
53702
+ async submitReaction() {
53703
+ const currentState = this.state();
53704
+ const props = this.resolvedProps();
53705
+ if (!currentState.selectedValue || props.disabled)
53706
+ return;
53707
+ this.state.update(s => ({ ...s, isLoading: true, error: null }));
53708
+ try {
53709
+ // Determinar si usar endpoint anónimo o autenticado
53710
+ const isAuthenticated = this.auth?.isAuthenticated() ?? false;
53711
+ const useAnonymous = this.props.allowAnonymous && !isAuthenticated;
53712
+ if (useAnonymous) {
53713
+ // Usar endpoint anónimo (público)
53714
+ await this.feedbackService.createAnonymousReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53715
+ }
53716
+ else {
53717
+ // Usar endpoint autenticado
53718
+ await this.feedbackService.createReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
53719
+ }
53720
+ this.state.update(s => ({
53721
+ ...s,
53722
+ isLoading: false,
53723
+ isSubmitted: true,
53724
+ comment: '',
53725
+ hadPreviousReaction: !useAnonymous, // Solo para autenticados
53726
+ }));
53727
+ this.reactionSubmit.emit({
53728
+ value: currentState.selectedValue,
53729
+ comment: currentState.comment || undefined,
53730
+ entityRef: props.entityRef,
53731
+ isUpdate: currentState.hadPreviousReaction,
53732
+ });
53733
+ if (props.showThankYou) {
53734
+ this.toast.show({
53735
+ message: props.thankYouMessage,
53736
+ duration: 2000,
53737
+ });
53738
+ }
53739
+ }
53740
+ catch (error) {
53741
+ console.error('Error submitting reaction:', error);
53742
+ this.state.update(s => ({
53743
+ ...s,
53744
+ isLoading: false,
53745
+ error: this.t('errorSubmitting'),
53746
+ }));
53747
+ this.toast.show({
53748
+ message: this.t('errorSubmitting'),
53749
+ duration: 3000,
53750
+ });
53751
+ }
53752
+ }
53753
+ updateComment(event) {
53754
+ const value = event.detail.value || '';
53755
+ this.state.update(s => ({ ...s, comment: value }));
53756
+ }
53757
+ getEmoji(index) {
53758
+ return this.resolvedProps().emojis[index];
53759
+ }
53760
+ getEmojiLabel(index) {
53761
+ return this.resolvedProps().emojiLabels[index];
53762
+ }
53763
+ isSelected(value) {
53764
+ return this.state().selectedValue === value;
53765
+ }
53766
+ t(key) {
53767
+ const translations = {
53768
+ question: '¿Te resultó útil este contenido?',
53769
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
53770
+ submit: 'Enviar opinión',
53771
+ update: 'Actualizar opinión',
53772
+ thankYou: '¡Gracias por tu opinión!',
53773
+ submitted: 'Tu opinión ha sido registrada',
53774
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
53775
+ positiveLabel: 'Sí, mucho',
53776
+ negativeLabel: 'No tanto',
53777
+ negativeFeedbackTitle: '¿Cuéntanos qué no te gustó?',
53778
+ };
53779
+ return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
53780
+ }
53781
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53782
+ 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"] }] }); }
53783
+ }
53784
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
53785
+ type: Component,
53786
+ 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"] }]
53787
+ }], ctorParameters: () => [], propDecorators: { props: [{
53788
+ type: Input
53789
+ }], reactionSubmit: [{
53790
+ type: Output
53791
+ }], reactionChange: [{
53792
+ type: Output
53793
+ }] } });
53794
+
53795
+ /**
53796
+ * Defaults i18n (es/en) embebidos en `val-about-view`. Auto-registrados en el
53797
+ * constructor del componente si el consumer no proveyó el namespace
53798
+ * (`Settings.About` por default). SOLO incluye las keys que la vista usa.
53799
+ *
53800
+ * Textos portados EXACTOS del override `Settings.About` de showcase
53801
+ * (`frontend/showcase/src/i18n/index.ts`). Las keys de las acciones
53802
+ * showcase-specific (`feedback*`, `share*`, `faq*`, `donate*`) NO se incluyen:
53803
+ * esas acciones se modelan ahora como `actions[]` de la config (el consumer
53804
+ * aporta sus propios textos). `copyrightText` se incluye aquí (antes salía del
53805
+ * namespace `Layout`) para que la vista sea autocontenida.
53806
+ */
53807
+ const ABOUT_VIEW_I18N = {
53808
+ es: {
53809
+ pageTitle: 'Acerca de',
53810
+ pageDescription: 'Demo del factory Valtech',
53811
+ tagline: 'App de demo — explora las funcionalidades en vivo',
53812
+ descTitle: 'Acerca de Showcase',
53813
+ 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.',
53814
+ actionsTitle: 'Acciones',
53815
+ rateTitle: 'Calificar la app',
53816
+ rateDesc: 'Cuéntanos qué te ha parecido la experiencia.',
53817
+ rateModalTitle: 'Tu opinión',
53818
+ rateQuestion: '¿Qué te ha parecido la experiencia hasta ahora?',
53819
+ rateComment: 'Cuéntanos más (opcional)…',
53820
+ rateThanks: '¡Gracias por tu opinión!',
53821
+ rateClose: 'Cerrar',
53822
+ rateBad: 'No tanto',
53823
+ rateMid: 'Bien',
53824
+ rateGood: '¡Me encanta!',
53825
+ socialTitle: 'Síguenos',
53826
+ socialDesc: 'Estamos en redes sociales.',
53827
+ versionLabel: 'Versión',
53828
+ versionCopied: 'v{v} copiado',
53829
+ copyrightText: '© {year}, Soluciones Valtech Ltda. o sus afiliados. Todos los derechos reservados.',
53830
+ },
53831
+ en: {
53832
+ pageTitle: 'About',
53833
+ pageDescription: 'Valtech factory demo',
53834
+ tagline: 'Demo app — explore features live',
53835
+ descTitle: 'About Showcase',
53836
+ descBody: 'Showcase is the reference app for the Valtech factory. Explore the full flow live: authentication, profile, organizations, notifications and account settings.',
53837
+ actionsTitle: 'Actions',
53838
+ rateTitle: 'Rate the app',
53839
+ rateDesc: 'Tell us how your experience has been.',
53840
+ rateModalTitle: 'Your feedback',
53841
+ rateQuestion: 'How has your experience been so far?',
53842
+ rateComment: 'Tell us more (optional)…',
53843
+ rateThanks: 'Thanks for your feedback!',
53844
+ rateClose: 'Close',
53845
+ rateBad: 'Not great',
53846
+ rateMid: 'Good',
53847
+ rateGood: 'Love it!',
53848
+ socialTitle: 'Follow us',
53849
+ socialDesc: "We're on social media.",
53850
+ versionLabel: 'Version',
53851
+ versionCopied: 'v{v} copied',
53852
+ copyrightText: '© {year}, Soluciones Valtech Ltda. or its affiliates. All rights reserved.',
53853
+ },
53854
+ };
53855
+
53856
+ addIcons({
53857
+ copyOutline,
53858
+ starOutline,
53859
+ chatbubbleEllipsesOutline,
53860
+ shareSocialOutline,
53861
+ helpCircleOutline,
53862
+ logoFacebook,
53863
+ logoInstagram,
53864
+ logoLinkedin,
53865
+ logoTwitter,
53866
+ logoYoutube,
53867
+ logoTiktok,
53868
+ });
53869
+ const DEFAULT_NAMESPACE = 'Settings.About';
53870
+ const DEFAULT_BRAND_NAME = 'Valtech';
53871
+ const DEFAULT_LOGO_SRC = 'assets/images/main-icon.png';
53872
+ const DEFAULT_RATING_ENTITY_ID = 'experience';
53873
+ /**
53874
+ * `val-about-view` — vista About full-feature autocontenida (organism).
53875
+ * Promovida desde `showcase` bajo el proceso de ADR-021. Reúne:
53876
+ * - **HERO** — logo + nombre de marca + tagline + pill de versión (copiable).
53877
+ * - **Descripción** — título + cuerpo.
53878
+ * - **Acciones** — action-card de rating built-in (abre `val-content-reaction`
53879
+ * DIRECTAMENTE vía `ModalService`, sin wrapper local — decisión CTO) + las
53880
+ * acciones genéricas que el consumer aporte por `config.actions`.
53881
+ * - **Redes** — links sociales (`config.socialLinks`); sección oculta si vacía.
53882
+ * - **Footer** — copyright con el año actual.
53883
+ *
53884
+ * Lo app-specific (logo, nombre, versión, redes, acciones share/faq) viaja como
53885
+ * DATA de config — NO como slots (ADR-021). La versión la pasa el consumer
53886
+ * (`config.version` = su `APP_VERSION`); la lib NO importa el `version.ts` de la
53887
+ * app. Si no se pasa versión, el pill se oculta.
53888
+ *
53889
+ * NO renderiza ion-content — vive dentro de val-page-wrapper. `ActivatedRoute`
53890
+ * se inyecta `{ optional: true }` solo para leer el route data de config.
53891
+ *
53892
+ * Auto-registra sus defaults i18n (es/en) en el constructor si el consumer no
53893
+ * proveyó el namespace configurado (default `Settings.About`).
53894
+ *
53895
+ * GOTCHA i18n ContentReaction: `val-content-reaction` exige su namespace
53896
+ * `ContentReaction` registrado donde se use (key faltante en binding + CD =
53897
+ * navegador congelado en dev). `ContentReactionComponent` se auto-registra ese
53898
+ * namespace en SU constructor, y como se abre vía `ModalService` ese constructor
53899
+ * corre antes de que su template binde — queda cubierto. Igual lo importamos
53900
+ * como dependencia explícita de esta vista para que la garantía sea local.
53901
+ */
53902
+ class AboutViewComponent {
53903
+ /** Namespace i18n resuelto (capturado para llamadas no-reactivas). */
53904
+ get ns() {
53905
+ return this.resolvedConfig().i18nNamespace;
53906
+ }
53907
+ constructor() {
53908
+ this.nav = inject(NavigationService);
53909
+ this.i18n = inject(I18nService);
53910
+ this.modalService = inject(ModalService);
53911
+ this.toast = inject(ToastService);
53912
+ this.route = inject(ActivatedRoute, { optional: true });
53913
+ this.resolvedConfig = computed(() => {
53914
+ const fromRoute = (this.route?.snapshot.data['aboutConfig'] ?? {});
53915
+ const merged = { ...fromRoute, ...(this.config ?? {}) };
53916
+ return {
53917
+ brand: {
53918
+ name: merged.brand?.name ?? DEFAULT_BRAND_NAME,
53919
+ logoSrc: merged.brand?.logoSrc ?? DEFAULT_LOGO_SRC,
53920
+ },
53921
+ version: merged.version ?? '',
53922
+ socialLinks: merged.socialLinks ?? [],
53923
+ showRating: merged.showRating ?? true,
53924
+ ratingEntityId: merged.ratingEntityId ?? DEFAULT_RATING_ENTITY_ID,
53925
+ actions: merged.actions ?? [],
53926
+ i18nNamespace: merged.i18nNamespace ?? DEFAULT_NAMESPACE,
53927
+ onRated: merged.onRated,
53928
+ };
53929
+ });
53930
+ this.brandName = computed(() => this.resolvedConfig().brand.name);
53931
+ this.logoSrc = computed(() => this.resolvedConfig().brand.logoSrc);
53932
+ this.version = computed(() => this.resolvedConfig().version);
53933
+ this.socialLinks = computed(() => this.resolvedConfig().socialLinks);
53934
+ this.showRating = computed(() => this.resolvedConfig().showRating);
53935
+ this.actions = computed(() => this.resolvedConfig().actions);
53936
+ /** Copyright del footer — año actual interpolado en `copyrightText`. */
53937
+ this.copyright = computed(() => {
53938
+ this.i18n.lang();
53939
+ const year = String(new Date().getFullYear());
53940
+ return this.tt('copyrightText').replace('{year}', year);
53941
+ });
53942
+ /** Title metadata para secciones — bold + size small. */
53943
+ this.rateActionCard = computed(() => {
53944
+ this.i18n.lang();
53945
+ return {
53946
+ icon: { ionicon: 'star-outline', color: 'primary' },
53947
+ title: this.tt('rateTitle'),
53948
+ description: this.tt('rateDesc'),
53949
+ size: 'medium',
53950
+ bordered: false,
53951
+ shadowed: false,
53952
+ };
53953
+ });
53954
+ const ns = this.ns;
53955
+ if (!this.i18n.hasNamespace(ns)) {
53956
+ this.i18n.registerContent(ns, ABOUT_VIEW_I18N);
53957
+ }
53958
+ this.nav.setBackHeader('pageTitle', ns, { withMenu: true });
53959
+ }
53960
+ /** Helper i18n bound al namespace resuelto. Reactivo vía `lang()`. */
53961
+ tt(key) {
53962
+ this.i18n.lang();
53963
+ return this.i18n.t(key, this.ns);
53964
+ }
53965
+ /** Title metadata para secciones — bold + size small. */
53966
+ sectionTitle(key) {
53967
+ this.i18n.lang();
53968
+ return {
53969
+ content: this.tt(key),
53970
+ size: 'small',
53971
+ color: 'dark',
53972
+ bold: true,
53973
+ };
53974
+ }
53975
+ /** ActionCardMetadata para una acción genérica del consumer. */
53976
+ extraActionCard(action) {
53977
+ return {
53978
+ icon: { ionicon: action.icon ?? 'chatbubble-ellipses-outline', color: 'primary' },
53979
+ title: action.label,
53980
+ description: action.description,
53981
+ size: 'medium',
53982
+ bordered: false,
53983
+ shadowed: false,
53984
+ };
53985
+ }
53986
+ /** Copia la versión al portapapeles + toast (color dark). */
53987
+ async copyVersion() {
53988
+ const v = this.version();
53989
+ if (!v)
53990
+ return;
53991
+ try {
53992
+ await navigator.clipboard.writeText(v);
53993
+ this.toast.show({
53994
+ message: this.tt('versionCopied').replace('{v}', v),
53995
+ color: 'dark',
53996
+ duration: 1500,
53997
+ });
53998
+ }
53999
+ catch {
54000
+ // clipboard no disponible — silencioso
54001
+ }
54002
+ }
54003
+ /** Abre una URL externa en nueva tab. `mailto:` queda en la misma tab. */
54004
+ openUrl(url) {
54005
+ if (url.startsWith('mailto:')) {
54006
+ window.location.href = url;
54007
+ return;
54008
+ }
54009
+ window.open(url, '_blank', 'noopener,noreferrer');
54010
+ }
54011
+ /**
54012
+ * "Calificar la app" — abre `val-content-reaction` DIRECTAMENTE como sheet
54013
+ * modal (sin wrapper local — decisión CTO). El componente persiste la reacción
54014
+ * contra el backend de feedback y muestra su propio estado de "gracias"; el
54015
+ * sheet se cierra arrastrando (breakpoint 0). El namespace i18n
54016
+ * `ContentReaction` lo auto-registra `ContentReactionComponent` en su
54017
+ * constructor, que corre al crear el modal — sin riesgo de key faltante.
54018
+ *
54019
+ * Sheet (bottom sheet): el contenido scrollea dentro y el botón "Enviar" queda
54020
+ * siempre alcanzable, también en viewports chicos.
54021
+ */
54022
+ async openRating() {
54023
+ const reactionProps = {
54024
+ // entityType "app" distingue esta reacción de las de artículos/FAQs.
54025
+ entityRef: { entityType: 'app', entityId: this.resolvedConfig().ratingEntityId },
54026
+ question: this.tt('rateQuestion'),
54027
+ commentPlaceholder: this.tt('rateComment'),
54028
+ thankYouMessage: this.tt('rateThanks'),
54029
+ showComment: true,
54030
+ showThankYou: true,
54031
+ // Modal one-shot: NO bloquear el render con el check de rating previo.
54032
+ skipInitialCheck: true,
54033
+ emojis: ['😕', '🙂', '😍'],
54034
+ emojiLabels: [this.tt('rateBad'), this.tt('rateMid'), this.tt('rateGood')],
54035
+ };
54036
+ await this.modalService.open({
54037
+ component: ContentReactionComponent,
54038
+ componentProps: { props: reactionProps },
54039
+ breakpoints: {
54040
+ initialBreakpoint: 0.9,
54041
+ breakpoints: [0, 0.9, 1],
54042
+ showHandle: true,
54043
+ handleBehavior: 'cycle',
54044
+ },
54045
+ });
54046
+ this.resolvedConfig().onRated?.();
54047
+ }
54048
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
54049
+ 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: `
54050
+ <ion-grid class="about-grid">
54051
+ <!-- HERO: logo + name + version -->
54052
+ <div class="about-hero">
54053
+ <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54054
+ <val-title
54055
+ [props]="{
54056
+ content: brandName(),
54057
+ size: 'xlarge',
54058
+ color: 'dark',
54059
+ bold: true,
54060
+ }"
54061
+ />
54062
+ <val-text
54063
+ [props]="{
54064
+ content: tt('tagline'),
54065
+ size: 'medium',
54066
+ color: 'dark',
54067
+ bold: false,
54068
+ }"
54069
+ />
54070
+ @if (version()) {
54071
+ <div
54072
+ class="version-row"
54073
+ (click)="copyVersion()"
54074
+ role="button"
54075
+ [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54076
+ >
54077
+ <val-pill
54078
+ [props]="{
54079
+ label: tt('versionLabel') + ' ' + version(),
54080
+ color: 'dark',
54081
+ weight: 'bold',
54082
+ size: 'small',
54083
+ }"
54084
+ />
54085
+ <ion-icon name="copy-outline" class="version-copy-icon" />
54086
+ </div>
54087
+ }
54088
+ </div>
54089
+
54090
+ <!-- DESCRIPTION -->
54091
+ <section class="about-section">
54092
+ <val-title [props]="sectionTitle('descTitle')" />
54093
+ <val-text
54094
+ [props]="{
54095
+ content: tt('descBody'),
54096
+ size: 'medium',
54097
+ color: 'dark',
54098
+ bold: false,
54099
+ }"
54100
+ />
54101
+ </section>
54102
+
54103
+ <!-- ACTIONS — rating built-in + acciones del consumer. -->
54104
+ @if (showRating() || actions().length > 0) {
54105
+ <section class="about-section about-section--reviews">
54106
+ <val-title [props]="sectionTitle('actionsTitle')" />
54107
+ <div class="about-actions">
54108
+ @if (showRating()) {
54109
+ <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54110
+ }
54111
+ @for (action of actions(); track action.label) {
54112
+ <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54113
+ }
54114
+ </div>
54115
+ </section>
54116
+ }
54117
+
54118
+ <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54119
+ @if (socialLinks().length > 0) {
54120
+ <section class="about-section">
54121
+ <val-title [props]="sectionTitle('socialTitle')" />
54122
+ <val-text
54123
+ [props]="{
54124
+ content: tt('socialDesc'),
54125
+ size: 'medium',
54126
+ color: 'dark',
54127
+ bold: false,
54128
+ }"
54129
+ />
54130
+ <div class="about-social">
54131
+ @for (s of socialLinks(); track s.url) {
54132
+ <ion-button
54133
+ fill="clear"
54134
+ size="small"
54135
+ class="social-btn"
54136
+ [attr.aria-label]="s.label"
54137
+ (click)="openUrl(s.url)"
54138
+ >
54139
+ <ion-icon [name]="s.icon" slot="icon-only" />
54140
+ </ion-button>
54141
+ }
54142
+ </div>
54143
+ </section>
54144
+ }
54145
+
54146
+ <!-- FOOTER -->
54147
+ <footer class="about-footer">
54148
+ <val-text
54149
+ [props]="{
54150
+ content: copyright(),
54151
+ size: 'small',
54152
+ color: 'dark',
54153
+ bold: false,
54154
+ }"
54155
+ />
54156
+ </footer>
54157
+ </ion-grid>
54158
+ `, 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"] }] }); }
54159
+ }
54160
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AboutViewComponent, decorators: [{
54161
+ type: Component,
54162
+ args: [{ selector: 'val-about-view', standalone: true, imports: [IonButton, IonGrid, IonIcon, PillComponent, TitleComponent, TextComponent, ActionCardComponent], template: `
54163
+ <ion-grid class="about-grid">
54164
+ <!-- HERO: logo + name + version -->
54165
+ <div class="about-hero">
54166
+ <img class="about-hero__logo" [src]="logoSrc()" [alt]="brandName()" />
54167
+ <val-title
54168
+ [props]="{
54169
+ content: brandName(),
54170
+ size: 'xlarge',
54171
+ color: 'dark',
54172
+ bold: true,
54173
+ }"
54174
+ />
54175
+ <val-text
54176
+ [props]="{
54177
+ content: tt('tagline'),
54178
+ size: 'medium',
54179
+ color: 'dark',
54180
+ bold: false,
54181
+ }"
54182
+ />
54183
+ @if (version()) {
54184
+ <div
54185
+ class="version-row"
54186
+ (click)="copyVersion()"
54187
+ role="button"
54188
+ [attr.aria-label]="tt('versionLabel') + ' ' + version()"
54189
+ >
54190
+ <val-pill
54191
+ [props]="{
54192
+ label: tt('versionLabel') + ' ' + version(),
54193
+ color: 'dark',
54194
+ weight: 'bold',
54195
+ size: 'small',
54196
+ }"
54197
+ />
54198
+ <ion-icon name="copy-outline" class="version-copy-icon" />
54199
+ </div>
54200
+ }
54201
+ </div>
54202
+
54203
+ <!-- DESCRIPTION -->
54204
+ <section class="about-section">
54205
+ <val-title [props]="sectionTitle('descTitle')" />
54206
+ <val-text
54207
+ [props]="{
54208
+ content: tt('descBody'),
54209
+ size: 'medium',
54210
+ color: 'dark',
54211
+ bold: false,
54212
+ }"
54213
+ />
54214
+ </section>
54215
+
54216
+ <!-- ACTIONS — rating built-in + acciones del consumer. -->
54217
+ @if (showRating() || actions().length > 0) {
54218
+ <section class="about-section about-section--reviews">
54219
+ <val-title [props]="sectionTitle('actionsTitle')" />
54220
+ <div class="about-actions">
54221
+ @if (showRating()) {
54222
+ <val-action-card [props]="rateActionCard()" (onClick)="openRating()" />
54223
+ }
54224
+ @for (action of actions(); track action.label) {
54225
+ <val-action-card [props]="extraActionCard(action)" (onClick)="action.onClick()" />
54226
+ }
54227
+ </div>
54228
+ </section>
54229
+ }
54230
+
54231
+ <!-- SOCIAL — penúltimo, justo antes del footer de derechos. -->
54232
+ @if (socialLinks().length > 0) {
54233
+ <section class="about-section">
54234
+ <val-title [props]="sectionTitle('socialTitle')" />
54235
+ <val-text
54236
+ [props]="{
54237
+ content: tt('socialDesc'),
54238
+ size: 'medium',
54239
+ color: 'dark',
54240
+ bold: false,
54241
+ }"
54242
+ />
54243
+ <div class="about-social">
54244
+ @for (s of socialLinks(); track s.url) {
54245
+ <ion-button
54246
+ fill="clear"
54247
+ size="small"
54248
+ class="social-btn"
54249
+ [attr.aria-label]="s.label"
54250
+ (click)="openUrl(s.url)"
54251
+ >
54252
+ <ion-icon [name]="s.icon" slot="icon-only" />
54253
+ </ion-button>
54254
+ }
54255
+ </div>
54256
+ </section>
54257
+ }
54258
+
54259
+ <!-- FOOTER -->
54260
+ <footer class="about-footer">
54261
+ <val-text
54262
+ [props]="{
54263
+ content: copyright(),
54264
+ size: 'small',
54265
+ color: 'dark',
54266
+ bold: false,
54267
+ }"
54268
+ />
54269
+ </footer>
54270
+ </ion-grid>
54271
+ `, 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"] }]
54272
+ }], ctorParameters: () => [], propDecorators: { config: [{
54273
+ type: Input
54274
+ }] } });
54275
+
54276
+ /**
54277
+ * Configuración acotada de `val-about-view` (object-first). La vista About es
54278
+ * app-specific por naturaleza (logo, nombre, versión, redes, acciones extra),
54279
+ * así que lo propio de cada app se parametriza como **DATA de config** — NO como
54280
+ * slots arbitrarios (ADR-021: una vista full-feature no abre slots; si falta
54281
+ * algo, se promueve un nuevo punto de extensión).
54282
+ *
54283
+ * Ejes de variación permitidos:
54284
+ * - **branding** — `brand.name` / `brand.logoSrc` (qué muestra el HERO).
54285
+ * - **versión** — `version` la pasa el consumer (la lib NO puede importar el
54286
+ * `version.ts` de la app). Si no se pasa, se oculta el pill de versión.
54287
+ * - **redes** — `socialLinks`; si está vacío se oculta la sección social.
54288
+ * - **rating** — `showRating` + `ratingEntityId` + `onRated`.
54289
+ * - **acciones extra** — `actions[]` genéricas que el consumer agrega junto a
54290
+ * la acción de rating built-in.
54291
+ * - **branding i18n** — `i18nNamespace` (default `'Settings.About'`).
54292
+ */
54293
+
54294
+ /**
54295
+ * Helper para montar la vista About (`val-about-view`) como ruta en una app del
54296
+ * factory. El `config` se pasa por route `data` (`aboutConfig`) y el componente
54297
+ * lo lee como fallback de su `@Input() config` (el `@Input` gana).
54298
+ *
54299
+ * @example
54300
+ * ```ts
54301
+ * // settings.routes.ts
54302
+ * import { APP_VERSION } from '../version';
54303
+ *
54304
+ * export const settingsRoutes: Routes = [
54305
+ * ...provideValtechAboutRoutes({ config: { version: APP_VERSION } }), // → /app/about
54306
+ * { path: 'preferences', loadComponent: () => ... },
54307
+ * ];
54308
+ *
54309
+ * // con redes y acciones extra:
54310
+ * ...provideValtechAboutRoutes({
54311
+ * config: {
54312
+ * version: APP_VERSION,
54313
+ * socialLinks: VALTECH_SOCIAL_LINKS.map(s => ({ icon: s.icon, url: s.url, label: s.name })),
54314
+ * actions: [{ label: 'Compartir', icon: 'share-social-outline', onClick: () => share() }],
54315
+ * },
54316
+ * }),
54317
+ * ```
54318
+ */
54319
+ function provideValtechAboutRoutes(opts) {
54320
+ return [
54321
+ {
54322
+ path: opts?.path ?? 'about',
54323
+ component: AboutViewComponent,
54324
+ data: { aboutConfig: opts?.config },
54325
+ },
54326
+ ];
54327
+ }
54328
+
52500
54329
  /** Built-in label sets for the three platform locales. */
52501
54330
  const CALLOUT_LABELS = {
52502
54331
  es: {
@@ -58626,263 +60455,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
58626
60455
  type: Output
58627
60456
  }] } });
58628
60457
 
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
60458
  const DEFAULT_SPLASH_SCREEN_CONFIG = {
58887
60459
  fadeOutDuration: 300,
58888
60460
  };
@@ -62735,5 +64307,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
62735
64307
  * Generated bundle index. Do not edit.
62736
64308
  */
62737
64309
 
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 };
64310
+ 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
64311
  //# sourceMappingURL=valtech-components.mjs.map