valtech-components 2.0.595 → 2.0.596

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 (31) hide show
  1. package/esm2022/lib/components/molecules/content-reaction/content-reaction.component.mjs +191 -0
  2. package/esm2022/lib/components/molecules/content-reaction/types.mjs +2 -0
  3. package/esm2022/lib/components/molecules/docs-section/docs-section.component.mjs +85 -0
  4. package/esm2022/lib/components/molecules/docs-section/types.mjs +2 -0
  5. package/esm2022/lib/components/molecules/feedback-form/feedback-form.component.mjs +354 -0
  6. package/esm2022/lib/components/molecules/feedback-form/types.mjs +2 -0
  7. package/esm2022/lib/components/templates/docs-page/docs-page.component.mjs +188 -0
  8. package/esm2022/lib/components/templates/docs-page/types.mjs +2 -0
  9. package/esm2022/lib/services/ads/types.mjs +1 -1
  10. package/esm2022/lib/services/feedback/config.mjs +49 -0
  11. package/esm2022/lib/services/feedback/feedback.service.mjs +228 -0
  12. package/esm2022/lib/services/feedback/index.mjs +44 -0
  13. package/esm2022/lib/services/feedback/types.mjs +30 -0
  14. package/esm2022/public-api.mjs +10 -5
  15. package/fesm2022/valtech-components.mjs +1135 -4
  16. package/fesm2022/valtech-components.mjs.map +1 -1
  17. package/lib/components/molecules/content-reaction/content-reaction.component.d.ts +57 -0
  18. package/lib/components/molecules/content-reaction/types.d.ts +61 -0
  19. package/lib/components/molecules/docs-section/docs-section.component.d.ts +29 -0
  20. package/lib/components/molecules/docs-section/types.d.ts +27 -0
  21. package/lib/components/molecules/feedback-form/feedback-form.component.d.ts +58 -0
  22. package/lib/components/molecules/feedback-form/types.d.ts +54 -0
  23. package/lib/components/templates/docs-page/docs-page.component.d.ts +54 -0
  24. package/lib/components/templates/docs-page/types.d.ts +69 -0
  25. package/lib/services/ads/types.d.ts +10 -0
  26. package/lib/services/feedback/config.d.ts +35 -0
  27. package/lib/services/feedback/feedback.service.d.ts +110 -0
  28. package/lib/services/feedback/index.d.ts +40 -0
  29. package/lib/services/feedback/types.d.ts +159 -0
  30. package/package.json +1 -1
  31. package/public-api.d.ts +9 -0
@@ -5,14 +5,14 @@ import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal,
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgStyle, NgFor, NgClass, isPlatformBrowser } from '@angular/common';
7
7
  import { addIcons } from 'ionicons';
8
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, settings, home, search, person, helpCircle, informationCircle, documentText, notifications, 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, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, menuOutline, searchOutline } from 'ionicons/icons';
8
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, settings, home, search, person, helpCircle, informationCircle, documentText, notifications, 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, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline, searchOutline } from 'ionicons/icons';
9
9
  import * as i1$1 from '@angular/router';
10
10
  import { RouterLink, Router, NavigationEnd, RouterOutlet } from '@angular/router';
11
11
  import { Browser } from '@capacitor/browser';
12
12
  import * as i1$2 from '@angular/platform-browser';
13
13
  import QRCodeStyling from 'qr-code-styling';
14
14
  import * as i1$3 from '@angular/forms';
15
- import { ReactiveFormsModule, FormsModule, FormControl, Validators } from '@angular/forms';
15
+ import { ReactiveFormsModule, FormsModule, FormControl, Validators, FormBuilder } from '@angular/forms';
16
16
  import { BehaviorSubject, isObservable, firstValueFrom, Subject, map, distinctUntilChanged, of, Observable, throwError, from, filter as filter$1 } from 'rxjs';
17
17
  import * as i1$4 from 'ng-otp-input';
18
18
  import { NgOtpInputComponent, NgOtpInputModule } from 'ng-otp-input';
@@ -28,7 +28,7 @@ import 'prismjs/components/prism-bash';
28
28
  import Swiper from 'swiper';
29
29
  import { Navigation, Pagination, EffectFade, EffectCube, EffectCoverflow, EffectFlip, Autoplay } from 'swiper/modules';
30
30
  import * as i1$8 from '@angular/common/http';
31
- import { provideHttpClient, withInterceptors } from '@angular/common/http';
31
+ import { provideHttpClient, withInterceptors, HttpClient } from '@angular/common/http';
32
32
  import { filter, map as map$1, catchError, tap, switchMap, finalize, take, debounceTime, takeUntil } from 'rxjs/operators';
33
33
  import { Analytics, logEvent, setUserId, setUserProperties, provideAnalytics, getAnalytics } from '@angular/fire/analytics';
34
34
  import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
@@ -34761,6 +34761,872 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
34761
34761
  type: Input
34762
34762
  }] } });
34763
34763
 
34764
+ /**
34765
+ * Token de inyección para la configuración de Feedback.
34766
+ */
34767
+ const VALTECH_FEEDBACK_CONFIG = new InjectionToken('ValtechFeedbackConfig');
34768
+ /**
34769
+ * Configuración por defecto.
34770
+ */
34771
+ const DEFAULT_FEEDBACK_CONFIG = {
34772
+ feedbackPrefix: '/v1/feedback',
34773
+ maxAttachments: 5,
34774
+ maxFileSize: 10 * 1024 * 1024, // 10MB
34775
+ allowedFileTypes: ['image/*', 'video/*', 'application/pdf'],
34776
+ storagePath: 'feedback',
34777
+ };
34778
+ /**
34779
+ * Provee el servicio de feedback a la aplicación Angular.
34780
+ *
34781
+ * @param config - Configuración de feedback
34782
+ * @returns EnvironmentProviders para usar en bootstrapApplication
34783
+ *
34784
+ * @example
34785
+ * ```typescript
34786
+ * // main.ts
34787
+ * import { bootstrapApplication } from '@angular/platform-browser';
34788
+ * import { provideValtechFeedback } from 'valtech-components';
34789
+ * import { environment } from './environments/environment';
34790
+ *
34791
+ * bootstrapApplication(AppComponent, {
34792
+ * providers: [
34793
+ * provideValtechAuth({ apiUrl: environment.apiUrl }),
34794
+ * provideValtechFeedback({
34795
+ * apiUrl: environment.apiUrl,
34796
+ * appId: 'my-app-name',
34797
+ * }),
34798
+ * ],
34799
+ * });
34800
+ * ```
34801
+ */
34802
+ function provideValtechFeedback(config) {
34803
+ const mergedConfig = {
34804
+ ...DEFAULT_FEEDBACK_CONFIG,
34805
+ ...config,
34806
+ };
34807
+ return makeEnvironmentProviders([
34808
+ { provide: VALTECH_FEEDBACK_CONFIG, useValue: mergedConfig },
34809
+ ]);
34810
+ }
34811
+
34812
+ /**
34813
+ * Servicio para gestionar feedback de usuarios.
34814
+ *
34815
+ * @example
34816
+ * ```typescript
34817
+ * @Component({...})
34818
+ * export class MyComponent {
34819
+ * private feedbackService = inject(FeedbackService);
34820
+ *
34821
+ * async submitFeedback() {
34822
+ * const response = await this.feedbackService.createAsync(
34823
+ * 'feedback',
34824
+ * 'Mi comentario',
34825
+ * 'Descripción detallada...'
34826
+ * );
34827
+ * console.log('Feedback enviado:', response.feedbackId);
34828
+ * }
34829
+ * }
34830
+ * ```
34831
+ */
34832
+ class FeedbackService {
34833
+ constructor() {
34834
+ this.config = inject(VALTECH_FEEDBACK_CONFIG);
34835
+ this.http = inject(HttpClient);
34836
+ }
34837
+ /**
34838
+ * URL base para endpoints de feedback.
34839
+ */
34840
+ get baseUrl() {
34841
+ return `${this.config.apiUrl}${this.config.feedbackPrefix}`;
34842
+ }
34843
+ /**
34844
+ * Captura el contexto del dispositivo automáticamente.
34845
+ */
34846
+ captureDeviceContext() {
34847
+ const ua = navigator.userAgent;
34848
+ return {
34849
+ browser: this.detectBrowser(ua),
34850
+ os: this.detectOS(ua),
34851
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
34852
+ language: navigator.language,
34853
+ userAgent: ua,
34854
+ pageUrl: window.location.href,
34855
+ };
34856
+ }
34857
+ /**
34858
+ * Crea un nuevo feedback.
34859
+ *
34860
+ * @param type - Tipo de feedback
34861
+ * @param title - Título del feedback
34862
+ * @param description - Descripción detallada
34863
+ * @param attachments - URLs de archivos adjuntos (opcional)
34864
+ * @param contentRef - Referencia a contenido específico (opcional)
34865
+ * @returns Observable con la respuesta
34866
+ */
34867
+ create(type, title, description, attachments = [], contentRef) {
34868
+ const request = {
34869
+ type,
34870
+ title,
34871
+ description,
34872
+ attachments,
34873
+ contentRef,
34874
+ deviceContext: this.captureDeviceContext(),
34875
+ appId: this.config.appId,
34876
+ };
34877
+ return this.http.post(this.baseUrl, request);
34878
+ }
34879
+ /**
34880
+ * Crea un nuevo feedback (versión async/await).
34881
+ */
34882
+ async createAsync(type, title, description, attachments = [], contentRef) {
34883
+ return firstValueFrom(this.create(type, title, description, attachments, contentRef));
34884
+ }
34885
+ /**
34886
+ * Obtiene un feedback por ID (solo el propietario).
34887
+ *
34888
+ * @param feedbackId - ID del feedback
34889
+ * @returns Observable con la respuesta
34890
+ */
34891
+ getById(feedbackId) {
34892
+ return this.http.get(`${this.baseUrl}/${feedbackId}`);
34893
+ }
34894
+ /**
34895
+ * Obtiene un feedback por ID (versión async/await).
34896
+ */
34897
+ async getByIdAsync(feedbackId) {
34898
+ return firstValueFrom(this.getById(feedbackId));
34899
+ }
34900
+ /**
34901
+ * Valida si un archivo cumple con las restricciones.
34902
+ */
34903
+ validateFile(file) {
34904
+ // Verificar tamaño
34905
+ if (file.size > this.config.maxFileSize) {
34906
+ const maxSizeMB = Math.round(this.config.maxFileSize / (1024 * 1024));
34907
+ return {
34908
+ valid: false,
34909
+ error: `El archivo excede el tamaño máximo de ${maxSizeMB}MB`,
34910
+ };
34911
+ }
34912
+ // Verificar tipo
34913
+ const allowedTypes = this.config.allowedFileTypes || [];
34914
+ const isAllowed = allowedTypes.some((pattern) => {
34915
+ if (pattern.endsWith('/*')) {
34916
+ const baseType = pattern.replace('/*', '');
34917
+ return file.type.startsWith(baseType);
34918
+ }
34919
+ return file.type === pattern;
34920
+ });
34921
+ if (!isAllowed) {
34922
+ return {
34923
+ valid: false,
34924
+ error: 'Tipo de archivo no permitido',
34925
+ };
34926
+ }
34927
+ return { valid: true };
34928
+ }
34929
+ /**
34930
+ * Obtiene la configuración actual del servicio.
34931
+ */
34932
+ getConfig() {
34933
+ return this.config;
34934
+ }
34935
+ // =========================================================================
34936
+ // Reaction Methods (Content feedback with emojis)
34937
+ // =========================================================================
34938
+ /**
34939
+ * Verifica si el usuario ya dio feedback para una entidad específica.
34940
+ *
34941
+ * @param entityType - Tipo de entidad (article, docs, feature, etc.)
34942
+ * @param entityId - ID de la entidad
34943
+ * @returns Promise con la respuesta de verificación
34944
+ *
34945
+ * @example
34946
+ * ```typescript
34947
+ * const check = await this.feedbackService.checkFeedback('article', 'art-123');
34948
+ * if (check.hasFeedback) {
34949
+ * console.log('Ya dio feedback:', check.reactionValue);
34950
+ * }
34951
+ * ```
34952
+ */
34953
+ async checkFeedback(entityType, entityId) {
34954
+ const params = new URLSearchParams({
34955
+ appId: this.config.appId,
34956
+ entityType,
34957
+ entityId,
34958
+ });
34959
+ return firstValueFrom(this.http.get(`${this.baseUrl}/check?${params}`));
34960
+ }
34961
+ /**
34962
+ * Crea o actualiza una reacción (feedback con emoji).
34963
+ *
34964
+ * @param entityRef - Referencia a la entidad
34965
+ * @param value - Valor de la reacción (negative, neutral, positive)
34966
+ * @param comment - Comentario opcional (máx 500 caracteres)
34967
+ * @returns Promise con la respuesta
34968
+ *
34969
+ * @example
34970
+ * ```typescript
34971
+ * const response = await this.feedbackService.createReaction(
34972
+ * { entityType: 'article', entityId: 'art-123' },
34973
+ * 'positive',
34974
+ * 'Muy útil!'
34975
+ * );
34976
+ * ```
34977
+ */
34978
+ async createReaction(entityRef, value, comment) {
34979
+ const request = {
34980
+ type: 'reaction',
34981
+ entityRef,
34982
+ reactionValue: value,
34983
+ description: comment || '',
34984
+ deviceContext: this.captureDeviceContext(),
34985
+ appId: this.config.appId,
34986
+ };
34987
+ return firstValueFrom(this.http.post(this.baseUrl, request));
34988
+ }
34989
+ // =========================================================================
34990
+ // Helpers privados para detección de browser/OS
34991
+ // =========================================================================
34992
+ detectBrowser(ua) {
34993
+ if (ua.includes('Edg/'))
34994
+ return 'Edge';
34995
+ if (ua.includes('Chrome/'))
34996
+ return 'Chrome';
34997
+ if (ua.includes('Firefox/'))
34998
+ return 'Firefox';
34999
+ if (ua.includes('Safari/') && !ua.includes('Chrome'))
35000
+ return 'Safari';
35001
+ if (ua.includes('Opera') || ua.includes('OPR/'))
35002
+ return 'Opera';
35003
+ return 'Unknown';
35004
+ }
35005
+ detectOS(ua) {
35006
+ if (ua.includes('Windows NT 10'))
35007
+ return 'Windows 10';
35008
+ if (ua.includes('Windows NT 11'))
35009
+ return 'Windows 11';
35010
+ if (ua.includes('Windows'))
35011
+ return 'Windows';
35012
+ if (ua.includes('Mac OS X')) {
35013
+ const match = ua.match(/Mac OS X (\d+[._]\d+)/);
35014
+ if (match) {
35015
+ return `macOS ${match[1].replace('_', '.')}`;
35016
+ }
35017
+ return 'macOS';
35018
+ }
35019
+ if (ua.includes('Android'))
35020
+ return 'Android';
35021
+ if (ua.includes('iPhone') || ua.includes('iPad'))
35022
+ return 'iOS';
35023
+ if (ua.includes('Linux'))
35024
+ return 'Linux';
35025
+ return 'Unknown';
35026
+ }
35027
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
35028
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackService, providedIn: 'root' }); }
35029
+ }
35030
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackService, decorators: [{
35031
+ type: Injectable,
35032
+ args: [{ providedIn: 'root' }]
35033
+ }] });
35034
+
35035
+ /**
35036
+ * Configuración por defecto de tipos de feedback.
35037
+ */
35038
+ const DEFAULT_FEEDBACK_TYPE_OPTIONS = [
35039
+ {
35040
+ value: 'issue',
35041
+ label: 'Reportar problema',
35042
+ description: 'Algo no funciona correctamente',
35043
+ icon: 'bug-outline',
35044
+ },
35045
+ {
35046
+ value: 'poor-content',
35047
+ label: 'Contenido incorrecto',
35048
+ description: 'Información incorrecta o desactualizada',
35049
+ icon: 'document-text-outline',
35050
+ },
35051
+ {
35052
+ value: 'feedback',
35053
+ label: 'Comentario general',
35054
+ description: 'Tu opinión o experiencia',
35055
+ icon: 'chatbubble-outline',
35056
+ },
35057
+ {
35058
+ value: 'suggestion',
35059
+ label: 'Sugerencia',
35060
+ description: 'Propuesta de mejora o nueva funcionalidad',
35061
+ icon: 'bulb-outline',
35062
+ },
35063
+ ];
35064
+
35065
+ /**
35066
+ * Valtech Feedback Service
35067
+ *
35068
+ * Servicio para gestionar feedback de usuarios a nivel de plataforma.
35069
+ *
35070
+ * @example
35071
+ * ```typescript
35072
+ * // main.ts - Configuración
35073
+ * import { provideValtechFeedback } from 'valtech-components';
35074
+ *
35075
+ * bootstrapApplication(AppComponent, {
35076
+ * providers: [
35077
+ * provideValtechAuth({ apiUrl: environment.apiUrl }),
35078
+ * provideValtechFeedback({
35079
+ * apiUrl: environment.apiUrl,
35080
+ * appId: 'my-app-name',
35081
+ * }),
35082
+ * ],
35083
+ * });
35084
+ *
35085
+ * // component.ts - Uso
35086
+ * import { FeedbackService } from 'valtech-components';
35087
+ *
35088
+ * @Component({...})
35089
+ * export class MyComponent {
35090
+ * private feedbackService = inject(FeedbackService);
35091
+ *
35092
+ * async submitFeedback() {
35093
+ * const response = await this.feedbackService.createAsync(
35094
+ * 'feedback',
35095
+ * 'Título',
35096
+ * 'Descripción...'
35097
+ * );
35098
+ * }
35099
+ * }
35100
+ * ```
35101
+ */
35102
+ // Configuration
35103
+
35104
+ /**
35105
+ * val-feedback-form
35106
+ *
35107
+ * Formulario reutilizable para enviar feedback desde cualquier parte de la aplicación.
35108
+ *
35109
+ * @example
35110
+ * ```html
35111
+ * <!-- Feedback general -->
35112
+ * <val-feedback-form
35113
+ * [props]="{ defaultType: 'feedback', showTypeSelector: true }"
35114
+ * (onSubmit)="handleSuccess($event)"
35115
+ * (onCancel)="closeModal()"
35116
+ * />
35117
+ *
35118
+ * <!-- Reportar contenido incorrecto -->
35119
+ * <val-feedback-form
35120
+ * [props]="{
35121
+ * defaultType: 'poor-content',
35122
+ * showTypeSelector: false,
35123
+ * contentRef: { contentId: article.id, contentType: 'article' },
35124
+ * submitButtonText: 'Reportar contenido'
35125
+ * }"
35126
+ * />
35127
+ * ```
35128
+ */
35129
+ class FeedbackFormComponent {
35130
+ constructor() {
35131
+ /**
35132
+ * Configuración del formulario.
35133
+ */
35134
+ this.props = {};
35135
+ /**
35136
+ * Evento emitido cuando el feedback se envía exitosamente.
35137
+ */
35138
+ this.onSubmit = new EventEmitter();
35139
+ /**
35140
+ * Evento emitido cuando el usuario cancela.
35141
+ */
35142
+ this.onCancel = new EventEmitter();
35143
+ this.fb = inject(FormBuilder);
35144
+ this.feedbackService = inject(FeedbackService);
35145
+ this.i18n = inject(I18nService);
35146
+ this.typeOptions = DEFAULT_FEEDBACK_TYPE_OPTIONS;
35147
+ this.isSubmitting = signal(false);
35148
+ this.isSuccess = signal(false);
35149
+ this.error = signal(null);
35150
+ addIcons({
35151
+ bugOutline,
35152
+ bulbOutline,
35153
+ chatbubbleOutline,
35154
+ checkmarkCircleOutline,
35155
+ closeCircleOutline,
35156
+ documentTextOutline,
35157
+ });
35158
+ }
35159
+ ngOnInit() {
35160
+ // Filtrar tipos habilitados si se especifica
35161
+ if (this.props.enabledTypes?.length) {
35162
+ this.typeOptions = this.typeOptions.filter((opt) => this.props.enabledTypes.includes(opt.value));
35163
+ }
35164
+ // Usar opciones personalizadas si se proporcionan
35165
+ if (this.props.typeOptions?.length) {
35166
+ this.typeOptions = this.props.typeOptions;
35167
+ }
35168
+ // Inicializar formulario
35169
+ this.form = this.fb.group({
35170
+ type: [this.props.defaultType || 'feedback', Validators.required],
35171
+ title: [
35172
+ '',
35173
+ [Validators.required, Validators.minLength(5), Validators.maxLength(200)],
35174
+ ],
35175
+ description: [
35176
+ '',
35177
+ [Validators.required, Validators.minLength(10), Validators.maxLength(5000)],
35178
+ ],
35179
+ });
35180
+ }
35181
+ async handleSubmit() {
35182
+ if (this.form.invalid || this.isSubmitting())
35183
+ return;
35184
+ this.isSubmitting.set(true);
35185
+ this.error.set(null);
35186
+ this.isSuccess.set(false);
35187
+ try {
35188
+ const { type, title, description } = this.form.value;
35189
+ const response = await this.feedbackService.createAsync(type, title, description, [], // attachments (por ahora vacío)
35190
+ this.props.contentRef);
35191
+ this.isSuccess.set(true);
35192
+ this.form.reset({ type: this.props.defaultType || 'feedback' });
35193
+ this.onSubmit.emit({
35194
+ response,
35195
+ type: type,
35196
+ title,
35197
+ });
35198
+ }
35199
+ catch (err) {
35200
+ this.error.set(err.error?.message || err.message || this.i18n.t('feedbackError'));
35201
+ }
35202
+ finally {
35203
+ this.isSubmitting.set(false);
35204
+ }
35205
+ }
35206
+ onCancelClick() {
35207
+ this.onCancel.emit();
35208
+ }
35209
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
35210
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FeedbackFormComponent, isStandalone: true, selector: "val-feedback-form", inputs: { props: "props" }, outputs: { onSubmit: "onSubmit", onCancel: "onCancel" }, ngImport: i0, template: `
35211
+ <form
35212
+ [formGroup]="form"
35213
+ (ngSubmit)="handleSubmit()"
35214
+ class="feedback-form"
35215
+ [class.compact]="props.compact"
35216
+ [ngClass]="props.cssClass"
35217
+ >
35218
+ <!-- Type selector -->
35219
+ @if (props.showTypeSelector !== false) {
35220
+ <ion-item>
35221
+ <ion-select
35222
+ formControlName="type"
35223
+ [label]="i18n.t('feedbackType')"
35224
+ labelPlacement="floating"
35225
+ interface="popover"
35226
+ >
35227
+ @for (option of typeOptions; track option.value) {
35228
+ <ion-select-option [value]="option.value">
35229
+ {{ option.label }}
35230
+ </ion-select-option>
35231
+ }
35232
+ </ion-select>
35233
+ </ion-item>
35234
+ }
35235
+
35236
+ <!-- Title -->
35237
+ <ion-item>
35238
+ <ion-textarea
35239
+ formControlName="title"
35240
+ [label]="props.titleLabel || i18n.t('title')"
35241
+ labelPlacement="floating"
35242
+ [placeholder]="props.titlePlaceholder || i18n.t('titlePlaceholder')"
35243
+ [maxlength]="200"
35244
+ [counter]="true"
35245
+ [autoGrow]="false"
35246
+ rows="1"
35247
+ ></ion-textarea>
35248
+ </ion-item>
35249
+ @if (form.get('title')?.invalid && form.get('title')?.touched) {
35250
+ <ion-note color="danger" class="ion-padding-start">
35251
+ {{ i18n.t('titleValidation') }}
35252
+ </ion-note>
35253
+ }
35254
+
35255
+ <!-- Description -->
35256
+ <ion-item>
35257
+ <ion-textarea
35258
+ formControlName="description"
35259
+ [label]="props.descriptionLabel || i18n.t('description')"
35260
+ labelPlacement="floating"
35261
+ [placeholder]="props.descriptionPlaceholder || i18n.t('descriptionPlaceholder')"
35262
+ [maxlength]="5000"
35263
+ [counter]="true"
35264
+ [autoGrow]="true"
35265
+ rows="4"
35266
+ ></ion-textarea>
35267
+ </ion-item>
35268
+ @if (form.get('description')?.invalid && form.get('description')?.touched) {
35269
+ <ion-note color="danger" class="ion-padding-start">
35270
+ {{ i18n.t('descriptionValidation') }}
35271
+ </ion-note>
35272
+ }
35273
+
35274
+ <!-- Error message -->
35275
+ @if (error()) {
35276
+ <div class="feedback-alert error">
35277
+ <ion-icon name="close-circle-outline"></ion-icon>
35278
+ <span>{{ error() }}</span>
35279
+ </div>
35280
+ }
35281
+
35282
+ <!-- Success message -->
35283
+ @if (isSuccess()) {
35284
+ <div class="feedback-alert success">
35285
+ <ion-icon name="checkmark-circle-outline"></ion-icon>
35286
+ <span>{{ props.successMessage || i18n.t('feedbackSuccess') }}</span>
35287
+ </div>
35288
+ }
35289
+
35290
+ <!-- Actions -->
35291
+ <div class="form-actions">
35292
+ @if (props.cancelButtonText) {
35293
+ <ion-button
35294
+ fill="outline"
35295
+ color="medium"
35296
+ type="button"
35297
+ (click)="onCancelClick()"
35298
+ >
35299
+ {{ props.cancelButtonText }}
35300
+ </ion-button>
35301
+ }
35302
+ <ion-button
35303
+ type="submit"
35304
+ [disabled]="form.invalid || isSubmitting()"
35305
+ expand="block"
35306
+ >
35307
+ @if (isSubmitting()) {
35308
+ <ion-spinner name="crescent"></ion-spinner>
35309
+ } @else {
35310
+ {{ props.submitButtonText || i18n.t('submit') }}
35311
+ }
35312
+ </ion-button>
35313
+ </div>
35314
+ </form>
35315
+ `, isInline: true, styles: [".feedback-form{display:flex;flex-direction:column;gap:8px;&.compact{gap:4px}}.form-actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end;ion-button{flex:1}}.feedback-alert{display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:8px;margin-top:8px;&.error{background-color:var(--ion-color-danger-tint);color:var(--ion-color-danger-shade)}&.success{background-color:var(--ion-color-success-tint);color:var(--ion-color-success-shade)}ion-icon{font-size:20px}}ion-note{font-size:12px;margin-top:-4px;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { 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: 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"] }] }); }
35316
+ }
35317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FeedbackFormComponent, decorators: [{
35318
+ type: Component,
35319
+ args: [{ selector: 'val-feedback-form', standalone: true, imports: [
35320
+ CommonModule,
35321
+ ReactiveFormsModule,
35322
+ IonButton,
35323
+ IonIcon,
35324
+ IonItem,
35325
+ IonLabel,
35326
+ IonList,
35327
+ IonNote,
35328
+ IonSelect,
35329
+ IonSelectOption,
35330
+ IonSpinner,
35331
+ IonText,
35332
+ IonTextarea,
35333
+ ], template: `
35334
+ <form
35335
+ [formGroup]="form"
35336
+ (ngSubmit)="handleSubmit()"
35337
+ class="feedback-form"
35338
+ [class.compact]="props.compact"
35339
+ [ngClass]="props.cssClass"
35340
+ >
35341
+ <!-- Type selector -->
35342
+ @if (props.showTypeSelector !== false) {
35343
+ <ion-item>
35344
+ <ion-select
35345
+ formControlName="type"
35346
+ [label]="i18n.t('feedbackType')"
35347
+ labelPlacement="floating"
35348
+ interface="popover"
35349
+ >
35350
+ @for (option of typeOptions; track option.value) {
35351
+ <ion-select-option [value]="option.value">
35352
+ {{ option.label }}
35353
+ </ion-select-option>
35354
+ }
35355
+ </ion-select>
35356
+ </ion-item>
35357
+ }
35358
+
35359
+ <!-- Title -->
35360
+ <ion-item>
35361
+ <ion-textarea
35362
+ formControlName="title"
35363
+ [label]="props.titleLabel || i18n.t('title')"
35364
+ labelPlacement="floating"
35365
+ [placeholder]="props.titlePlaceholder || i18n.t('titlePlaceholder')"
35366
+ [maxlength]="200"
35367
+ [counter]="true"
35368
+ [autoGrow]="false"
35369
+ rows="1"
35370
+ ></ion-textarea>
35371
+ </ion-item>
35372
+ @if (form.get('title')?.invalid && form.get('title')?.touched) {
35373
+ <ion-note color="danger" class="ion-padding-start">
35374
+ {{ i18n.t('titleValidation') }}
35375
+ </ion-note>
35376
+ }
35377
+
35378
+ <!-- Description -->
35379
+ <ion-item>
35380
+ <ion-textarea
35381
+ formControlName="description"
35382
+ [label]="props.descriptionLabel || i18n.t('description')"
35383
+ labelPlacement="floating"
35384
+ [placeholder]="props.descriptionPlaceholder || i18n.t('descriptionPlaceholder')"
35385
+ [maxlength]="5000"
35386
+ [counter]="true"
35387
+ [autoGrow]="true"
35388
+ rows="4"
35389
+ ></ion-textarea>
35390
+ </ion-item>
35391
+ @if (form.get('description')?.invalid && form.get('description')?.touched) {
35392
+ <ion-note color="danger" class="ion-padding-start">
35393
+ {{ i18n.t('descriptionValidation') }}
35394
+ </ion-note>
35395
+ }
35396
+
35397
+ <!-- Error message -->
35398
+ @if (error()) {
35399
+ <div class="feedback-alert error">
35400
+ <ion-icon name="close-circle-outline"></ion-icon>
35401
+ <span>{{ error() }}</span>
35402
+ </div>
35403
+ }
35404
+
35405
+ <!-- Success message -->
35406
+ @if (isSuccess()) {
35407
+ <div class="feedback-alert success">
35408
+ <ion-icon name="checkmark-circle-outline"></ion-icon>
35409
+ <span>{{ props.successMessage || i18n.t('feedbackSuccess') }}</span>
35410
+ </div>
35411
+ }
35412
+
35413
+ <!-- Actions -->
35414
+ <div class="form-actions">
35415
+ @if (props.cancelButtonText) {
35416
+ <ion-button
35417
+ fill="outline"
35418
+ color="medium"
35419
+ type="button"
35420
+ (click)="onCancelClick()"
35421
+ >
35422
+ {{ props.cancelButtonText }}
35423
+ </ion-button>
35424
+ }
35425
+ <ion-button
35426
+ type="submit"
35427
+ [disabled]="form.invalid || isSubmitting()"
35428
+ expand="block"
35429
+ >
35430
+ @if (isSubmitting()) {
35431
+ <ion-spinner name="crescent"></ion-spinner>
35432
+ } @else {
35433
+ {{ props.submitButtonText || i18n.t('submit') }}
35434
+ }
35435
+ </ion-button>
35436
+ </div>
35437
+ </form>
35438
+ `, styles: [".feedback-form{display:flex;flex-direction:column;gap:8px;&.compact{gap:4px}}.form-actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end;ion-button{flex:1}}.feedback-alert{display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:8px;margin-top:8px;&.error{background-color:var(--ion-color-danger-tint);color:var(--ion-color-danger-shade)}&.success{background-color:var(--ion-color-success-tint);color:var(--ion-color-success-shade)}ion-icon{font-size:20px}}ion-note{font-size:12px;margin-top:-4px;margin-bottom:8px}\n"] }]
35439
+ }], ctorParameters: () => [], propDecorators: { props: [{
35440
+ type: Input
35441
+ }], onSubmit: [{
35442
+ type: Output
35443
+ }], onCancel: [{
35444
+ type: Output
35445
+ }] } });
35446
+
35447
+ /**
35448
+ * Componente para reacciones de contenido con emojis.
35449
+ *
35450
+ * @example
35451
+ * ```html
35452
+ * <val-content-reaction
35453
+ * [props]="{
35454
+ * entityRef: { entityType: 'article', entityId: 'art-123' },
35455
+ * question: '¿Te fue útil este artículo?'
35456
+ * }"
35457
+ * (reactionSubmit)="onReactionSubmit($event)"
35458
+ * />
35459
+ * ```
35460
+ */
35461
+ class ContentReactionComponent {
35462
+ constructor() {
35463
+ this.feedbackService = inject(FeedbackService);
35464
+ this.toast = inject(ToastService);
35465
+ this.i18n = inject(I18nService);
35466
+ this.props = {};
35467
+ this.reactionSubmit = new EventEmitter();
35468
+ this.reactionChange = new EventEmitter();
35469
+ // Estado reactivo
35470
+ this.state = signal({
35471
+ selectedValue: null,
35472
+ comment: '',
35473
+ isLoading: false,
35474
+ isSubmitted: false,
35475
+ hadPreviousReaction: false,
35476
+ error: null,
35477
+ });
35478
+ // Valores por defecto
35479
+ this.defaultEmojis = ['😞', '😐', '😊'];
35480
+ this.defaultLabels = [
35481
+ 'No me ayudó',
35482
+ 'Regular',
35483
+ 'Muy útil',
35484
+ ];
35485
+ this.reactionValues = ['negative', 'neutral', 'positive'];
35486
+ // Computed properties
35487
+ this.resolvedProps = computed(() => ({
35488
+ entityRef: this.props.entityRef,
35489
+ question: this.props.question || this.t('question'),
35490
+ showComment: this.props.showComment ?? true,
35491
+ commentPlaceholder: this.props.commentPlaceholder || this.t('commentPlaceholder'),
35492
+ maxCommentLength: this.props.maxCommentLength ?? 500,
35493
+ emojis: this.props.emojis || this.defaultEmojis,
35494
+ emojiLabels: this.props.emojiLabels || this.defaultLabels,
35495
+ showThankYou: this.props.showThankYou ?? true,
35496
+ thankYouMessage: this.props.thankYouMessage || this.t('thankYou'),
35497
+ disabled: this.props.disabled ?? false,
35498
+ readonly: this.props.readonly ?? false,
35499
+ }));
35500
+ this.showCommentField = computed(() => this.state().selectedValue !== null && this.resolvedProps().showComment);
35501
+ this.canSubmit = computed(() => this.state().selectedValue !== null && !this.state().isLoading);
35502
+ }
35503
+ ngOnInit() {
35504
+ this.loadPreviousReaction();
35505
+ }
35506
+ ngOnChanges(changes) {
35507
+ if (changes['props'] && !changes['props'].firstChange) {
35508
+ this.loadPreviousReaction();
35509
+ }
35510
+ }
35511
+ async loadPreviousReaction() {
35512
+ if (!this.props.entityRef)
35513
+ return;
35514
+ this.state.update((s) => ({ ...s, isLoading: true, error: null }));
35515
+ try {
35516
+ const check = await this.feedbackService.checkFeedback(this.props.entityRef.entityType, this.props.entityRef.entityId);
35517
+ if (check.hasFeedback && check.reactionValue) {
35518
+ this.state.update((s) => ({
35519
+ ...s,
35520
+ selectedValue: check.reactionValue,
35521
+ hadPreviousReaction: true,
35522
+ isLoading: false,
35523
+ isSubmitted: true,
35524
+ }));
35525
+ }
35526
+ else {
35527
+ this.state.update((s) => ({ ...s, isLoading: false }));
35528
+ }
35529
+ }
35530
+ catch (error) {
35531
+ console.error('Error loading previous reaction:', error);
35532
+ this.state.update((s) => ({ ...s, isLoading: false }));
35533
+ }
35534
+ }
35535
+ selectReaction(value) {
35536
+ if (this.resolvedProps().disabled || this.resolvedProps().readonly)
35537
+ return;
35538
+ const previousValue = this.state().selectedValue;
35539
+ this.state.update((s) => ({
35540
+ ...s,
35541
+ selectedValue: value,
35542
+ isSubmitted: false,
35543
+ error: null,
35544
+ }));
35545
+ this.reactionChange.emit({ value, previousValue });
35546
+ }
35547
+ async submitReaction() {
35548
+ const currentState = this.state();
35549
+ const props = this.resolvedProps();
35550
+ if (!currentState.selectedValue || props.disabled)
35551
+ return;
35552
+ this.state.update((s) => ({ ...s, isLoading: true, error: null }));
35553
+ try {
35554
+ await this.feedbackService.createReaction(props.entityRef, currentState.selectedValue, currentState.comment || undefined);
35555
+ this.state.update((s) => ({
35556
+ ...s,
35557
+ isLoading: false,
35558
+ isSubmitted: true,
35559
+ hadPreviousReaction: true,
35560
+ }));
35561
+ this.reactionSubmit.emit({
35562
+ value: currentState.selectedValue,
35563
+ comment: currentState.comment || undefined,
35564
+ entityRef: props.entityRef,
35565
+ isUpdate: currentState.hadPreviousReaction,
35566
+ });
35567
+ if (props.showThankYou) {
35568
+ this.toast.show({
35569
+ message: props.thankYouMessage,
35570
+ duration: 2000,
35571
+ position: 'bottom',
35572
+ color: 'success',
35573
+ });
35574
+ }
35575
+ }
35576
+ catch (error) {
35577
+ console.error('Error submitting reaction:', error);
35578
+ this.state.update((s) => ({
35579
+ ...s,
35580
+ isLoading: false,
35581
+ error: this.t('errorSubmitting'),
35582
+ }));
35583
+ this.toast.show({
35584
+ message: this.t('errorSubmitting'),
35585
+ duration: 3000,
35586
+ position: 'bottom',
35587
+ color: 'danger',
35588
+ });
35589
+ }
35590
+ }
35591
+ updateComment(event) {
35592
+ const value = event.detail.value || '';
35593
+ this.state.update((s) => ({ ...s, comment: value }));
35594
+ }
35595
+ getEmoji(index) {
35596
+ return this.resolvedProps().emojis[index];
35597
+ }
35598
+ getEmojiLabel(index) {
35599
+ return this.resolvedProps().emojiLabels[index];
35600
+ }
35601
+ isSelected(value) {
35602
+ return this.state().selectedValue === value;
35603
+ }
35604
+ t(key) {
35605
+ const translations = {
35606
+ question: '¿Te resultó útil este contenido?',
35607
+ commentPlaceholder: 'Cuéntanos más (opcional)...',
35608
+ submit: 'Enviar opinión',
35609
+ update: 'Actualizar opinión',
35610
+ thankYou: '¡Gracias por tu opinión!',
35611
+ submitted: 'Tu opinión ha sido registrada',
35612
+ errorSubmitting: 'Error al enviar. Intenta de nuevo.',
35613
+ };
35614
+ return this.i18n.t(key, 'ContentReaction') || translations[key] || key;
35615
+ }
35616
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
35617
+ 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: "<div\n class=\"content-reaction\"\n [class.disabled]=\"resolvedProps().disabled\"\n [class.readonly]=\"resolvedProps().readonly\"\n>\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\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of reactionValues; track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\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\">\n {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }}\n </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button\n expand=\"block\"\n [disabled]=\"!canSubmit()\"\n (click)=\"submitReaction()\"\n class=\"submit-button\"\n >\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else {\n {{ state().hadPreviousReaction ? t('update') : t('submit') }}\n }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">\n {{ t('submitted') }}\n </p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n }\n }\n</div>\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:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.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:14px}.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"] }] }); }
35618
+ }
35619
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContentReactionComponent, decorators: [{
35620
+ type: Component,
35621
+ args: [{ selector: 'val-content-reaction', standalone: true, imports: [CommonModule, FormsModule, IonButton, IonSpinner, IonTextarea], template: "<div\n class=\"content-reaction\"\n [class.disabled]=\"resolvedProps().disabled\"\n [class.readonly]=\"resolvedProps().readonly\"\n>\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\">{{ resolvedProps().question }}</p>\n\n <!-- Emojis -->\n <div class=\"emoji-container\">\n @for (value of reactionValues; track value; let i = $index) {\n <button\n type=\"button\"\n class=\"emoji-button\"\n [class.selected]=\"isSelected(value)\"\n [class.negative]=\"value === 'negative' && isSelected(value)\"\n [class.neutral]=\"value === 'neutral' && isSelected(value)\"\n [class.positive]=\"value === 'positive' && isSelected(value)\"\n [attr.aria-label]=\"getEmojiLabel(i)\"\n [attr.aria-pressed]=\"isSelected(value)\"\n [disabled]=\"resolvedProps().disabled || resolvedProps().readonly\"\n (click)=\"selectReaction(value)\"\n >\n <span class=\"emoji\">{{ getEmoji(i) }}</span>\n </button>\n }\n </div>\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\">\n {{ state().comment.length }}/{{ resolvedProps().maxCommentLength }}\n </span>\n </div>\n }\n\n <!-- Bot\u00F3n de env\u00EDo -->\n @if (state().selectedValue && !state().isSubmitted) {\n <ion-button\n expand=\"block\"\n [disabled]=\"!canSubmit()\"\n (click)=\"submitReaction()\"\n class=\"submit-button\"\n >\n @if (state().isLoading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else {\n {{ state().hadPreviousReaction ? t('update') : t('submit') }}\n }\n </ion-button>\n }\n\n <!-- Mensaje de confirmaci\u00F3n -->\n @if (state().isSubmitted) {\n <p class=\"submitted-message\">\n {{ t('submitted') }}\n </p>\n }\n\n <!-- Error -->\n @if (state().error) {\n <p class=\"error-message\">{{ state().error }}</p>\n }\n }\n</div>\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:16px;font-weight:500;color:var(--ion-color-dark);margin:0 0 16px}.emoji-container{display:flex;justify-content:center;gap:24px;margin-bottom:20px}.emoji-button{background:transparent;border:2px solid var(--ion-color-light-shade);border-radius:50%;width:64px;height:64px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:center;padding:0}.emoji-button .emoji{font-size:32px;transition:transform .3s cubic-bezier(.4,0,.2,1);line-height:1}.emoji-button:hover:not(:disabled){transform:scale(1.1);border-color:var(--ion-color-medium)}.emoji-button:focus{outline:2px solid var(--ion-color-primary);outline-offset:2px}.emoji-button.selected{transform:scale(1.2)}.emoji-button.selected .emoji{transform:scale(1.1)}.emoji-button.selected.negative{border-color:var(--ion-color-danger);background:var(--ion-color-danger-tint);box-shadow:0 4px 12px rgba(var(--ion-color-danger-rgb),.3)}.emoji-button.selected.neutral{border-color:var(--ion-color-warning);background:var(--ion-color-warning-tint);box-shadow:0 4px 12px rgba(var(--ion-color-warning-rgb),.3)}.emoji-button.selected.positive{border-color:var(--ion-color-success);background:var(--ion-color-success-tint);box-shadow:0 4px 12px rgba(var(--ion-color-success-rgb),.3)}.emoji-button:disabled{cursor:not-allowed;opacity:.5}.comment-section{margin-top:16px;animation:slideIn .3s ease-out}.comment-section .comment-textarea{--background: var(--ion-color-light);--border-radius: 8px;--padding-start: 12px;--padding-end: 12px;width:100%}.comment-section .char-count{display:block;text-align:right;font-size:12px;color:var(--ion-color-medium);margin-top:4px}.submit-button{margin-top:16px;--border-radius: 8px}.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:14px}.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"] }]
35622
+ }], propDecorators: { props: [{
35623
+ type: Input
35624
+ }], reactionSubmit: [{
35625
+ type: Output
35626
+ }], reactionChange: [{
35627
+ type: Output
35628
+ }] } });
35629
+
34764
35630
  addIcons({ menuOutline, closeOutline });
34765
35631
  /**
34766
35632
  * val-docs-layout
@@ -36718,6 +37584,271 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
36718
37584
  `, styles: [".docs-callout{--callout-bg: #fffbeb;--callout-border: #f59e0b;--callout-title: #92400e;--callout-text: #78350f;margin:1.5rem 0;padding:1rem 1.25rem;border-left:4px solid var(--callout-border);border-radius:0 8px 8px 0;background:var(--callout-bg)}.docs-callout__title{font-size:.9375rem;font-weight:700;color:var(--callout-title);margin-bottom:.5rem}.docs-callout__content{font-size:.9375rem;line-height:1.7;color:var(--callout-text)}.docs-callout__content p{margin:0}.docs-callout__content p+p{margin-top:.75rem}.docs-callout__content code{font-family:SF Mono,Fira Code,Monaco,Consolas,monospace;font-size:.875rem;background:#0000000f;padding:.125rem .375rem;border-radius:4px}.docs-callout__content a{color:var(--callout-title);text-decoration:underline;text-underline-offset:2px}.docs-callout__content a:hover{text-decoration:none}.docs-callout--note{--callout-bg: #fffbeb;--callout-border: #f59e0b;--callout-title: #92400e;--callout-text: #78350f}.docs-callout--warning{--callout-bg: #fff7ed;--callout-border: #f97316;--callout-title: #c2410c;--callout-text: #9a3412}.docs-callout--tip{--callout-bg: #ecfdf5;--callout-border: #10b981;--callout-title: #065f46;--callout-text: #047857}.docs-callout--danger{--callout-bg: #fef2f2;--callout-border: #ef4444;--callout-title: #991b1b;--callout-text: #b91c1c}.docs-callout--info{--callout-bg: #eff6ff;--callout-border: #3b82f6;--callout-title: #1e40af;--callout-text: #1d4ed8}:host-context(.dark) .docs-callout--note,:host-context([color-scheme=dark]) .docs-callout--note{--callout-bg: rgba(245, 158, 11, .1);--callout-border: #f59e0b;--callout-title: #fbbf24;--callout-text: #fcd34d}:host-context(.dark) .docs-callout--warning,:host-context([color-scheme=dark]) .docs-callout--warning{--callout-bg: rgba(249, 115, 22, .1);--callout-border: #f97316;--callout-title: #fb923c;--callout-text: #fdba74}:host-context(.dark) .docs-callout--tip,:host-context([color-scheme=dark]) .docs-callout--tip{--callout-bg: rgba(16, 185, 129, .1);--callout-border: #10b981;--callout-title: #34d399;--callout-text: #6ee7b7}:host-context(.dark) .docs-callout--danger,:host-context([color-scheme=dark]) .docs-callout--danger{--callout-bg: rgba(239, 68, 68, .1);--callout-border: #ef4444;--callout-title: #f87171;--callout-text: #fca5a5}:host-context(.dark) .docs-callout--info,:host-context([color-scheme=dark]) .docs-callout--info{--callout-bg: rgba(59, 130, 246, .1);--callout-border: #3b82f6;--callout-title: #60a5fa;--callout-text: #93c5fd}:host-context(.dark) .docs-callout__content code,:host-context([color-scheme=dark]) .docs-callout__content code{background:#ffffff1a}\n"] }]
36719
37585
  }] });
36720
37586
 
37587
+ /**
37588
+ * val-docs-section
37589
+ *
37590
+ * A semantic section wrapper for documentation pages.
37591
+ * Automatically creates headings with IDs for TOC linking.
37592
+ *
37593
+ * @example Basic usage
37594
+ * ```html
37595
+ * <val-docs-section [props]="{ id: 'installation', title: 'Installation' }">
37596
+ * <p>Install the package using npm...</p>
37597
+ * <val-docs-code-example [props]="codeExample"></val-docs-code-example>
37598
+ * </val-docs-section>
37599
+ * ```
37600
+ *
37601
+ * @example With level 3 heading
37602
+ * ```html
37603
+ * <val-docs-section [props]="{ id: 'npm', title: 'Using npm', level: 3 }">
37604
+ * <val-docs-code-example [props]="npmExample"></val-docs-code-example>
37605
+ * </val-docs-section>
37606
+ * ```
37607
+ */
37608
+ class DocsSectionComponent {
37609
+ constructor() {
37610
+ this.props = { id: '', title: '' };
37611
+ }
37612
+ get level() {
37613
+ return this.props.level ?? 2;
37614
+ }
37615
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
37616
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DocsSectionComponent, isStandalone: true, selector: "val-docs-section", inputs: { props: "props" }, ngImport: i0, template: `
37617
+ <section
37618
+ [id]="props.id"
37619
+ class="docs-section"
37620
+ [class]="props.cssClass"
37621
+ [class.docs-section--level-2]="level === 2"
37622
+ [class.docs-section--level-3]="level === 3"
37623
+ >
37624
+ @if (level === 2) {
37625
+ <h2 class="docs-section__title">{{ props.title }}</h2>
37626
+ } @else {
37627
+ <h3 class="docs-section__title">{{ props.title }}</h3>
37628
+ }
37629
+
37630
+ @if (props.description) {
37631
+ <p class="docs-section__description">{{ props.description }}</p>
37632
+ }
37633
+
37634
+ <div class="docs-section__content">
37635
+ <ng-content></ng-content>
37636
+ </div>
37637
+ </section>
37638
+ `, isInline: true, styles: [".docs-section{margin-bottom:2.5rem}.docs-section__title{margin:0 0 1rem;font-weight:600;color:var(--ion-text-color);scroll-margin-top:80px}.docs-section--level-2 .docs-section__title{font-size:1.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--ion-border-color, #e0e0e0)}.docs-section--level-3 .docs-section__title{font-size:1.25rem}.docs-section__description{margin:0 0 1.25rem;color:var(--ion-color-medium);font-size:1rem;line-height:1.6}.docs-section__content>*:last-child{margin-bottom:0}:host-context(.dark) .docs-section--level-2 .docs-section__title,:host-context([color-scheme=\"dark\"]) .docs-section--level-2 .docs-section__title{border-bottom-color:var(--ion-border-color, #333)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
37639
+ }
37640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsSectionComponent, decorators: [{
37641
+ type: Component,
37642
+ args: [{ selector: 'val-docs-section', standalone: true, imports: [CommonModule], template: `
37643
+ <section
37644
+ [id]="props.id"
37645
+ class="docs-section"
37646
+ [class]="props.cssClass"
37647
+ [class.docs-section--level-2]="level === 2"
37648
+ [class.docs-section--level-3]="level === 3"
37649
+ >
37650
+ @if (level === 2) {
37651
+ <h2 class="docs-section__title">{{ props.title }}</h2>
37652
+ } @else {
37653
+ <h3 class="docs-section__title">{{ props.title }}</h3>
37654
+ }
37655
+
37656
+ @if (props.description) {
37657
+ <p class="docs-section__description">{{ props.description }}</p>
37658
+ }
37659
+
37660
+ <div class="docs-section__content">
37661
+ <ng-content></ng-content>
37662
+ </div>
37663
+ </section>
37664
+ `, styles: [".docs-section{margin-bottom:2.5rem}.docs-section__title{margin:0 0 1rem;font-weight:600;color:var(--ion-text-color);scroll-margin-top:80px}.docs-section--level-2 .docs-section__title{font-size:1.5rem;padding-bottom:.5rem;border-bottom:1px solid var(--ion-border-color, #e0e0e0)}.docs-section--level-3 .docs-section__title{font-size:1.25rem}.docs-section__description{margin:0 0 1.25rem;color:var(--ion-color-medium);font-size:1rem;line-height:1.6}.docs-section__content>*:last-child{margin-bottom:0}:host-context(.dark) .docs-section--level-2 .docs-section__title,:host-context([color-scheme=\"dark\"]) .docs-section--level-2 .docs-section__title{border-bottom-color:var(--ion-border-color, #333)}\n"] }]
37665
+ }], propDecorators: { props: [{
37666
+ type: Input
37667
+ }] } });
37668
+
37669
+ /**
37670
+ * val-docs-page
37671
+ *
37672
+ * A complete documentation page template that eliminates boilerplate.
37673
+ * Provides automatic TOC generation, navigation links, and consistent styling.
37674
+ *
37675
+ * @example Basic usage
37676
+ * ```html
37677
+ * <val-docs-page [props]="{
37678
+ * title: 'Button',
37679
+ * lead: 'A clickable element for user interactions.',
37680
+ * previousPage: { title: 'Quick Start', route: ['/docs', 'quick-start'] },
37681
+ * nextPage: { title: 'Card', route: ['/docs', 'components', 'card'] }
37682
+ * }">
37683
+ * <val-docs-section [props]="{ id: 'basic-usage', title: 'Basic Usage' }">
37684
+ * <p>Content here...</p>
37685
+ * </val-docs-section>
37686
+ *
37687
+ * <val-docs-section [props]="{ id: 'variants', title: 'Variants' }">
37688
+ * <p>More content...</p>
37689
+ * </val-docs-section>
37690
+ * </val-docs-page>
37691
+ * ```
37692
+ *
37693
+ * @example With badge
37694
+ * ```html
37695
+ * <val-docs-page [props]="{
37696
+ * title: 'New Component',
37697
+ * badge: 'New',
37698
+ * badgeColor: 'success'
37699
+ * }">
37700
+ * ...
37701
+ * </val-docs-page>
37702
+ * ```
37703
+ */
37704
+ class DocsPageComponent {
37705
+ constructor() {
37706
+ this.elementRef = inject(ElementRef);
37707
+ this.props = { title: '' };
37708
+ this.tocItems = signal([]);
37709
+ this.observer = null;
37710
+ this.tocProps = computed(() => ({
37711
+ title: this.props.toc?.title ?? 'On this page',
37712
+ items: this.tocItems(),
37713
+ }));
37714
+ this.navLinksProps = computed(() => ({
37715
+ previous: this.props.previousPage
37716
+ ? {
37717
+ label: this.props.navLabels?.previous ?? 'Previous',
37718
+ title: this.props.previousPage.title,
37719
+ route: this.props.previousPage.route,
37720
+ }
37721
+ : undefined,
37722
+ next: this.props.nextPage
37723
+ ? {
37724
+ label: this.props.navLabels?.next ?? 'Next',
37725
+ title: this.props.nextPage.title,
37726
+ route: this.props.nextPage.route,
37727
+ }
37728
+ : undefined,
37729
+ }));
37730
+ this.showNavLinks = computed(() => {
37731
+ return !!this.props.previousPage || !!this.props.nextPage;
37732
+ });
37733
+ }
37734
+ ngAfterViewInit() {
37735
+ // Initial scan
37736
+ this.scanForSections();
37737
+ // Watch for dynamic content changes
37738
+ this.observer = new MutationObserver(() => {
37739
+ this.scanForSections();
37740
+ });
37741
+ const contentEl = this.elementRef.nativeElement.querySelector('.docs-page__sections');
37742
+ if (contentEl) {
37743
+ this.observer.observe(contentEl, { childList: true, subtree: true });
37744
+ }
37745
+ }
37746
+ ngOnDestroy() {
37747
+ this.observer?.disconnect();
37748
+ }
37749
+ scanForSections() {
37750
+ const contentEl = this.elementRef.nativeElement.querySelector('.docs-page__sections');
37751
+ if (!contentEl)
37752
+ return;
37753
+ const headings = contentEl.querySelectorAll('h2[id], h3[id], section[id] > h2, section[id] > h3');
37754
+ const items = [];
37755
+ headings.forEach((heading) => {
37756
+ // Get ID from heading or parent section
37757
+ let id = heading.id;
37758
+ if (!id && heading.parentElement?.tagName === 'SECTION') {
37759
+ id = heading.parentElement.id;
37760
+ }
37761
+ if (id) {
37762
+ const level = heading.tagName === 'H2' ? 2 : 3;
37763
+ items.push({ id, label: heading.textContent?.trim() || '', level });
37764
+ }
37765
+ });
37766
+ this.tocItems.set(items);
37767
+ }
37768
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
37769
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DocsPageComponent, isStandalone: true, selector: "val-docs-page", inputs: { props: "props" }, ngImport: i0, template: `
37770
+ <div class="docs-page" [class]="props.cssClass">
37771
+ <div class="docs-page__content" #content>
37772
+ <header class="docs-page__header">
37773
+ <div class="docs-page__title-row">
37774
+ <h1 class="docs-page__title">{{ props.title }}</h1>
37775
+ @if (props.badge) {
37776
+ <span
37777
+ class="docs-page__badge"
37778
+ [class.docs-page__badge--success]="props.badgeColor === 'success'"
37779
+ [class.docs-page__badge--warning]="props.badgeColor === 'warning'"
37780
+ [class.docs-page__badge--danger]="props.badgeColor === 'danger'"
37781
+ >
37782
+ {{ props.badge }}
37783
+ </span>
37784
+ }
37785
+ </div>
37786
+ @if (props.lead) {
37787
+ <p class="docs-page__lead">{{ props.lead }}</p>
37788
+ }
37789
+ </header>
37790
+
37791
+ <div class="docs-page__sections">
37792
+ <ng-content></ng-content>
37793
+ </div>
37794
+
37795
+ @if (showNavLinks()) {
37796
+ <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
37797
+ }
37798
+ </div>
37799
+
37800
+ @if (!props.toc?.hide) {
37801
+ <aside class="docs-page__toc">
37802
+ <val-docs-toc [props]="tocProps()"></val-docs-toc>
37803
+ </aside>
37804
+ }
37805
+ </div>
37806
+ `, isInline: true, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem;@media (min-width: 1200px){grid-template-columns:1fr 220px;padding:2rem}&__content{min-width:0;max-width:900px}&__header{margin-bottom:2rem}&__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}&__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color);line-height:1.2;@media (min-width: 768px){font-size:2.5rem}}&__badge{display:inline-flex;align-items:center;padding:.25rem .625rem;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.025em;border-radius:4px;background:#00000014;color:var(--ion-color-medium-shade);&--success{background:var(--ion-color-success-tint, #e8f5e9);color:var(--ion-color-success-shade, #2e7d32)}&--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}&--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}}&__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium)}&__sections{>*:last-child{margin-bottom:0}}&__toc{display:none;@media (min-width: 1200px){display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}}// Dark mode :host-context(.dark),:host-context([color-scheme=\"dark\"]){.docs-page{&__badge{background:#ffffff1a;color:var(--ion-color-medium);&--success{background:#2e7d3233;color:#81c784}&--warning{background:#e6510033;color:#ffb74d}&--danger{background:#c6282833;color:#e57373}}}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DocsNavLinksComponent, selector: "val-docs-nav-links", inputs: ["props"], outputs: ["navigate"] }, { kind: "component", type: DocsTocComponent, selector: "val-docs-toc", inputs: ["props"], outputs: ["sectionChange"] }] }); }
37807
+ }
37808
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DocsPageComponent, decorators: [{
37809
+ type: Component,
37810
+ args: [{ selector: 'val-docs-page', standalone: true, imports: [CommonModule, DocsNavLinksComponent, DocsTocComponent], template: `
37811
+ <div class="docs-page" [class]="props.cssClass">
37812
+ <div class="docs-page__content" #content>
37813
+ <header class="docs-page__header">
37814
+ <div class="docs-page__title-row">
37815
+ <h1 class="docs-page__title">{{ props.title }}</h1>
37816
+ @if (props.badge) {
37817
+ <span
37818
+ class="docs-page__badge"
37819
+ [class.docs-page__badge--success]="props.badgeColor === 'success'"
37820
+ [class.docs-page__badge--warning]="props.badgeColor === 'warning'"
37821
+ [class.docs-page__badge--danger]="props.badgeColor === 'danger'"
37822
+ >
37823
+ {{ props.badge }}
37824
+ </span>
37825
+ }
37826
+ </div>
37827
+ @if (props.lead) {
37828
+ <p class="docs-page__lead">{{ props.lead }}</p>
37829
+ }
37830
+ </header>
37831
+
37832
+ <div class="docs-page__sections">
37833
+ <ng-content></ng-content>
37834
+ </div>
37835
+
37836
+ @if (showNavLinks()) {
37837
+ <val-docs-nav-links [props]="navLinksProps()"></val-docs-nav-links>
37838
+ }
37839
+ </div>
37840
+
37841
+ @if (!props.toc?.hide) {
37842
+ <aside class="docs-page__toc">
37843
+ <val-docs-toc [props]="tocProps()"></val-docs-toc>
37844
+ </aside>
37845
+ }
37846
+ </div>
37847
+ `, styles: [".docs-page{display:grid;grid-template-columns:1fr;gap:2rem;max-width:1400px;margin:0 auto;padding:2rem 1.5rem;@media (min-width: 1200px){grid-template-columns:1fr 220px;padding:2rem}&__content{min-width:0;max-width:900px}&__header{margin-bottom:2rem}&__title-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}&__title{margin:0;font-size:2rem;font-weight:700;color:var(--ion-text-color);line-height:1.2;@media (min-width: 768px){font-size:2.5rem}}&__badge{display:inline-flex;align-items:center;padding:.25rem .625rem;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.025em;border-radius:4px;background:#00000014;color:var(--ion-color-medium-shade);&--success{background:var(--ion-color-success-tint, #e8f5e9);color:var(--ion-color-success-shade, #2e7d32)}&--warning{background:var(--ion-color-warning-tint, #fff3e0);color:var(--ion-color-warning-shade, #e65100)}&--danger{background:var(--ion-color-danger-tint, #ffebee);color:var(--ion-color-danger-shade, #c62828)}}&__lead{margin:1rem 0 0;font-size:1.125rem;line-height:1.7;color:var(--ion-color-medium)}&__sections{>*:last-child{margin-bottom:0}}&__toc{display:none;@media (min-width: 1200px){display:block;position:sticky;top:2rem;height:fit-content;max-height:calc(100vh - 4rem);overflow-y:auto}}}// Dark mode :host-context(.dark),:host-context([color-scheme=\"dark\"]){.docs-page{&__badge{background:#ffffff1a;color:var(--ion-color-medium);&--success{background:#2e7d3233;color:#81c784}&--warning{background:#e6510033;color:#ffb74d}&--danger{background:#c6282833;color:#e57373}}}}\n"] }]
37848
+ }], propDecorators: { props: [{
37849
+ type: Input
37850
+ }] } });
37851
+
36721
37852
  /*
36722
37853
  * Public API Surface of valtech-components
36723
37854
  */
@@ -36726,5 +37857,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
36726
37857
  * Generated bundle index. Do not edit.
36727
37858
  */
36728
37859
 
36729
- export { AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, ArticleBuilder, ArticleComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsSearchComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTION, MenuComponent, MessagingService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, VALTECH_ADS_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_DEFAULT_CONTENT, VALTECH_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
37860
+ export { AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AccordionComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, ArticleBuilder, ArticleComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BannerComponent, BaseDefault, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, ContentReactionComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTION, MenuComponent, MessagingService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, VALTECH_ADS_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
36730
37861
  //# sourceMappingURL=valtech-components.mjs.map